diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bda23701..0cf2dc72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: build_api: runs-on: ubuntu-latest container: - image: python:3.9 + image: python:3.12.2 options: --user root steps: - name: Checkout ${{ github.event.repository.name }} @@ -128,9 +128,9 @@ jobs: sonar.projectKey=usdot-jpo-ode_jpo-cvmanager sonar.projectName=jpo-cvmanager sonar.python.coverage.reportPaths=$GITHUB_WORKSPACE/services/cov.xml - sonar.python.version=3.12 + sonar.python.version=3.12.2 api.sonar.projectBaseDir=$GITHUB_WORKSPACE/services - api.sonar.sources=addons/images/bsm_query,addons/images/count_metric,addons/images/firmware_manager,addons/images/iss_health_check,addons/images/rsu_ping,api/src,common/pgquery.py + api.sonar.sources=addons/images/geo_msg_query,addons/images/count_metric,addons/images/firmware_manager,addons/images/iss_health_check,addons/images/rsu_status_check,api/src,common/pgquery.py api.sonar.tests=addons/tests,api/tests,common/tests webapp.sonar.projectBaseDir=$GITHUB_WORKSPACE/webapp webapp.sonar.sources=src diff --git a/.gitignore b/.gitignore index bc3ddab6..ce60120a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ cov.xml .venv cov_html htmlcov -.pytest_cache \ No newline at end of file +.pytest_cache +local_blob_storage diff --git a/.vscode/settings.json b/.vscode/settings.json index 7ba6ab22..ac1ec05f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,7 @@ }, "python.envFile": "${workspaceFolder}/.env", "terminal.integrated.env.windows": { - "PYTHONPATH": "${workspaceFolder}/services;${workspaceFolder}/services/addons/images/bsm_query;${workspaceFolder}/services/addons/images/count_metric;${workspaceFolder}/services/addons/images/firmware_manager;${workspaceFolder}/services/addons/images/iss_health_check;${workspaceFolder}/services/addons/images/rsu_ping_fetch;${workspaceFolder}/services/api/src;${workspaceFolder}/services/common" + "PYTHONPATH": "${workspaceFolder}/services;${workspaceFolder}/services/addons/images/geo_msg_query;${workspaceFolder}/services/addons/images/count_metric;${workspaceFolder}/services/addons/images/firmware_manager;${workspaceFolder}/services/addons/images/iss_health_check;${workspaceFolder}/services/addons/images/rsu_status_check;${workspaceFolder}/services/api/src;${workspaceFolder}/services/common" }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", diff --git a/README.md b/README.md index ecf65f35..56425ad5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The JPO Connected Vehicle Manager is a web-based application that helps an organ GUI: ReactJS with Redux Toolkit and Mapbox GL -API: Python +API: Python 3.12.2 Features: @@ -88,7 +88,46 @@ The following steps are intended to help get a new user up and running the JPO C ### Debugging -Note that it is recommended to work with the Python API from a [virtual environment](https://docs.python.org/3/library/venv.html). See [Visual Studio Code](https://code.visualstudio.com/docs/python/environments) documentation for more information on how to set up a virtual environment in VS Code. +Note that it is recommended to work with the Python API from a [virtual environment](https://docs.python.org/3/library/venv.html). + +#### Setting up a virtual environment from the command line + +1. Verify that you have Python 3.12.2 installed on your machine by running the following command: + ```bash + python3.12 --version + ``` + ```cmd + python --version + ``` + If you have a different version installed, download and install Python 3.12.2 from the [Python website](https://www.python.org/downloads/). +2. Open a terminal and navigate to the root of the project. +3. Run the following command to create a virtual environment in the project root: + ```bash + python3.12 -m venv .venv + ``` + ```cmd + python -m venv .venv + ``` +4. Activate the virtual environment: + ```bash + source .venv/bin/activate + ``` + ```cmd + .venv\Scripts\activate + ``` +5. Install the required packages: + ```bash + pip3.12 install -r services/requirements.txt + ``` + ```cmd + pip install -r services/requirements.txt + ``` + +#### Setting up a virtual environment with VSCode + +See [Visual Studio Code](https://code.visualstudio.com/docs/python/environments) documentation for information on how to set up a virtual environment with VS Code. + +#### Debugging Profile A debugging profile has been set up for use with VSCode to allow ease of debugging with this application. To use this profile, simply open the project in VSCode and select the "Debug" tab on the left side of the screen. Then, select the "Debug Solution" profile and click the green play button. This will spin up a postgresql instance as well as the keycloak auth solution within docker containers. Once running, this will also start the debugger and attach it to the running API container. You can then set breakpoints and step through the code as needed. @@ -137,6 +176,7 @@ For the "Debug Solution" to run properly on Windows 10/11 using WSL, the followi - WEBAPP_DOMAIN: The domain that the webapp will run on. This is required for Keycloak CORS authentication. - API_URI: The endpoint for the CV manager API, must be on a Keycloak Authorized domain. - COUNT_MESSAGE_TYPES: List of CV message types to query for counts. +- VIEWER_MSG_TYPES: List of CV message types to query geospatially. - DOT_NAME: The name of the DOT using the CV Manager. - MAPBOX_INIT_LATITUDE: Initial latitude value to use for MapBox view state. - MAPBOX_INIT_LONGITUDE: Initial longitude value to use for MapBox view state. @@ -144,10 +184,8 @@ For the "Debug Solution" to run properly on Windows 10/11 using WSL, the followi API Variables -- COUNTS_DB_TYPE: Set to either "MongoDB" or "BigQuery" depending on where the message counts are stored. - COUNTS_MSG_TYPES: Set to a list of message types to include in counts query. Sample format is described in the sample.env. -- COUNTS_DB_NAME: The BigQuery table or MongoDB collection name where the RSU message counts are located. -- BSM_DB_NAME: The database name for BSM visualization data. +- GEO_DB_NAME: The database name for geospatial message visualization data. This is currently only supported for BSM and PSM message types. - SSM_DB_NAME: The database name for SSM visualization data. - SRM_DB_NAME: The database name for SRM visualization data. - FIRMWARE_MANAGER_ENDPOINT: Endpoint for the firmware manager deployment's API. @@ -158,7 +196,7 @@ For the "Debug Solution" to run properly on Windows 10/11 using WSL, the followi - CSM_TARGET_SMTP_SERVER_ADDRESS: Destination SMTP server address. - CSM_TARGET_SMTP_SERVER_PORT: Destination SMTP server port. - API_LOGGING_LEVEL: The level of which the CV Manager API will log. (DEBUG, INFO, WARNING, ERROR) -- WZDX_ENDPOINT: WZDX datafeed enpoint. +- WZDX_ENDPOINT: WZDX datafeed endpoint. - WZDX_API_KEY: API key for the WZDX datafeed. - TIMEZONE: Timezone to be used for the API. - GOOGLE_APPLICATION_CREDENTIALS: Path to the GCP service account credentials file. Attached as a volume to the CV manager API service. @@ -172,7 +210,7 @@ For the "Debug Solution" to run properly on Windows 10/11 using WSL, the followi MongoDB Variables -- MONGO_DB_URI: URI for the MongoDB connection. +- MONGO_DB_URI: URI for the MongoDB connections. - MONGO_DB_NAME: Database name for RSU counts. Keycloak Variables @@ -189,6 +227,8 @@ For the "Debug Solution" to run properly on Windows 10/11 using WSL, the followi - GOOGLE_CLIENT_ID: GCP OAuth2.0 client ID for SSO Authentication within keycloak. - GOOGLE_CLIENT_SECRET: GCP OAuth2.0 client secret for SSO Authentication within keycloak. +Environment variables from addon services can also be set in the main `.env` file. These variables are defined in their own `README` files in the `services/addons/images` location of this repository. + ## License Information Licensed under the Apache License, Version 2.0 (the "License"); you may not use this diff --git a/docker-compose-addons.yml b/docker-compose-addons.yml index ee312543..0cd834c1 100644 --- a/docker-compose-addons.yml +++ b/docker-compose-addons.yml @@ -4,16 +4,20 @@ include: - docker-compose.yml services: - # ADDONS: - jpo_bsm_query: + jpo_geo_msg_query: build: context: ./services - dockerfile: Dockerfile.bsm_query - image: bsm_query:latest - restart: always + dockerfile: Dockerfile.geo_msg_query + image: geo_msg_query:latest + restart: on-failure:3 + environment: + MONGO_DB_URI: ${MONGO_DB_URI} + MONGO_DB_NAME: ${MONGO_DB_NAME} + MONGO_INPUT_COLLECTIONS: ${GEO_INPUT_COLLECTIONS} + MONGO_GEO_OUTPUT_COLLECTION: ${GEO_DB_NAME} + MONGO_TTL: ${GEO_TTL_DURATION} - env_file: - - ./services/addons/images/bsm_query/.env + LOGGING_LEVEL: ${GEO_LOGGING_LEVEL} logging: options: max-size: '10m' @@ -24,26 +28,64 @@ services: context: ./services dockerfile: Dockerfile.count_metric image: count_metric:latest - restart: always + restart: on-failure:3 + environment: + ENABLE_EMAILER: ${ENABLE_EMAILER} + DEPLOYMENT_TITLE: ${DEPLOYMENT_TITLE} + + SMTP_SERVER_IP: ${SMTP_SERVER_IP} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_EMAIL: ${SMTP_EMAIL} + SMTP_EMAIL_RECIPIENTS: ${SMTP_EMAIL_RECIPIENTS} + + MESSAGE_TYPES: ${COUNT_MESSAGE_TYPES} + PROJECT_ID: ${GCP_PROJECT_ID} + ODE_KAFKA_BROKERS: ${ODE_KAFKA_BROKERS} + + PG_DB_HOST: ${PG_DB_HOST} + PG_DB_NAME: ${PG_DB_NAME} + PG_DB_USER: ${PG_DB_USER} + PG_DB_PASS: ${PG_DB_PASS} - env_file: - - ./services/addons/images/count_metric/.env + DESTINATION_DB: ${COUNT_DESTINATION_DB} + + MONGO_DB_URI: ${MONGO_DB_URI} + MONGO_DB_NAME: ${MONGO_DB_NAME} + INPUT_COUNTS_MONGO_COLLECTION_NAME: ${INPUT_COUNTS_MONGO_COLLECTION_NAME} + OUTPUT_COUNTS_MONGO_COLLECTION_NAME: ${OUTPUT_COUNTS_MONGO_COLLECTION_NAME} + + KAFKA_BIGQUERY_TABLENAME: ${KAFKA_BIGQUERY_TABLENAME} + + LOGGING_LEVEL: ${COUNTS_LOGGING_LEVEL} logging: options: max-size: '10m' max-file: '5' - jpo_rsu_ping_fetch: + rsu_status_check: build: context: ./services - dockerfile: Dockerfile.rsu_ping_fetch - image: rsu_ping_fetch:latest - restart: always + dockerfile: Dockerfile.rsu_status_check + image: rsu_status_check:latest + restart: on-failure:3 + environment: + RSU_PING: ${RSU_PING} + ZABBIX: ${ZABBIX} + RSU_SNMP_FETCH: ${RSU_SNMP_FETCH} - depends_on: - - cvmanager_postgres - env_file: - - ./services/addons/images/rsu_ping/.env + ZABBIX_ENDPOINT: ${ZABBIX_ENDPOINT} + ZABBIX_USER: ${ZABBIX_USER} + ZABBIX_PASSWORD: ${ZABBIX_PASSWORD} + + STALE_PERIOD: ${STALE_PERIOD} + + PG_DB_HOST: ${PG_DB_HOST} + PG_DB_NAME: ${PG_DB_NAME} + PG_DB_USER: ${PG_DB_USER} + PG_DB_PASS: ${PG_DB_PASS} + + LOGGING_LEVEL: ${RSU_STATUS_LOGGING_LEVEL} logging: options: max-size: '10m' @@ -54,12 +96,27 @@ services: context: ./services dockerfile: Dockerfile.iss_health_check image: iss_health_check:latest - restart: always - + restart: on-failure:3 depends_on: - cvmanager_postgres - env_file: - - ./services/addons/images/iss_health_check/.env + environment: + ISS_API_KEY: ${ISS_API_KEY} + ISS_API_KEY_NAME: ${ISS_API_KEY_NAME} + ISS_PROJECT_ID: ${ISS_PROJECT_ID} + ISS_SCMS_TOKEN_REST_ENDPOINT: ${ISS_SCMS_TOKEN_REST_ENDPOINT} + ISS_SCMS_VEHICLE_REST_ENDPOINT: ${ISS_SCMS_VEHICLE_REST_ENDPOINT} + + PG_DB_HOST: ${PG_DB_HOST} + PG_DB_NAME: ${PG_DB_NAME} + PG_DB_USER: ${PG_DB_USER} + PG_DB_PASS: ${PG_DB_PASS} + + PROJECT_ID: ${GCP_PROJECT_ID} + GOOGLE_APPLICATION_CREDENTIALS: ${GOOGLE_APPLICATION_CREDENTIALS} + + LOGGING_LEVEL: ${ISS_LOGGING_LEVEL} + volumes: + - ${GOOGLE_APPLICATION_CREDENTIALS}:/google/gcp_credentials.json logging: options: max-size: '10m' @@ -70,7 +127,7 @@ services: context: services dockerfile: Dockerfile.firmware_manager image: jpo_firmware_manager:latest - restart: always + restart: on-failure:3 ports: - '8089:8080' @@ -86,10 +143,16 @@ services: GCP_PROJECT: ${GCP_PROJECT} GOOGLE_APPLICATION_CREDENTIALS: '/google/gcp_credentials.json' - LOGGING_LEVEL: ${API_LOGGING_LEVEL} + FW_EMAIL_RECIPIENTS: ${FW_EMAIL_RECIPIENTS} + SMTP_SERVER_IP: ${SMTP_SERVER_IP} + SMTP_EMAIL: ${SMTP_EMAIL} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + LOGGING_LEVEL: ${FIRMWARE_MANAGER_LOGGING_LEVEL} volumes: - ${GOOGLE_APPLICATION_CREDENTIALS}:/google/gcp_credentials.json + - ${HOST_BLOB_STORAGE_DIRECTORY}:/mnt/blob_storage logging: options: max-size: '10m' - max-file: '5' + max-file: '5' \ No newline at end of file diff --git a/docker-compose-webapp-deployment.yml b/docker-compose-webapp-deployment.yml index c719ebe2..17484d07 100644 --- a/docker-compose-webapp-deployment.yml +++ b/docker-compose-webapp-deployment.yml @@ -12,6 +12,7 @@ services: MAPBOX_TOKEN: ${MAPBOX_TOKEN} KEYCLOAK_HOST_URL: ${KEYCLOAK_DOMAIN} # e.g. http://localhost COUNT_MESSAGE_TYPES: ${COUNTS_MSG_TYPES} + VIEWER_MESSAGE_TYPES: ${VIEWER_MSG_TYPES} DOT_NAME: ${DOT_NAME} MAPBOX_INIT_LATITUDE: ${MAPBOX_INIT_LATITUDE} MAPBOX_INIT_LONGITUDE: ${MAPBOX_INIT_LONGITUDE} diff --git a/docker-compose.yml b/docker-compose.yml index f28d210e..c2a5117e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,14 +22,14 @@ services: MONGO_DB_NAME: ${MONGO_DB_NAME} COUNTS_MSG_TYPES: ${COUNTS_MSG_TYPES} - COUNTS_DB_TYPE: ${COUNTS_DB_TYPE} - COUNTS_DB_NAME: ${COUNTS_DB_NAME} GOOGLE_APPLICATION_CREDENTIALS: '/google/gcp_credentials.json' - BSM_DB_NAME: ${BSM_DB_NAME} + GEO_DB_NAME: ${GEO_DB_NAME} SSM_DB_NAME: ${SSM_DB_NAME} SRM_DB_NAME: ${SRM_DB_NAME} + MAX_GEO_QUERY_RECORDS: ${MAX_GEO_QUERY_RECORDS} + FIRMWARE_MANAGER_ENDPOINT: ${FIRMWARE_MANAGER_ENDPOINT} WZDX_API_KEY: ${WZDX_API_KEY} @@ -66,6 +66,7 @@ services: MAPBOX_TOKEN: ${MAPBOX_TOKEN} KEYCLOAK_HOST_URL: http://${KEYCLOAK_DOMAIN}:8084/ COUNT_MESSAGE_TYPES: ${COUNTS_MSG_TYPES} + VIEWER_MESSAGE_TYPES: ${VIEWER_MSG_TYPES} DOT_NAME: ${DOT_NAME} MAPBOX_INIT_LATITUDE: ${MAPBOX_INIT_LATITUDE} MAPBOX_INIT_LONGITUDE: ${MAPBOX_INIT_LONGITUDE} @@ -118,6 +119,7 @@ services: KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} WEBAPP_ORIGIN: http://${WEBAPP_DOMAIN} + WEBAPP_CM_ORIGIN: http://${WEBAPP_CM_DOMAIN} KC_HEALTH_ENABLED: true KC_DB: postgres KC_DB_URL: jdbc:postgresql://${PG_DB_HOST}/postgres?currentSchema=keycloak @@ -125,6 +127,7 @@ services: KC_DB_PASSWORD: ${PG_DB_PASS} KC_HOSTNAME: ${KEYCLOAK_DOMAIN} KEYCLOAK_API_CLIENT_SECRET_KEY: ${KEYCLOAK_API_CLIENT_SECRET_KEY} + KEYCLOAK_CM_API_CLIENT_SECRET_KEY: ${KEYCLOAK_CM_API_CLIENT_SECRET_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} command: diff --git a/docs/Release_notes.md b/docs/Release_notes.md index 24ae5b68..c7e198db 100644 --- a/docs/Release_notes.md +++ b/docs/Release_notes.md @@ -1,5 +1,32 @@ ## JPO CV Manager Release Notes +## Version 1.3.0 + +### **Summary** +This release includes enhanced MongoDB support, replacing GCP BigQuery in the CV Manager and integrating with the existing [Conflict Visualizer](https://github.com/usdot-jpo-ode/jpo-conflictvisualizer) MongoDB deployment. The web application now meets WCAG accessibility standards, featuring improved V2X data visualization and CV counts. Key updates include a daily aggregate of CV counts for better MongoDB query performance, Keycloak token refresh optimization, SNMP configurations pulled from PostgreSQL, support for PSM and TIM messages and new services like the RSU Status Checker. Additional enhancements include email alerts for firmware manager failures, a 'Contact Support' button on the 'Help' page and a filter for RSU vendors. The project now fully supports Python 3.12.2, includes various bug fixes and introduces several performance improvements across different modules. + +Enhancements in this release: + +- CDOT PR 69: Keycloak token refresh timer increased to reduce the frequency of site refreshes. +- CDOT PR 67: Daily aggregate CV counts to improve CV Manager count query performance in MongoDB. +- CDOT PR 66: Email alerts on firmware manager fail cases. +- CDOT PR 62: 'Contact Support' button now present on the 'Help' page. +- CDOT PR 61: CV Manager SNMP configurations now pull from PostgreSQL instead of directly from RSUs for performance. +- CDOT PR 60: PSM message visualization support and changing 'BSM Visualizer' to 'V2X Visualizer'. +- CDOT PR 59: CV Manager support for TIM messages. +- CDOT PR 57: Firmware Manager upgrade queue for handling excessive numbers of simultaneous upgrades. +- CDOT PR 56: Firmware Manager post-upgrade bash script support. +- CDOT PR 54: CV Manager full support of MongoDB instead of GCP BigQuery. +- CDOT PR 52: Rework the existing counter to utilize MongoDB. +- CDOT PR 51: RSU vendor filter added to the CV Manager web application. +- CDOT PRs 45-50: Updates to visual elements of the CV Manager web application to meet WCAG standard requirements for accessibility. +- CDOT PR 44: Project updated to fully support Python 3.12.2. +- CDOT PR 42: Adds support for a unique encryption SNMP password separate from the authentication password. +- CDOT PR 38: RSU status check service added to perform regular, automated pings and SNMP message forwarding configuration checks on RSUs within PostgreSQL. +- CDOT PR 37: URL page routing for the CV Manager web application. +- CDOT PR 36: Keycloak realm updates to support the Conflict Visualizer realm within the same Keycloak deployment as the CV Manager. +- Additional bug fixes + ## Version 1.2.0 ### **Summary** diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md new file mode 100644 index 00000000..d503b58f --- /dev/null +++ b/docs/pull_request_template.md @@ -0,0 +1,34 @@ + + +# PR Details + +## Description + + + +## How Has This Been Tested? + + + + + +## Types of changes + + + +- [ ] Defect fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that cause existing functionality to change) + +## Checklist: + + + + +- [ ] My changes require new environment variables: + - [ ] I have updated the docker-compose, K8s YAML, and all dependent deployment configuration files. +- [ ] My changes require updates to the documentation: + - [ ] I have updated the documentation accordingly. +- [ ] My changes require updates and/or additions to the unit tests: + - [ ] I have modified/added tests to cover my changes. +- [ ] All existing tests pass. diff --git a/resources/keycloak/azure-pipelines.yml b/resources/keycloak/azure-pipelines.yml new file mode 100644 index 00000000..e74d81a8 --- /dev/null +++ b/resources/keycloak/azure-pipelines.yml @@ -0,0 +1,26 @@ +# Pipeline for creating and pushing artifacts for all services + +trigger: + branches: + include: + - develop + paths: + include: + - 'resources/keycloak/*' + +pool: + vmImage: ubuntu-latest + +steps: + - task: CopyFiles@2 + inputs: + SourceFolder: 'resources/keycloak' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + # Publish the artifacts directory for consumption in publish pipeline + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'jpo-cvmanager-keycloak' + publishLocation: 'Container' diff --git a/resources/keycloak/realm.json b/resources/keycloak/realm.json index 8c0f25c1..a3babe99 100644 --- a/resources/keycloak/realm.json +++ b/resources/keycloak/realm.json @@ -1,4501 +1,7500 @@ -[ - { - "id": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "realm": "master", - "displayName": "Keycloak", - "displayNameHtml": "
Keycloak
", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "550ca506-0504-41f5-8f86-4137d6ce5a95", - "name": "create-realm", - "description": "${role_create-realm}", - "composite": false, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "attributes": {} - }, - { - "id": "4fdc8a80-c38b-4bd3-ba8a-a65c063503e9", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "attributes": {} - }, - { - "id": "909901d8-b862-4862-9405-33b9e92c1136", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "attributes": {} - }, - { - "id": "8889647c-84ae-4623-b823-dde66cb264a7", - "name": "admin", - "description": "${role_admin}", - "composite": true, - "composites": { - "realm": ["create-realm"], - "client": { - "cvmanager-realm": [ - "manage-users", - "manage-identity-providers", - "view-realm", - "manage-clients", - "view-authorization", - "create-client", - "query-realms", - "manage-events", - "query-clients", - "view-clients", - "manage-authorization", - "manage-realm", - "view-identity-providers", - "query-users", - "view-events", - "view-users", - "impersonation", - "query-groups" - ], - "master-realm": [ - "view-events", - "view-authorization", - "manage-clients", - "query-users", - "view-realm", - "manage-events", - "manage-realm", - "manage-authorization", - "query-realms", - "manage-users", - "query-clients", - "view-users", - "query-groups", - "manage-identity-providers", - "impersonation", - "view-clients", - "view-identity-providers", - "create-client" - ] - } - }, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "attributes": {} - }, - { - "id": "7a8f519b-da37-44d9-b371-6de34c9a8280", - "name": "default-roles-master", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": ["offline_access", "uma_authorization"], - "client": { - "account": ["manage-account", "view-profile"] - } - }, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc", - "attributes": {} - } - ], - "client": { - "cvmanager-realm": [ - { - "id": "9e93f607-36a3-4cc9-99f5-13bca6b85235", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "9fb4e3b9-f83e-4cbb-8e02-02198b9e20e4", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "96c629f2-6639-4a06-9f7c-57ff6a569fd1", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "1c6f78c8-8b42-4c59-b2c3-a3aff62be094", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "38c96740-8b14-4aaa-be38-bda2936bc739", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "cvmanager-realm": ["query-clients"] - } - }, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "dc6d3c7a-e704-4d51-b484-3131a290c0e1", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "6721b98c-33d0-429a-94ed-84e372dea190", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "10247006-a03b-47af-9fb8-0e49b6b86ff5", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "8a540cd8-09cb-4198-931a-6c57cec5b225", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "02619b2d-e45a-4cff-ada8-caae64c04a67", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "ee3e0752-8116-4e13-9f57-fd94cff3e070", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "6ecc52e9-3272-4663-b284-96254c994349", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "ae074b5b-9447-40c1-b0da-507916110119", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "0ab0dd3a-3370-4bdc-b5cd-0c0a2c4d9426", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "cvmanager-realm": ["query-users", "query-groups"] - } - }, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "c10dc946-6394-4327-b9ee-747ffad07cda", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "23865710-1147-4ce0-8a61-99e74a423ca3", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "40159225-ab1e-418e-93c6-aed5eaf79411", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - }, - { - "id": "9d706959-cf4a-48ea-adef-a5455a549acf", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [ - { - "id": "1ccd324f-d8ac-4617-a1e7-f4db037b9d0e", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "bb404193-644a-4156-9c6d-a58e0a0dc68c", - "attributes": {} - } - ], - "master-realm": [ - { - "id": "60fd0243-32e3-4c9d-aaab-74e8d4b61c32", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "b398115e-f10c-4b39-989b-b1eda6ea7868", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "a010f898-0fae-4eea-8254-ea00ed37f7ad", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "0ff56420-0f05-4254-aa35-3f41a8314e60", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "447fd793-7837-4f0e-81a6-1ef708eebd30", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "7430782e-a5d3-4cfc-aafe-03ceda7c9074", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "eea71d93-4250-4eb3-a0d8-a3a521db713c", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "0ef34529-8288-40d0-8e3f-e53280d8caa1", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "81a7921b-f431-4f90-ba51-72c602e38539", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "7214fbd6-a22a-4245-9af2-36fd0c9e88e8", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "master-realm": ["query-groups", "query-users"] - } - }, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "90700541-b47f-4023-9bf6-c2b6eb61836a", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "b6f9b175-f2cf-4977-88ab-753f3d9be144", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "3b17a58a-9b1e-48b4-8575-5e03e09773a5", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "2a679c2a-b7b7-4fc0-b8b6-b9c305c9e6e3", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "master-realm": ["query-clients"] - } - }, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "688ad21d-fa36-48e3-9d07-0775b1bba63e", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "44fcca6e-3338-4478-bbd0-c9e152eee293", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "035e70ed-4894-4a98-b0ce-a59b2938420e", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - }, - { - "id": "ab9712b1-d5bd-47dd-83ff-0728681ed04a", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "19bbe769-040b-412d-8824-5c988d81aeb8", - "attributes": {} - } - ], - "account": [ - { - "id": "d5b304bf-cd49-4e0c-bb9c-2b962538aa85", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": ["manage-account-links"] - } - }, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "b02ee396-90bb-4709-85a9-a1ccb6a3e36a", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": ["view-consent"] - } - }, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "68d7bac9-c03b-4191-b870-3e2f0dcd439f", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "d27b566c-9f65-41d4-b206-376e380e1ffe", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "dac55640-2cf6-4ae8-a325-c8b2e4bdc802", - "name": "view-groups", - "description": "${role_view-groups}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "aac5c4f7-ec1d-467b-a04f-dc105bd09b8b", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "4ea807da-5e77-4012-bdd8-e872ffab7901", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - }, - { - "id": "e8bee731-70ed-4563-a422-1f7a2add5caf", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "23baa920-f7a0-4fd0-b657-12a26668d730", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRole": { - "id": "7a8f519b-da37-44d9-b371-6de34c9a8280", - "name": "default-roles-master", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "60944e0c-5a1d-484e-a840-e0db728a8bbc" - }, - "requiredCredentials": ["password"], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName"], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": ["ES256"], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "users": [ - { - "id": "3eed2eb8-6ab0-43ec-98ad-9f42b0327689", - "createdTimestamp": 1684171738993, - "username": "admin", - "enabled": true, - "totp": false, - "emailVerified": false, - "credentials": [ - { - "id": "07639a52-9ae5-47a1-9135-eef57eb77209", - "type": "password", - "createdDate": 1684171739094, - "secretData": "{\"value\":\"FjUy7KUFTLTmF3zHTS5ZXh/7sSg8l0YkhckpMSC37es=\",\"salt\":\"Oc0KCi13OO147+zuVnorkQ==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["admin", "default-roles-master"], - "clientRoles": { - "cvmanager-realm": [ - "view-realm", - "view-clients", - "manage-users", - "manage-identity-providers", - "manage-authorization", - "query-clients", - "manage-realm", - "view-identity-providers", - "manage-clients", - "query-users", - "view-authorization", - "view-events", - "create-client", - "view-users", - "query-realms", - "manage-events", - "query-groups" - ] - }, - "notBefore": 0, - "groups": [] - } - ], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": ["offline_access"] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": ["manage-account", "view-groups"] - } - ] - }, - "clients": [ - { - "id": "23baa920-f7a0-4fd0-b657-12a26668d730", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/master/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/master/account/*"], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "fd883cae-ef37-48b2-bba4-642e4f18353a", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/master/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/master/account/*"], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "092a081b-1a76-4455-86bc-4a3e4af5d556", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "583a4a62-41f3-49d6-9535-8e5c83b0821b", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "bb404193-644a-4156-9c6d-a58e0a0dc68c", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "8ebbb241-987a-4c51-8da5-f8ce61673001", - "clientId": "cvmanager-realm", - "name": "cvmanager Realm", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [], - "optionalClientScopes": [] - }, - { - "id": "19bbe769-040b-412d-8824-5c988d81aeb8", - "clientId": "master-realm", - "name": "master Realm", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "5c6c1416-a2d5-4cad-9116-24ab04dd0acd", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/master/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/admin/master/console/*"], - "webOrigins": ["+"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "7b0670e3-81fc-4a9f-90eb-2c658124db24", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - } - ], - "clientScopes": [ - { - "id": "74067e94-c9c3-4082-93cc-9e602eb3befb", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "004f7401-8201-4457-99fc-a52e4096b048", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "944f01ea-54d8-444e-bd0c-f2da7b293042", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "b6d77c92-c78f-4754-9be6-07835d52eca9", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "41d1679f-e512-4442-b201-e90bbf49d186", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "300bf607-271b-4921-926c-dd5bb2066300", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "895b986a-fb7d-45ed-beba-ab892e8984de", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "9779ee99-968a-43f5-af6c-cd7b806f9ae6", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "ceed0919-7166-44a8-94da-740b03f8c01a", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "489f2cd3-ac26-4d49-a1b1-9c1a5e07bbb5", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "4e00e07a-85c3-45de-86ca-7240cee19d7f", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "c6abcd9a-79e5-4ac6-946f-3a2ded3ff404", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "c1e0ccdd-a61a-4cb9-bf8c-4bcfc5357f13", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "f1eb62f3-fca3-412b-a7b1-360f60f80553", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "e1495d24-fadb-42cd-b92a-90228ce0e022", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "3a65d40c-ded5-4eb2-8f51-798effc05fbd", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "90ed4c3b-002b-4b53-9211-133e772c3888", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "a1bec371-45b6-4414-8a2b-d39e5dfa62d1", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "739b3670-7ee9-4f30-81ec-f75e0dfe50a1", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "4ca1754f-887d-4df3-aa5d-64310beecfe6", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "2fcf6459-3084-494c-920b-ae3b9e4547d2", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "c85a3fc6-3f8c-46f7-a1ef-07ee0fcef6a7", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "c9b1cd63-1a3b-4ebe-a3c2-bb3a2b050818", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "56ad68ad-bf99-4772-a403-45e2395128e0", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "20d186ac-0a28-44ae-a89d-4fd9a115eb59", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "4579c4a7-087a-48b4-b786-e79839a31448", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "4f191861-2dce-41fd-97d0-0f79dd73e789", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "a135d798-b99f-49b2-a740-2264ae088183", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "528cce23-72f5-44d2-9173-032829831b43", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "41c00038-0b0c-4ee9-8d7a-5506315cded7", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "1439ee4a-5e8d-4b06-a39d-9711435615d4", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "c236283b-e28d-4474-b0e9-fe142da8abfb", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "0175550f-c7d2-4339-b0c2-b195f1594a82", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "b81a5632-45d9-458c-90e9-1ad27e25f0a1", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "96db6633-361f-45a6-88bd-8f09e7d01a63", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "9bb1ae4d-9d33-4a0b-b64d-35370e39a378", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "8dcc4385-bb1d-4c60-9cc1-7e38ce0b2a6f", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - } - ] - } - ], - "defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"], - "defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "xXSSProtection": "1; mode=block", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": ["jboss-logging"], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "50be0f52-a0b7-41ec-9461-8f3efa1ea8d9", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } - }, - { - "id": "b084f4b5-a889-4caa-b648-dbbd86510faa", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } - }, - { - "id": "c6f9557c-3662-4f0b-85c4-733c1f84d82a", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": ["true"], - "client-uris-must-match": ["true"] - } - }, - { - "id": "968266f9-1691-4476-bfca-e732a1616bb0", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-role-list-mapper", - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-address-mapper", - "oidc-usermodel-attribute-mapper" - ] - } - }, - { - "id": "cdc03b03-2c21-4abd-8849-f8172d6462ef", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "3e85869f-61e0-488a-969d-a29085eed4a7", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "87240b4f-b4e6-4b08-a22d-6591e732d1c2", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": ["200"] - } - }, - { - "id": "f6f84a01-df5f-415d-a356-43c7579bda6d", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-attribute-mapper", - "saml-user-property-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper", - "oidc-address-mapper", - "oidc-sha256-pairwise-sub-mapper" - ] - } - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "1600aa60-a4a6-4b29-ae47-4b9fc5c0be36", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": ["5cc50d18-867c-4fcc-acec-be9d12a81e15"], - "secret": ["UpvFOip7j09GSCknZPzxCA"], - "priority": ["100"] - } - }, - { - "id": "6b1db97b-0d9b-4163-9059-641062c6d5a0", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": ["a5310d9b-7eb3-499e-8e15-47787ccea82b"], - "secret": ["jOZy-8rvgSGw3YIh9BXvrcBYJZ53caqKhOXkPyJ3eDhsIu3L-66KjHr_3Iqt7oCuZYfT60q3mOCVKZDSJuvmAw"], - "priority": ["100"], - "algorithm": ["HS256"] - } - }, - { - "id": "30ec3aa5-8120-4ccd-ae3c-3e75e3fc7aae", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEogIBAAKCAQEA0pfFpbMM6/LgRFxuHhX/UtkrNlQqItRqDqS2Im6W++UsObh2XZbvJvxJormfq/BN1c6nYTspCz7gKniVt7VWUr1uGpqQ1F7/UQbOlBCXsPwmX48wqoUMMqEMaOlQlM7x306Bt9yBxPuOonA+skBxHOjPI0azlu3UVoZO/wYk3PmF1wdlAt7M4ERglK7u4EFdGcWMBkpB1samOr38uyiMpTbyh7U7geCwW77GVfYJida40f3Yg9PuaveAGQvllrhY8jEEiy2yA7DvJAM6nusBgwOvY4lkhu8xPp1mKvjaHF+wiYchZuoXYQairlMn7tS6vk79ykazur/icTI8d5FUaQIDAQABAoIBABN4OqquGhS6WKeZUAjyCa/0Kf2U3Gxp7QA7lCcsyEKzllojxrWyXZviGUC1HqD7z4Zj26+uk4XZo0np6hWY60ktAD/vaFJqEfrAwVqJCi0vsrCFAX/SWyVXJFmSsIOBenUlwfJorYqzoyU7cWBzesGseHIPeE0M4eO5+RW00LWpJRDwJqIvTR//W3W6OpLF1kdBNvWbd/1fI3RHm/y56H4prwSHizujvyecHIXUfbIYQBy5smi13vwrEpMyOHsk3adf/V/LE1BXM/ECqA5d0h0p7bdo3cn5mqNnB9hGKVMx2qM1JyTqiMYkfZJiujXQcbGHbbLj/TDgyQJiDtiqWB0CgYEA8i0/3T9PYCucqKRgRUIP0XYIJXTAzD48EfaTOdTrMjRrMUi4tQ/tBZMN/rXavls2ZTH6UV9rzxEyljc2lMAehpruqgpozo8oaWGabaaqz/141ZI/ugE7UkFx2DWYCATx4vNWR8yT1gjo4rRH4tPfWZJF3bjNVR89IFd0YjjZ6o8CgYEA3p0CqGjvJxr3mKCCBv42kjoXTVogprLQ70zImbuHMvmZC7nURRMkGH7ILmzQJhpj5hGX0ux0Tb2mCfCAyOaZcy5VNUnzRpnnXVDlvBF63CzFVOGHo+VCGtvOQOEjvgyltV/HttPFF93/w30jCD2efVrNdPrMY8SJpmBszhacrYcCgYAvwfSiMOX6VR9jsma5wblU2S3qFQggPvWhpTqLYQ2wrN2mrmyeaDGZs9JCtKIaf+pOMnjmqyabgwyyyticpPZgHRWLutnIQjr57SGDFicBNT8q/thKWgvUwMsulAMTMcZxtwMAzbsprkAe1OaIOLgG9e2JN/RmIO7w/c4ZwJRYyQKBgH1gsvGFAETnFFI6/GDR5oxe+WbQfxlEFxbgCQ9EpjXYjBElnV82E5tUNIypLmZ3cJJQkFD0aeCrEowAj5gXAI/1pXn453J5FezhtjJBKhA9ivpud0WgxqV37OdvpgDRALdxdXsMNLJnjzhcD1IB2nTBonvjESCTve8d2coMxDubAoGAKrAqs812hE1Rueic/8oUYGEGKkkddvjJy5Gu2NHVqbrDUj9s38m8mg1CujoBThX08H3P9VuBNkPQKI5usxtRcyIcIp2t7dnfHTg2cedhxmKC7nnXH+4LvrYJjM53b0EZ3jSZUDRpawdPHwRqVhq89Pj2wuysWYXDJRvh0Wlic0o=" - ], - "keyUse": ["SIG"], - "certificate": [ - "MIICmzCCAYMCBgGIIHVNeTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjMwNTE1MTcyNzE4WhcNMzMwNTE1MTcyODU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSl8Wlswzr8uBEXG4eFf9S2Ss2VCoi1GoOpLYibpb75Sw5uHZdlu8m/EmiuZ+r8E3VzqdhOykLPuAqeJW3tVZSvW4ampDUXv9RBs6UEJew/CZfjzCqhQwyoQxo6VCUzvHfToG33IHE+46icD6yQHEc6M8jRrOW7dRWhk7/BiTc+YXXB2UC3szgRGCUru7gQV0ZxYwGSkHWxqY6vfy7KIylNvKHtTuB4LBbvsZV9gmJ1rjR/diD0+5q94AZC+WWuFjyMQSLLbIDsO8kAzqe6wGDA69jiWSG7zE+nWYq+NocX7CJhyFm6hdhBqKuUyfu1Lq+Tv3KRrO6v+JxMjx3kVRpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABMYJfqpdi+aXazXJ6fCXBslVlYsE6FMFZhCaHvoYw+Lq2AfmZMwxRX1qnJuVrTxyoSjOEAQgAI8toXfw0lvnc4rtV3y2J3kurjZSqkwY8LttotjgjUh3AzDo5o3PU5fL59JuwrwK5fEkYEOJrbsrfMJ4f2Q+eVKo2XPhtvfL6c7haqsDCbxf/vtVflBLIc1uUO4KR50bB3F+3XNa9RrYla/13mfszPvgSGPv50463P6XypiaMYm7GlO9aA+rnaKmYT27qlZ8QlIvPn7A5kXXpSsxXsqKEf0yP7FwKrnygTVMygVgeozqJ+DhXQqG1Tzk8JH+3Fe/1miqbTBBuzuK68=" - ], - "priority": ["100"] - } - }, - { - "id": "51d04be4-d22c-4945-9571-3fb1721b948d", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEpQIBAAKCAQEAvB6yp/8VvPBYhl6NsY13guODt7hE7xMgYwQX2zcv0NkDTVnxUe3T8ylHLUKJtjg+1hC7nHrOqa5553OhUaJzsg2MSkswtvVIbhMxEeM61O9X1yZfP7YGjxlgWmPuLJGl4ZoVsMSLScm1hgzSORavXqYcJhI5n5FhPffXayZqBETQpbU6LwcHvbzG/Zd1S4Ht7QUZ26Md3rNPUgoF7QZvY2tBvLZlkUa3WUQr3wOQfDml111MubII2YFcq9GzdIU2MfYf19Hj0Wf2phRuur3Ljpzp9kwR3WNveI74Fxy4oswwSwkNZWGJzYxH2zvnLEYxF+x9XRWFc8NYMXsEJONZlwIDAQABAoIBAFSLv2N9YzmtEzwglrHrkIDE29ff+ysvf2jA2C/vl4/XWIKVH73gk1c/f/u5YccBdEwk7QygrOzZu3PoJeJYjoLBUAG71UME7e56tZ0CcNhuUR4i4r/xgPUjRIibTNm/A7xm0cTGMIuTGgALFxgNN/fj09bQbhFm9zswfiJr+027EZ7+FXQmRv1CDElfGcyxNjG0cjNEMkZ5u0uJgArbYiv8qHq8JJlHkqmf8ioSY8LUEbO/rVD6aOFdZHDQTG1AIefVN3ZygrA/xAsCWRVlT/50EjnSb9jKn0VHw2SDyhsznnHhdc4FlH9WpaZNYOygfO0YsWbCj7dD4E9DjMtPK1ECgYEA8TjoPm+vV/ThVqQ9+aE1I11ctZ+/9q8Ntyl3Grcv18BB6iWTTeLeLO4Z4h+uc3jI/JGIrGDCcZoQadZbi+5Nh5WeH08Yhfc6p+LLa3TfGk1bhL+jPsn/jT9+cD87eV4OpK4Wqmq/dE7DfHEwykb+8ewWyqXGdclbum4jWQHLFNMCgYEAx6T8OgLuHAzZwbTpTNZOFlfeu1NQ8sU6U1zfX3/uBAasE2/PrVY9d03jCW4UT5HynLlLSoWLDG3IMhs2l1pxMUeBOTA1u3zyoysKiY8izfBfIcA5G0qlktpRamlQP/SJly1ZOjhmgf4AOHMkp8BTu06fHG1TssWuqhhYw1yQPa0CgYEAg+Xk/7bb/tE0ocZ+6M9gGe1D8z/dnEpNyphOuvntnCBRKnHPYOgrKhArcPx3zEYASDJftDnYOHvQe76tIg90ry19X4tFUoNDvGcDacdm8p/X6fdLkNqs9JQCU+gPYiavBRb358kk1Lj4pUPTNNerMacxMy+AHAm1MXRluZaEb+cCgYEAmxVZbULrblS5LxT2id6LiCW3+nItFnkI5srlJc09uljogKzBeZfdZXjWXXPqSSlJel8h0oDMU9pPwkSLcqUp+qreAeumQb88yG1d4R+UXL9VVuV4NvAUkHARAIVQdm3iF9J9VpGLZ31E9JTVK45mPMFFLhLrCpsvJCiHgzK7RX0CgYEAvs+5JdYoa1FBdSVmJt61++h3tHIxqxTec0Hh0878po9N+QvU1UVQAoc4PH1dT9cZJ4ROsLvs1agE6VdIDuVvADskA4RCqi71Iwkoi6exUXpcwY61wSYCQQVD9LTWHX2XOQKTgiUO6ZbSpVR3JYNQjtuBbx80UxhtzB8LOb1pWp8=" - ], - "keyUse": ["ENC"], - "certificate": [ - "MIICmzCCAYMCBgGIIHVOLzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjMwNTE1MTcyNzE4WhcNMzMwNTE1MTcyODU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8HrKn/xW88FiGXo2xjXeC44O3uETvEyBjBBfbNy/Q2QNNWfFR7dPzKUctQom2OD7WELuces6prnnnc6FRonOyDYxKSzC29UhuEzER4zrU71fXJl8/tgaPGWBaY+4skaXhmhWwxItJybWGDNI5Fq9ephwmEjmfkWE999drJmoERNCltTovBwe9vMb9l3VLge3tBRnbox3es09SCgXtBm9ja0G8tmWRRrdZRCvfA5B8OaXXXUy5sgjZgVyr0bN0hTYx9h/X0ePRZ/amFG66vcuOnOn2TBHdY294jvgXHLiizDBLCQ1lYYnNjEfbO+csRjEX7H1dFYVzw1gxewQk41mXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFI0LLUp4WTFr66lt0tvUR1cPxF3DWRqw1EzavsHudg3MsMX+GB6bDZbpt+VpzqzfxzC52WoZHfzKiHz7KUUILD8HczP74k+hvnGOpNHKM4TKeIk7xGvY/B3opgpEnN+Pf5OE79KZl+0358fzGKYeAt+ApjG5KSgdNm3JuL5XmO0Hx26iMc7nYykqrkQuPOq1XGhI4v9mWLtnOlaQGxIweUGae19LijY77zmANilRuN5pLFQ7X0QMvqT1062Ug7mlOlM6nACFuYP/+zDXqXi836knGgfoQ2VbI83PUAVOzqYsP9dTtGlUDTgz0qPlIeOoPmekd6xcBeIFtfcrTXC8LQ=" - ], - "priority": ["100"], - "algorithm": ["RSA-OAEP"] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "4716cd11-87ee-42f0-99d4-866dd4215597", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "7ead36a1-36da-4afd-ab0c-00ba2912a4b1", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "16ed8b1a-b99a-47c6-8527-751434c3eea1", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "4c8daecd-36d1-42b5-96cf-49d69fcd065e", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "b6fc66d5-5e19-4cf7-82c9-c07178083ea9", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "044fd292-37aa-44b6-abab-19b358eb6e7d", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "b6cc7d8e-b3dd-4b65-af13-97710960d05c", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "41452beb-f865-4693-9817-ed8b16b47b10", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "5ae7f6ad-dbf7-4206-abb7-d694ad53543f", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "18022061-cff5-4632-ad7e-4e0a1a47b18b", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "cb00e215-972f-4d11-95a1-8d75b5064605", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "a092ac1f-d568-402e-bbe1-b804b94eafe0", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "a981c350-a208-4c31-9387-3dbd4c551b56", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "bf27a32d-53df-45dd-9639-a53591526225", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "d9c41a81-0b80-4623-b064-f0827c9b82f1", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "df6be423-6ecc-4508-b004-ff62fbd0fe14", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Authentication Options", - "userSetupAllowed": false - } - ] - }, - { - "id": "85d8820a-6f71-47c0-9fe5-8ba20c9aeedf", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "63e9d6ee-5336-4e98-b474-0e407b878275", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "f6f8c67e-176a-4c71-ae09-647b3974d37e", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "e1d4212a-7e15-4564-9aa0-f20d52028f16", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "9908868d-295d-4fa9-baa4-7f89176ec158", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "d3a8eb4a-a414-419b-b2f7-ed2d1d11fb4b", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", - "cibaAuthRequestedUserHint": "login_hint", - "parRequestUriLifespan": "60", - "cibaInterval": "5", - "realmReusableOtpCode": "false" - }, - "keycloakVersion": "21.1.1", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } - }, - { - "id": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", - "realm": "cvmanager", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "998ccb10-fd0a-4e8a-9e56-e1c7962bf20c", - "name": "default-roles-cvmanager", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": ["offline_access", "uma_authorization"], - "client": { - "account": ["manage-account", "view-profile"] - } - }, - "clientRole": false, - "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", - "attributes": {} - }, - { - "id": "b0a79e2b-c806-492e-8505-00588ff3432a", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", - "attributes": {} - }, - { - "id": "2a0612f5-fc06-4ccc-afd0-ea7e2191f310", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", - "attributes": {} - } - ], - "client": { - "realm-management": [ - { - "id": "31ec7c7d-244c-41be-9d4c-a5593383298a", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "ee4eb6b8-d92c-4cbb-bf3c-b1debe5c3ced", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "83a91028-27ab-44fb-9af5-4f20263182b9", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "d0da36a6-8b14-4465-8dc8-28437cc84a2a", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": ["query-groups", "query-users"] - } - }, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "bd8514af-c5d1-4465-b456-15f6879411ee", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "cd560ff0-c8f5-414e-8642-716c9e2efa5e", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "7062c7d0-944e-4c18-8919-d0b0f7379a71", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "e6df53d4-fd66-4918-8e9c-b8e4446bfee3", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "3fce0be6-6b1e-46b4-8632-ff35f545c435", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": ["query-clients"] - } - }, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "e37406f2-43f3-4790-a4f7-e614003a9b62", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "376a674a-5a2d-4f3b-8158-17eea74e5b46", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "83f52ff4-7f65-464d-a59d-76543f3095b4", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "0c38752f-df92-4f48-b0db-70aa831e88ce", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "b6b227af-e6c2-4a8d-b59c-022c2ab83943", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "cc2bb729-2724-44da-b62a-c6669b1c8714", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "567cd8ab-e068-49c1-b70b-fdce835a828d", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "b42adb18-571c-40eb-a9e6-62c68850bdab", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "9900411e-06cc-4a2b-8d5f-646d5b718dee", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - }, - { - "id": "15a49a79-49d4-4a0a-a4e2-a95faca496b1", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "create-client", - "view-realm", - "view-authorization", - "view-users", - "impersonation", - "view-events", - "manage-events", - "manage-clients", - "view-clients", - "view-identity-providers", - "query-groups", - "query-users", - "manage-users", - "manage-identity-providers", - "manage-authorization", - "query-realms", - "query-clients", - "manage-realm" - ] - } - }, - "clientRole": true, - "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "attributes": {} - } - ], - "cvmanager-api": [], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "cvmanager-gui": [], - "broker": [ - { - "id": "db2c3f89-dd8f-4f7a-8076-774548973a13", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "a48352c8-caf6-4175-8f10-b889a6815067", - "attributes": {} - } - ], - "account": [ - { - "id": "9d4f040d-96d8-44ce-a276-bd9bea962d64", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": ["view-consent"] - } - }, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "a8b8c7c4-6677-4ff4-a080-d957de0e954f", - "name": "view-groups", - "description": "${role_view-groups}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "3c8a806d-363d-40d3-a845-db1af46642d3", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "3d7e73b4-90fa-4c74-bc0f-9dd2c3e619e4", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": ["manage-account-links"] - } - }, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "2c52f8b6-72fc-4662-af7e-26fb225011d7", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "574a3cd6-b084-4568-a7ef-ba3bbca1fb1f", - "name": "delete-account", - "description": "${role_delete-account}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "e8ff12eb-3830-40ef-8de5-183de3af4d45", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - }, - { - "id": "73651ba8-a717-4599-b291-273776db88db", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRole": { - "id": "998ccb10-fd0a-4e8a-9e56-e1c7962bf20c", - "name": "default-roles-cvmanager", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7" - }, - "requiredCredentials": ["password"], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName"], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": ["ES256"], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "users": [ - { - "id": "140a5f80-f0fe-4aa7-981b-a07e35dfda03", - "createdTimestamp": 1684172188006, - "username": "test", - "enabled": true, - "totp": false, - "emailVerified": true, - "firstName": "test", - "lastName": "user", - "email": "test@gmail.com", - "credentials": [ - { - "id": "6a037273-410c-49a1-9cab-f5a93654d53c", - "type": "password", - "userLabel": "My password", - "createdDate": 1684172206165, - "secretData": "{\"value\":\"Z2Wxmk8PNk6LYa0a02Yhg/2Gqudz1bGxWrKWVgPZwWI=\",\"salt\":\"ZgC/WbN8ujMIvtTy2RiKlA==\",\"additionalParameters\":{}}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": ["default-roles-cvmanager"], - "notBefore": 0, - "groups": [] - } - ], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": ["offline_access"] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": ["manage-account", "view-groups"] - } - ] - }, - "clients": [ - { - "id": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/cvmanager/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/cvmanager/account/*"], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "48417936-3262-4cc6-a53d-69809be217ec", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/cvmanager/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/cvmanager/account/*"], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "dfe4414e-e166-4c56-bacd-e7bde8d3bca0", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "14c1acec-de65-4f71-a62e-dfc20eb42a11", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "a48352c8-caf6-4175-8f10-b889a6815067", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "02c60f24-1909-4d39-bab5-1db615e1b224", - "clientId": "cvmanager-api", - "name": "", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "${KEYCLOAK_API_CLIENT_SECRET_KEY}", - "redirectUris": ["*"], - "webOrigins": ["*"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1684172946", - "backchannel.logout.session.required": "true", - "post.logout.redirect.uris": "*", - "oauth2.device.authorization.grant.enabled": "false", - "display.on.consent.screen": "false", - "backchannel.logout.revoke.offline.tokens": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": ["web-origins", "acr", "openid", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "8232316f-728b-4e89-90ca-c91907d7718d", - "clientId": "cvmanager-gui", - "name": "", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:3000/*", "${WEBAPP_ORIGIN}/*"], - "webOrigins": ["http://localhost:3000", "${WEBAPP_ORIGIN}"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "http://localhost:3000/*##${WEBAPP_ORIGIN}/*", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "use.refresh.tokens": "true", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "tls.client.certificate.bound.access.tokens": "false", - "require.pushed.authorization.requests": "false", - "acr.loa.map": "{}", - "display.on.consent.screen": "false", - "token.response.type.bearer.lower-case": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": ["web-origins", "acr", "openid", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "b2cdce72-e870-4473-99c2-5dba6ebee37e", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - }, - { - "id": "71948848-7dc7-4c8f-9437-ffb5e27662d6", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/cvmanager/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": ["/admin/cvmanager/console/*"], - "webOrigins": ["+"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "post.logout.redirect.uris": "+", - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "2e6d6ea3-769a-4685-823c-242e072fc360", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] - } - ], - "clientScopes": [ - { - "id": "db9cb18f-c262-4603-8902-c97221ccec56", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "9a717837-98ff-4984-8666-52f42af56200", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "8a3ead48-2b70-4a64-8aaf-425a6087333b", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "03a2abbc-8ff0-47b0-98ac-5fb35a694929", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "6ca30e3d-0ea0-4980-889c-64e3becc0db0", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "494e6946-b98d-481e-85dd-5d218f5240f9", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "2431e4d9-1082-4e99-97e0-972d13743be7", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "01752d8f-0747-43c4-8f11-b63aece6998c", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "1d61e415-2ffd-436d-af25-95a3eff9aa76", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "6c0e9870-0e7b-4687-9ac8-a53395661a90", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "923ead18-f830-47bc-8254-7a2e920f7f58", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "06263302-3360-4f6f-8f3d-b798c3963458", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "fecc8a79-84de-4971-a4ce-ddc895213dbf", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "b5044e26-8005-461b-91e5-54dbf10670c6", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "10adab8d-9a79-4cdc-ae56-98f7c3979a4e", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "0ac06716-e460-4845-99f0-dd410d676a66", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "6d513f34-166b-49ac-b4ad-aed59c0defc9", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "4fbe0ed4-2344-4391-b203-5aa0df0ebb9b", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true" - } - } - ] - }, - { - "id": "7cd87530-e597-46d1-9ea5-93a8ea9f1eb1", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "e714fced-1ef7-4e9b-8f9d-f73cb7fc7470", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "01008bb2-32f8-4cdf-8c9e-8dfa9819c185", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "50214ed5-b9e0-467b-9730-6456851c9316", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "6280fb5e-6ee7-404a-96e6-102cb2d8d25b", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "98703b2c-a82e-4aab-9fad-a40b13d2b515", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "f6bb1b01-b013-4834-a966-bf0c43b77f25", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "5792b55d-7445-4fd2-a3b0-ea3f67242132", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "68a27666-77cc-45e0-8eaa-a91e19bab274", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "299e35d1-e917-4ed0-8402-ed023da2e2b5", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "bc9abcb8-bca1-4abd-8b9c-5f4ba66092e0", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "0df3576b-15b2-4dab-bcac-e6fbacb0fc6f", - "name": "openid", - "description": "", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "gui.order": "", - "consent.screen.text": "" - } - }, - { - "id": "f1eb307e-c865-4381-859a-4d72ae40f5b5", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "eb9bb791-6ec1-4d6e-894c-d1e43bb0d4b7", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - }, - { - "id": "ee0d0b99-5309-41c8-972e-e9089d7a79f6", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "d953dfd5-95e0-4bfe-9fc6-ae1fc9cf627a", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "7c85f8fd-cb30-44e2-ae73-dc5fb179a0a0", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "0d0e748e-68db-4851-9bbb-da90a7553c88", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "f9c88be3-af53-4d7a-9128-c8a15b8955b9", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "c209c131-f033-4d55-bebd-fe8945d89900", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - } - ] - } - ], - "defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"], - "defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "loginTheme": "cv-manager", - "eventsEnabled": false, - "eventsListeners": ["jboss-logging"], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [ - { - "alias": "google", - "internalId": "770daf54-0400-4081-ac81-df57abb079ad", - "providerId": "google", - "enabled": true, - "updateProfileFirstLoginMode": "on", - "trustEmail": true, - "storeToken": false, - "addReadTokenRoleOnCreate": false, - "authenticateByDefault": false, - "linkOnly": false, - "firstBrokerLoginFlowAlias": "first broker login", - "config": { - "hideOnLoginPage": "false", - "offlineAccess": "false", - "acceptsPromptNoneForwardFromClient": "false", - "clientId": "${GOOGLE_CLIENT_ID}", - "disableUserInfo": "false", - "syncMode": "IMPORT", - "userIp": "false", - "clientSecret": "${GOOGLE_CLIENT_SECRET}" - } - } - ], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "a207ede2-6937-450d-866a-7ae9e291a205", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": ["true"], - "client-uris-must-match": ["true"] - } - }, - { - "id": "779312d6-304b-4dc7-8274-2cbe8f0b88f0", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-sha256-pairwise-sub-mapper", - "oidc-address-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper" - ] - } - }, - { - "id": "b44329a4-463a-4cc9-962d-5cf8ae2a578e", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } - }, - { - "id": "623e3c2a-b428-4361-93eb-9710fedb66c8", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": ["true"] - } - }, - { - "id": "a1954774-53b4-4696-b357-a11449d69198", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "8d6e98a6-42b1-4391-b371-bbcb3af9e896", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": ["200"] - } - }, - { - "id": "e89a9a42-ea8d-4871-aeb7-1dfa9aad8e9b", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "585f6d70-c93e-461a-ac17-a9e0ed5c5be9", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-property-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-role-list-mapper", - "oidc-full-name-mapper", - "oidc-address-mapper", - "saml-user-attribute-mapper" - ] - } - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "03ada59e-4b8e-43a7-90af-181d60bcbae5", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEowIBAAKCAQEAsnxqBGGqOEoyQ0Dq3iWSf+H+7C/xbh/ROakF2+oEfGemWVjsfrDXBUZhm9wWPQnW/Ow8qioG0H//1Nv1dryzfScLm49luGfxbieUbRNDb+OvcDTf6i/8r1T+mCEW/vUDon7PNTpOckrP8rRUcbvTSqX0HGo9weMrYlnBcIZxYzkflYNmBrbj46Fp7wrmG1TzoeEIVBviq/jYBRdnCoJE2mLbCnXupJErnQeuhRezfw6uCMASjRkZb+HGk1mGQrykE3/U3eeGXLlVA0G+Fe34SoXStTCuENYFb/zXMskjC2L3EgqXatuPNt+pNZuJyFz8jKEtbgkBKGMLeDkm5NNr1QIDAQABAoIBAE+ZEoKvt4Tw+edqTRQS93mWpORaITZ2dA1d5qIDhEqiwtn3wUhivxG4KJGknjpMaBdVl1xf77gOTV51VcvFLdqzjgaq9bc+i7oPZq8aNynwBW5p9i3vhqX+pqfbofDD/gH6wZfAT/nCiWh4qWwrUnho+Cuv6ajNEa0D0DPJkUmpEP5r0Eq1ZBpkV6I7brKtCPoMHIbofm7oqshRYxvzSVhlYJUSU0aMVw/7Dww6zT5vrX6QJWHLY9mBR3YxS/uMG/vYMKEu2hXbsdjtBRDl/u4aiAIk+OIulcmAlDnxC0dFHgw7rsDpK5QI2SJ1m3MF3lZ4AiOZs/o900US+431ryUCgYEA8LiMGI5kdKvEnVbYAqEzoo5EHjx1l9ha6F8Dzy/bKKY6noPePCC24j27Zv25OjBcq8MxFvo7grqDG+ZtWzes17pxmpe3ttaXtuf9RRf8wLw323USRrwJu52xD5STfombRKHwpKEy/NRewTnX6qK6QvEki/9YbEbgPEuA8sOvUHsCgYEAvdCeE7MdiBZEinV3+voBZuCJzlN7d2gSoGWv/ZZUNBMqE3Wiyr5dx85TiBkIbghO+t5EOZRQFL403nIpDPLVXLSsZxdAbtOtSGcc7zjUITpuz6malr4Tzlp+4d0+8Vjc+ooDTnIanCq0bx1vpBTitx3qGHZR/OCVkQ57Lsl2C+8CgYAhCvQQGtunODzQ7C7SjZYs5iJrlBkAMu6nnwNC2WrX9ZluUOOclVEFVTv4MzPNzP2rhiui385zb2630bWJI+dR5YHamqDZNDO3I7kcVuKXAj8YnMVZeE5NtqOrY9WrNPBfR2tk7cu18ODg3TPKPXQb5EYEAZT9p+z32dVlfX7/KQKBgQClei+1YNuH/lG2m34DsNx0AaBh3WmvyW0jpELvQpUZ6PMvj8hiE9/SBs/PwHMW6etgzVCRGflOfBu/Kasb/L+BWIlMPnsPoz5X9nzFGLfmV/iu1V9Nt1uw9DfVVHpBEYVkbdlAFD2ak6hFjlX7p7GWjl+8/7muSWRa11MQkNV2xQKBgC1Fq/L0+pcNSNNKd323mKjZaGClN7Pz4FzivT2sM9qYjZsyhoX/N5rVPRqK1w3Sovq9sUaPJGAHkluBwcULYT0/wa3EAsGfOkwrHNh1bBqO39bfvhuoXuDhmxg5AkyXyuvz130SoCc9WXw+iWIA1sEy2xcnrqrzPCWsk39zCCa9" - ], - "keyUse": ["SIG"], - "certificate": [ - "MIICoTCCAYkCBgGIIHh1fTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAljdm1hbmFnZXIwHhcNMjMwNTE1MTczMDQ1WhcNMzMwNTE1MTczMjI1WjAUMRIwEAYDVQQDDAljdm1hbmFnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyfGoEYao4SjJDQOreJZJ/4f7sL/FuH9E5qQXb6gR8Z6ZZWOx+sNcFRmGb3BY9Cdb87DyqKgbQf//U2/V2vLN9Jwubj2W4Z/FuJ5RtE0Nv469wNN/qL/yvVP6YIRb+9QOifs81Ok5ySs/ytFRxu9NKpfQcaj3B4ytiWcFwhnFjOR+Vg2YGtuPjoWnvCuYbVPOh4QhUG+Kr+NgFF2cKgkTaYtsKde6kkSudB66FF7N/Dq4IwBKNGRlv4caTWYZCvKQTf9Td54ZcuVUDQb4V7fhKhdK1MK4Q1gVv/NcyySMLYvcSCpdq248236k1m4nIXPyMoS1uCQEoYwt4OSbk02vVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD+vpYJuJ9ZG+nXcttflEs+FvUg4sFvQZVxLwHxusLKb095K9/UTbOi13bp/XyB/04SGZDZkb/RRjCXfLGbwAhlLvta83Y7D9egmYL4qT+4aQrctOdjX7qIjec8nklWSm/E5zAxF6qZ4pB8msK1X1d9vWJPtTY+y4uWWE/SO01acRPInazEtkwAVYaquZqZaP0d9/G7+a2DZ+0OMInaPBNF/Vw2PM1hItnJG8HzrqUMm5Y8zH76OpTBvZ6OxzhX4k1Z+mGnD3qv97vgfk65fpWVTjzymIyjPuVMOYXSuC4GWhQJQHlfoxATE3Rqs78Spa+y/4lkNWGmqgFcHVHoS6jw=" - ], - "priority": ["100"] - } - }, - { - "id": "9ea96f48-10a5-42fb-bf64-53df044df527", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": ["e3118a62-6142-4a6d-8676-77b02f80bc94"], - "secret": ["5XzP4ih0trhxh8R-lDT1gA"], - "priority": ["100"] - } - }, - { - "id": "6641f8c2-f1fc-4b3c-8fda-fdb55c7755ce", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": ["7bd6a6e5-2dc7-4274-a1a5-a5983aa82444"], - "secret": ["9rFpOZF24rMuJGtf2eBLOYsPg73OJYYWMZn-MJsLSD06G6BaBeVx8q8i_kG_07qx5Dd2W33-Yst36qzkXOFP_Q"], - "priority": ["100"], - "algorithm": ["HS256"] - } - }, - { - "id": "9201d1ec-6ff4-4130-b1ee-088eb113aa85", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEpAIBAAKCAQEA0MQIbGxBHMkPp/yI6dSj3/SWWsyQrGe+ftdg8CCUMrlFRAq34wamnbP5k0GozWqAY6paTPrPGjcuXQ0EaMwL5OKdFaFWGXwLLjrmTicCXIEyb8lo3Ty8/izPxVlHLs+iZtZm5uP9xfL7Nw6Ja9gXhQStHfZPnt7Q31Ht4GfN08FrKAoGTsy+EyLv4o/4LoSUwRyuyvNpva3fuwTUoZPP7Y3Mmo/Ijk3L1R7cXelxNkh5ApN6whzhcec3yPQrveBed39mZ0WXyslSvVHCDQ5uvGj5fk45tRCXsOVTz01EOmAbm2W6/OOs8Ixw2Ll3l33+5U+M8uL+neEKg1aqDUew7wIDAQABAoIBACS4Bh9D3yP3/Uf3tAEkxHoUpAluZ5fbW3cl3Mf/gvF1AsjX9cX5mn6sdB5BczZGIDTndqCJkLm0sPPu4TKpiQIGFckDKoiq97B27aEbXV/13XAqBca78yXlrdmxPULvhEoANfMwcKdLeIITjXopdOGRk/1sIE76M9TDrUpGF77BufUrQUcg7DCDBOVjGTt83RMQpXVxobEu/PIYwyhUH6tOgdVl9dvcCfaShQUNlW8kYYNlMYW0Giude9Aeu8pZAUM6UXTUtkyyFdarerhWFVSTT0x1u7EbqhdeqANmDbkpQ/sNCBCF4qwj2ff7ZN9M/oqf6wyoHzOV0RTaHJtpawECgYEA8h7beSfOZ0q/QVYymfGGSEnl99twbX+y0IrrfaudX0410kXGrxRrplxq9UfOfnVCl/LmOqr36Ne1lqDiGPRQWJfNp74NIMzs8z0pLLZJYfhd2yiUh4wtpMh+dV8Hrnm6lLvNhT9QR7bnm0OhESv3N0MgMHsch7AxhV0l21cu9YECgYEA3Luw3L1opSXRHgyeyAFiDD1YD3rTM0HaJKO/DjmmC10AlVmnYYZ0rvlvnrIE82d1kxG0Std691ILnvHHw9Vh1mLprGFCszwgJGicNy2Tr/DeXuaXmifVtUFxNaxYTeFcPCJPL2BjmQ9Naxhin/g4w5maK5LduGJTUR8aDjd2Pm8CgYBMGeTT/O4ES1s39xbqih6x5ABTWnbJBAU5RSDlnCZXyWZjVCkx6JI5dPztYYeG+eZXijJRKGHJnttln+XRACGs5vHuEm9f6uljPssNUbJZB87ATs34mNfT3mzZCWiJr5s0mp7rjc327Id5ptUeZ5pJlWCtvFRoVboK+A8pFQsegQKBgQDJHF0NEanJZkY8iaUVd2Uc37tfBzpsZiBZ57NIQ7AMhGTmrnO5gKbJUUyom2u1VVsjbysEUYWg1ujtnT60J7NngGGFBGygHzTt1z4Va/o2gFAqyQ/xjT/CUGjUTT17X8wIof3hnYHBT9bqr6IUPDWDyWxVLQ/EUhm1PJAhydh7EwKBgQDNgKsBjTrTpDtcewfYarAlyLnmsJ1Fd7t4ICpKl0zwOcBacN+IqHwKZ/CTnYbpuwbURm6B0uQe6ApJ49RRFIa9Zdi9hQ4MkGEvDmAYRxuzqYPm65ssYVwcPPVYNpo8TfY3ulCPhR0zKKuZS968SKUlVWxhlqovZslFZD01OAYRrQ==" - ], - "keyUse": ["ENC"], - "certificate": [ - "MIICoTCCAYkCBgGIIHh17TANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAljdm1hbmFnZXIwHhcNMjMwNTE1MTczMDQ1WhcNMzMwNTE1MTczMjI1WjAUMRIwEAYDVQQDDAljdm1hbmFnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQxAhsbEEcyQ+n/Ijp1KPf9JZazJCsZ75+12DwIJQyuUVECrfjBqads/mTQajNaoBjqlpM+s8aNy5dDQRozAvk4p0VoVYZfAsuOuZOJwJcgTJvyWjdPLz+LM/FWUcuz6Jm1mbm4/3F8vs3Dolr2BeFBK0d9k+e3tDfUe3gZ83TwWsoCgZOzL4TIu/ij/guhJTBHK7K82m9rd+7BNShk8/tjcyaj8iOTcvVHtxd6XE2SHkCk3rCHOFx5zfI9Cu94F53f2ZnRZfKyVK9UcINDm68aPl+Tjm1EJew5VPPTUQ6YBubZbr846zwjHDYuXeXff7lT4zy4v6d4QqDVqoNR7DvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGGOT5zqUXa3vj683lB77y81VszT/raGg/GOMxOuL+9jDRnqKukY4keqT+O0q8xHn4SxQ2r0P+1dQ4WOUL91Nmi5lAC31O411qgbRvNF2WjYiqwjYr4FxJxiCruCx0fnDjl/eCfQeojXt0jmOwkRdSeZW3oyxwSv1g2p4jVe5ICczDLAmCgF+3SCfudSMzdf533s9FiB2rYeslePYr9+/ukWAN9eZH7Gz7c6qEEpYmfs9IgK0MbYwmgjNIPvSu1R3Rg5O9Zbi3pC0NcLy5hTiFB2M68fsM/ZRz4xsh2K5bLDZdiqbZCgTbrw10yoZZ8YhqKcIYVtyizzs9zAqqNqBLk=" - ], - "priority": ["100"], - "algorithm": ["RSA-OAEP"] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "4689148e-de94-4e88-868b-b372c1903794", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "1e3160c8-aa62-410b-9738-772a7105ec4a", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "0a62d54c-461d-439e-98b3-e0c3cb80d91c", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "ba8d3a35-c9be-4e16-b08e-3f4cfd24db8e", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "c5254378-0be6-4bd7-8608-3a0ea47a92f0", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "cd365a4e-ec37-43d5-adf2-55d584f8ea2e", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "d3693eb3-71a6-43f6-92a3-d2b0d8bb0766", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "ef949287-7d40-4f51-8624-f7482c0f49dd", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "012dc79a-df7d-4e16-8a3b-99bc50c3787d", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "6014612b-5c73-49a2-9644-b10a01dd325e", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "3b401240-483a-4bf8-a00c-61ee5b6fc526", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "6ed0653b-d12a-4711-a069-ae2533e2f83f", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "4b1643a7-2721-4794-867d-da29ec85b43a", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "efd91669-c825-4d4a-bb54-05807c724e6e", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "3ce46d63-d520-4c2a-bc7d-4e004469a948", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "280afc5a-1bbc-484f-964b-097308132b4e", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Authentication Options", - "userSetupAllowed": false - } - ] - }, - { - "id": "217c1bfb-125c-4ccc-969d-aebd864055f7", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "00627c7a-3e3d-4535-8c73-6167c8be0db9", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "7915f9e8-547b-4a89-b933-eeb23b2ede44", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "c18c9716-c71a-4bae-aff3-6c2e42d2dd43", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "a75d1438-49e8-46cf-872c-a455338010f5", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "f000330c-7003-4872-b439-9885ac5c34ab", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DeviceCodeLifespan": "600", - "oauth2DevicePollingInterval": "5", - "parRequestUriLifespan": "60", - "cibaInterval": "5", - "realmReusableOtpCode": "false" - }, - "keycloakVersion": "21.1.1", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } - } -] +[ + { + "id": "02814271-c58d-403f-89a6-7c831ad6a61d", + "realm": "master", + "displayName": "Keycloak", + "displayNameHtml": "
Keycloak
", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "bd4a79ab-faf3-4aaa-acad-6512a3e4a9e3", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d", + "attributes": {} + }, + { + "id": "b779f038-0fb6-492a-b8ce-d167f5a5f777", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d", + "attributes": {} + }, + { + "id": "d99b3c3b-38dd-4a7b-97c1-dce5a2f6d137", + "name": "create-realm", + "description": "${role_create-realm}", + "composite": false, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d", + "attributes": {} + }, + { + "id": "992878ab-f90a-47c8-9650-d6f08162b5ec", + "name": "admin", + "description": "${role_admin}", + "composite": true, + "composites": { + "realm": [ + "create-realm" + ], + "client": { + "cvmanager-realm": [ + "manage-events", + "view-events", + "query-realms", + "impersonation", + "view-authorization", + "query-users", + "view-realm", + "manage-realm", + "query-groups", + "view-users", + "manage-clients", + "view-identity-providers", + "manage-users", + "view-clients", + "create-client", + "query-clients", + "manage-identity-providers", + "manage-authorization" + ], + "conflictvisualizer-realm": [ + "query-groups", + "manage-identity-providers", + "view-clients", + "manage-events", + "view-events", + "create-client", + "view-users", + "query-clients", + "manage-users", + "manage-clients", + "manage-authorization", + "query-realms", + "query-users", + "impersonation", + "view-realm", + "view-authorization", + "manage-realm", + "view-identity-providers" + ], + "master-realm": [ + "view-realm", + "query-groups", + "impersonation", + "manage-identity-providers", + "manage-clients", + "query-clients", + "view-events", + "manage-events", + "create-client", + "manage-realm", + "view-authorization", + "view-identity-providers", + "view-users", + "manage-authorization", + "view-clients", + "query-realms", + "manage-users", + "query-users" + ] + } + }, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d", + "attributes": {} + }, + { + "id": "06bc6684-6927-48db-a660-287404cbf8fa", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d", + "attributes": {} + } + ], + "client": { + "conflictvisualizer-realm": [ + { + "id": "2e78e018-59ea-45d5-a4fb-652e6133b997", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "0bc73818-d00c-419a-8c85-2de05657dafe", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "d3aa5cf3-3062-49e7-a2d9-7d5f33683e0a", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "d21fb43d-1042-496a-b43a-8cfcc1523876", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "4654aedb-da36-4906-aa01-c6a7e7bb3143", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "d43e2f84-8529-4520-9d23-d449163a465e", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "e904cc9a-836e-486b-8157-2679943837b3", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "d4e07615-da08-4d94-bf53-d35c028b804d", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "conflictvisualizer-realm": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "8bcbea8d-b546-47be-90f0-9915ef020d3b", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "69c59805-9b6b-465a-8716-e29171c08aa5", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "conflictvisualizer-realm": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "22af2d25-709c-49d9-8c74-4734bc4ba657", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "0fb7b12d-8428-45ec-a6e4-5ffd3611e948", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "be15bebe-7cfe-4b28-80cf-72c2ed322f36", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "baa70d85-9582-4d36-be1c-5b82af8f1499", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "68be5ffe-c0f9-4787-ad40-b165d45c10e0", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "dfb62192-542c-451d-bf8f-8f9223dbd43a", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "25d83c7e-9ff7-4919-b652-5621abf277e8", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + }, + { + "id": "83035a41-237f-43b0-af81-b64f3b840af2", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "2624779c-3669-4386-aeb4-9a4623d1098c", + "attributes": {} + } + ], + "cvmanager-realm": [ + { + "id": "b98a18db-f6aa-4aa7-9400-f58d9f42cbf7", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "35ffc78b-3273-4c64-8402-8cf16a6d8de5", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "1c596a80-7724-4539-b65b-fd81d9438c35", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "9e599de8-e423-4675-9270-1bd5b9bb10f6", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "0baca7e9-cb1d-4c4a-a1a7-399c7ed55c53", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "32874344-b840-4a51-86d4-cdd02b71dda4", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "66f4dcd7-6b74-4efe-8d1e-50c8dde878e5", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "2906b449-5d55-43b8-84ee-7c0d9d62ece9", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "3aefc026-02ae-474e-aed5-37c337e91d8a", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "118812c2-df0e-4063-a9c8-e88b291d338b", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "cvmanager-realm": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "4be88ae2-0a89-4a44-9bfb-b3a33d69f6aa", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "ae8cfa61-3ab2-4f6f-bcd7-79d5f2d5ba76", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "c8a243c9-0dcf-4bf0-9a99-6584fff57779", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "59de74e3-006e-4db9-9d4e-d4bee6770690", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "b8d0343f-1ef3-43a8-b153-c549af1b3740", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "948a3f26-3226-4d76-adb5-7adbb53b4acc", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "9ea04be7-11b0-412d-b9b7-ad883281dee8", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + }, + { + "id": "a7011af8-ded3-4957-8f1d-2b48002de467", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "cvmanager-realm": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "67679c37-5126-4e3f-8890-250eaa98fe6b", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "52b6ea69-e6e2-4f49-b0a5-794e544dbecb", + "attributes": {} + } + ], + "master-realm": [ + { + "id": "9e9888f9-0004-4739-8805-7a44b78a5013", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "0393f9b6-6cf7-41ef-bea9-0bea3b7f7441", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "e664e442-6f4c-4342-87c3-9e248c2e58a6", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "f615ef47-0e65-4a36-af7c-dd7922d2d0fe", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "013995a5-e2f2-4508-b60b-5d0dd86d487e", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "3cd66d48-b3cd-4d15-a881-465dd003dc51", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "579d0d70-753e-4262-94a7-e3567d620ea6", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "cf2b4258-8ba5-4f80-8462-03a3f56ba8c9", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "master-realm": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "4d67a073-0974-49d0-bbdf-7a188dc9e96e", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "ddbb98a7-8f59-4c6c-a29f-c70c79f66558", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "79cd50b7-3c90-4029-b3b7-ec5adb8a1ff4", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "c9aff25d-0b9e-4022-95ee-4b5ecdbf3cdb", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "5d7e4744-05f7-44cf-9c69-6cf18f2527c4", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "master-realm": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "6271ea42-44b8-4555-80a6-97adb61862b3", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "5698a987-b757-4780-907e-19a192ff8520", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "a0e4ed03-062b-4327-b657-e205e14a5d78", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "f0afa4f1-111f-4edd-9949-c6c107379cb8", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + }, + { + "id": "70e20742-cda2-4f57-9379-e061a465831b", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "attributes": {} + } + ], + "account": [ + { + "id": "bf2a8395-964e-407b-85a0-a9285e10d0d3", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "de121f4d-b425-41b1-9e50-62a317dc7058", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "685cf824-08b9-4f56-88cf-2f30a49678c4", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "54f35edf-5ec9-4ed7-8e37-7b90c7946e63", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "5c39303d-9e63-4d44-a1bb-ab4e740987f2", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "582c9138-f9ad-4398-9b8a-1df421f659af", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "2619697f-9fef-452c-8b99-56833dee8d05", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + }, + { + "id": "e4a83114-d493-44a9-916a-713638b0260e", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "06bc6684-6927-48db-a660-287404cbf8fa", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "02814271-c58d-403f-89a6-7c831ad6a61d" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppMicrosoftAuthenticatorName", + "totpAppGoogleName", + "totpAppFreeOTPName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "1f20ea0b-1d1d-438f-bc3a-fee434da6243", + "createdTimestamp": 1705363658618, + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "e37d03ed-f109-452d-81e2-eaa33fc0d6b0", + "type": "password", + "createdDate": 1705363658806, + "secretData": "{\"value\":\"E+FF/wYlGr4B/mB/DZgy4teG+1VAU+rRM91pon4vhao=\",\"salt\":\"j5llmMYTcduaAbiHkS5xRw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-master", + "admin" + ], + "clientRoles": { + "conflictvisualizer-realm": [ + "manage-clients", + "view-events", + "query-groups", + "create-client", + "manage-identity-providers", + "manage-authorization", + "query-realms", + "view-clients", + "view-users", + "query-users", + "view-identity-providers", + "query-clients", + "view-realm", + "view-authorization", + "manage-realm", + "manage-users", + "manage-events" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "71cf538b-cf82-48b3-a961-eb6e65e48559", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/master/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8517338d-71d1-4b50-b04f-24782591477a", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/master/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "1c09b781-0a7d-4c42-ae9f-f13fbbc0abe4", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "57a06e7a-767a-4756-99d1-803e9d9fc7db", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "52b6ea69-e6e2-4f49-b0a5-794e544dbecb", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "2624779c-3669-4386-aeb4-9a4623d1098c", + "clientId": "conflictvisualizer-realm", + "name": "conflictvisualizer Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [], + "optionalClientScopes": [] + }, + { + "id": "f712d6f1-710b-4912-9c3e-08d2aa666ea0", + "clientId": "cvmanager-realm", + "name": "cvmanager Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [], + "optionalClientScopes": [] + }, + { + "id": "627795fa-3d2a-4362-abe1-1a334fb8f622", + "clientId": "master-realm", + "name": "master Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "4d685882-aae4-4931-8f69-add8a8523629", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/master/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/master/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "ab3465e7-4061-4e67-b723-cfd4cd451112", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "29cce42a-1dd8-427c-9800-2354b121570a", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "916dc869-fbfa-4d9e-a7c9-497f97ae7f45", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "758be55d-e03b-409e-a414-d50ae3c66079", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "3a6964f8-45ba-40ca-8460-04232a4128de", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6cc7d90d-3155-478a-a6bf-f07a87ed5edd", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "3ee5c045-e277-4ece-a94a-5820ee03b63a", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "9ae3835d-4427-47a6-8258-ca454453d1b0", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "3e27b6b7-a6de-4727-b5fa-1e15177bb660", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "8030f5c5-2a15-4d86-83b7-ada47a49882f", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b1a617c4-b0a7-4d4b-aed2-06a47efa9bf9", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "e94d2bb4-4745-4069-9652-eea4a9bb7e7e", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "3cd831e0-7cfa-4e89-a8b7-aef4d8cf5de2", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "31f0542d-dd28-4569-971a-794f9f6480f8", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "61d8b05c-9ad0-4794-89ea-92bff60b88b6", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "cb1b4b5c-7128-4960-8c05-b9b98eee380a", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "3cbe8cb7-766b-45aa-9fac-e98b4b650f8b", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "6863a7d7-8781-4d42-93c9-bfc94fd942bf", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "c1f49f64-df6e-477e-b5c1-b24d129d2fde", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "309710dc-f62b-4dec-b05e-f3d4988bfd2a", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "fc3d969c-174a-407b-8d00-ca70dccd009a", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "f104e06a-51b3-452e-9f9b-15e09fe602c4", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "9c4947cc-86a3-4f23-b74d-9d46c4ccc81b", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "2171e7b9-0228-400a-830c-94f7a98d0e5f", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "7decd40f-93b9-4a48-8d72-e7d3222ffdc6", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "5989f9d5-fc61-4ab5-bb38-cb26a403b98b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "ee8d8715-124b-4db9-9ee2-f22e6fae1abe", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "0ddc7227-3852-4240-9bae-01cbf7220610", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "37d8dfb2-7923-4aa6-9ff0-69167223915f", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "cbff8f8e-ea04-4f20-98dd-56b9464571a6", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "a227691a-435f-46ee-bd43-90ae15c4050e", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "76a6facf-6d3c-4043-9030-ca6cd386022b", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "4212318b-046b-444c-994b-385fc29f9bc3", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ee3703af-47ee-4e1c-9134-5cc1c0e37869", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "7e1736c6-e1eb-42b5-8fc0-56c7b9b46df1", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "600a3607-5d33-451e-b19e-93263134044f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "f2c26cc5-1a6d-421a-a2ff-9147b13b42ce", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "188f63f9-6266-49a4-8ccb-b32b61c27f20", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "05063684-d19c-48e4-8ae4-7b7ea240bc86", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "42d42ffb-b8bb-4e0d-9b51-c7fa0af32af1", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "837b519e-6251-46a0-bc63-1fc1431dc99e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "d3d1380d-a53a-402d-bafc-064f84e5e104", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "703740b3-0f75-4051-85a3-0e0780d72b13", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "a9e75b9e-8bcf-44e6-853c-d9159e1f02a4", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "3b94a71c-209e-4a7f-be13-332ae401e3ad", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "450ce117-a44f-4257-8f12-eb444f2e9b38", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "f0b6b70c-563e-408d-950c-d4591b299024", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "dfbf699f-79c0-4f35-aaa7-db56e599f03b" + ], + "secret": [ + "W9kRtZFV_dTxQNGd-nueSDnnNxWBfZ08d76T4W0TTi46fO7Fl5Wh-5QlciXAXsps96kMSN0hfJeYf6d_1fIeBw" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "61498a0c-eaff-44c4-9c77-358502ac5570", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "c9aa2fa7-ccd5-42ed-9a7b-a62772c09191" + ], + "secret": [ + "QoOE0VU8d5-rflhDANBHxQ" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "06446ced-8eb7-4420-bf4d-1f93ac26c66c", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAwT7c3z44h6uPMd4X7jqoH0DAnjyi+PoPllJXl0M9km0uD/ppE2WG/BLAdWepYYoiSbRv3iXjx7IJ6PPkQ4gJ4Gxj3+6yREruN3FrvAbD7VHLpoKsdGRnzmnHnV+XOE01a4KIMGp9wBnZzQ35PrTtkRqp5qLtkZ0KLtNAEGNKLj4lZtW/aLUg536NlSCn/LmvQhtpF60BtfKfGSPi5tEz3DbfBI9rxlo7K9ZImH6Z/HDEjSKrQhlPdiBlqBH85xRFTHTkP1ESusJ/grKyT3dlIFUjTTmh5A1EMsMKkO0CmczIdzhXbMZ1MpisQXgEU01dbLQpjDslkjqNzKLfiMwWpwIDAQABAoIBAAN4V6HQOSM/LqgrL4/dJRAeILYQTwEzjKgGfzlcakQIgFmwYbxVtgd/ZjmUNY6PhAFjDtj2LH+yv1nt0c7/CQ6wGp/7uCa9EoT6CLPTJBNhusE3x2m4chYx9hPfu5l4TPsSkMmABc2n5A+gipHs71N8uOL1KAcI7Aq6p6zE8MnzMv8TUH2a7DF/EaBDrHOX3YbZJHV88+0g3ILf6cBtRVhvIyB/wWaQb01AV/H3Fg9z/YMd6SgbJItn0wWpkePT7bXKIuK5O1Ba8Rcepw5wpMHOeX/ZtK1rm8PplgcuOZ4v5jwnBlvGTO6PN3LhPhqf7itlHncRD8ujlgvX+HtvjzECgYEA85y71HAsAduWZuOjPDk3BBGpF2QfRhLm5qEvw0vxpOwFkynGlieKiEKI5BAGY0w5iz4/g95yiFHEF/bHz/UUXm9LCe2Gy/cd6v46daqfWkhHMEFlNEfxHCRCj9d/J+Kuxpi9K5E/qSHZHC+nHo5V52gpZ8HlJZLRqrfqYDYKoa8CgYEAyxJ4wGv2uQdZ63GKP10vJ2jyPUFmLrysd8ekqpyyFTX1cKcxtOCnofFJ9xURCYHV7NXkJbBZU1tXibHtOvRwzjjqvllr9sSCdhWehxSdta61N4hlYYFIXujEMC3boT/d72BVpQyE7eZ64/1fz4gesj8rV91AQmjzjtigRZJRcIkCgYBZXze9YQWUDOYpivu4vVjEomIBVdbvU0HofFvUbwkQsxH8gkf7kDgPczFbUdG2HiHCRqzwiOxFvJGPJRb64PN/DZ9e3ggkzdzo+CmkP1tEuN19A5DIVFhNNbRBpxJcJJpv+1rzH89WEjffUlAiMp+rTJhcG1MgrLNEyUIv18OguwKBgQCWFUzRSfnKvjgi3oNCWWhkRBfkVdVjbWY6EH8O6UhkjMCdRbRi7jZ2ZZI43oT89cxZgatgf3lFNhj4V1vxWn+UqlQz4nr8ojeZdlj3lLEKedjM9i2XZqlKG9YDlaDhCAbKx/QES8Bi4xioL7cD9qJZMn6iLY80hcScKlYplP5DoQKBgA3hirTJB+2+D3ou+HQJEMP8X8pMZ9mNQ8YBqOQEm+UaQ4Cg06/HLdKqHmZ1srpGybZ6atZwddW90lEbWDUafz9x6K7AUpEfyx0jw7jfLDy1Sl9SSbU3zpVUZJbJfPVL/xZic9ACJVa315qTI7z45fB+pCau6Xv6pmEeOgSCo1BQ" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIICmzCCAYMCBgGND5WfHjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwMTE2MDAwMzAwWhcNMzQwMTE2MDAwNDQwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBPtzfPjiHq48x3hfuOqgfQMCePKL4+g+WUleXQz2SbS4P+mkTZYb8EsB1Z6lhiiJJtG/eJePHsgno8+RDiAngbGPf7rJESu43cWu8BsPtUcumgqx0ZGfOacedX5c4TTVrgogwan3AGdnNDfk+tO2RGqnmou2RnQou00AQY0ouPiVm1b9otSDnfo2VIKf8ua9CG2kXrQG18p8ZI+Lm0TPcNt8Ej2vGWjsr1kiYfpn8cMSNIqtCGU92IGWoEfznFEVMdOQ/URK6wn+CsrJPd2UgVSNNOaHkDUQywwqQ7QKZzMh3OFdsxnUymKxBeARTTV1stCmMOyWSOo3Mot+IzBanAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAL38ECCrYGlZDfY1urNoQS6xDDgSyyzTN8jhKpffur1K7t2Gsa9LhjKAYYmx0/BzP/3e24QaYIGfM/kMx2FnnQ/eXA+AFy8pGAAHZadag2WKNu7aU2DY4ng9KubfdA4HvxZFbhRJVg3sdF02y0OxY+UWEV7y3/LKbwjeOwDNvth7NTGIvip10HGMGb2PjCdtQXFF8PcFmfx6uJPKDqOyzS3StG3fquLmhfk+YB3MdVmsMEVxs8qP1ReiJ3XJV6lUKd1A3AR1R/+odCXnsYgf/rjmhauwzF7F57mumLBLS1OADhOLLCFs6boMmhJF5t7vs/084kcnGcVCRBNOSppE+rE=" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "69ff2c13-dbd4-4865-b2c1-1028560549e1", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEA3h062LCEjaFRP5RmGymtdXcdn61ZrINb9j4kMt2dSYDi1euN9G85LBePslVytwv8rk7k8Z4ReWf33GGJjonBFTKYaL0g3ZRCp4cizWtDIJtXBJSodjsigze73rMXs395Y4n/uMcKo1c7va16x3agUrntX9HZRpOGQ0UUpYzzdUiMYGoa9bWKuvxqAQDonNRXx04Mz+J48TtmfB73R9pM07NvclihZikp3kUv43vffKbu7jaEVcmBO7Zj2rlTHB3xRnDNhGKes4bK00ldzdUwpjWyqGw7fxMj0XK19VPbIaqwq7VJIqi4wM6qcZFKo/mFIkvvLX1jZLLSe+AoRCrznwIDAQABAoIBABN5M5TA8SDWYQsL0IyWxwRH87scAIMypx2ROCzqJ3qaHV53qaP3G7zvueOpldunkOPwEHwrNB6qoggFLda8jBxMKcxGXKXLSbFqFH/huhUXRCCnhJeNEAsPbV4VN0GVA7MEQSfVnOtSNW99ULOWhRrg/dygnNs7tDycnLahA3f866O6hHPF6ggT3j9kEV54ya2krUmU0eGZXZAMcIAbuGgJr/MPiE5u9r2VIjwJTujyf1TgBMpfJaWLOIYDUs528VEZUFJbv5CHc3c8cKexiBafqcz8/keL7teninV6ASLRSNmYHjhFtIk68OSKYvZqJAceZR2wYxLh/IwP2+xH3o0CgYEA+k8hm/x5XZ9PuFT5Hl0SluYLumIUeDRvzwK96WdlBRe2tVmHvDg9wQFa2YyDx5nXl+YoglnlgAZ+Nk34L1sOBCpEZhFZ7c0efLZEtjFf1cbO4Ar0FgDfU6KxyZj7HXJn0FvgW1h1KleZV5miaTfEGZ3uE841TlFcKWk76tC1UEsCgYEA4yn/CmZeYU+ZbqTOWStOluXzfJpHmZOFE+cKlICH6RrcX7VCxhjXRVgJ4dpjAMnGUfkL821V3JknEPFyKS1JpfSWvF5751re97MtRysD+2xw6P8rrgjQuuHE+uZAGuCWCUjgFUUElSYgNPpvaU508vyi4CDYrZoFzEplzkC23X0CgYATRMYskNny6BGl+fyXZsjIjvr2JRi4TCkTQX3HGut+4d1xxmuZhKbUVbtdpeB7HA+ppNEXf74YBefvXD8vvg2tKmfLh6hpkvG23f0aHWDoPv6r5ov1qamHca3H/BvQn374Xio+Pef/E3E9ehkzilRxOGQcaDJYThEPKweuwtRCUwKBgDjrG+laLwnI7RPpHX8AN+fdZD3zVj9n1C9hc6gz8Fn7Df65JysFrGLGpWs+0hGvfQ6rDVCIM7xbb4tyQ/2HSG3ZtC8sqXUVsspzzcOIRq4nxL7MuQAZW1uIGFgZezSA03cuGF+b9IL+k5FSsrm7G9iKbrEj6cbN0egXOB0O4ALtAoGBAMHZA9TqnWR+G8rj0eNSR/YXVQDY5uQJbj2sCke2VvwTZ0G5VHK+xuLESjsy5KMKYQtcT/ZRR6EDo0Jr/xbNeLs+D1iGBklRUOwtED7i7mlPDj6P+nfLAWR+k5L9zYg5mddYdq6+J7nf/l1VDnppE3NDwETOTG8nd34ENdOHn9qq" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIICmzCCAYMCBgGND5Wg5jANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwMTE2MDAwMzAwWhcNMzQwMTE2MDAwNDQwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeHTrYsISNoVE/lGYbKa11dx2frVmsg1v2PiQy3Z1JgOLV6430bzksF4+yVXK3C/yuTuTxnhF5Z/fcYYmOicEVMphovSDdlEKnhyLNa0Mgm1cElKh2OyKDN7vesxezf3ljif+4xwqjVzu9rXrHdqBSue1f0dlGk4ZDRRSljPN1SIxgahr1tYq6/GoBAOic1FfHTgzP4njxO2Z8HvdH2kzTs29yWKFmKSneRS/je998pu7uNoRVyYE7tmPauVMcHfFGcM2EYp6zhsrTSV3N1TCmNbKobDt/EyPRcrX1U9shqrCrtUkiqLjAzqpxkUqj+YUiS+8tfWNkstJ74ChEKvOfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJX4WIFQtIxBmbG/0NlJ+aGTSmayU1Qo+La2Qy1yvclX0OfvyQBkXlvOmnDRsBdl/lKXh804LCHl2qXFbS8r9Qy5d/Vb71hynUc5IAHyRlMY3xaz8mH8xxFZh7iUFkl3hXA4bNqa9f4zDmTyoNjlFn5pgL98PFqU3Lc4nuL73woow5JCMuTALBj9+IKP6UZxh05eR5T9JEV3fwXW+MCvRwxl8b6Eu2g7WOykk3IjMV7u7zBGtTjMsQAREmFTtP4Pk2oeeDr08YpxufSZWAY8nDtVteq54ueP39fnAYwrApRT5zFrW/XbMtYffL+l/VHwCTYRV1BqcyUNrRli4sMYSYw=" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "d7aa1a03-710b-49d2-a00c-50b1ea6748c9", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "1ba2ed67-c3fe-45ad-a7e3-795b6a38dc2c", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c386f7b8-29c9-413f-87d1-d933915dc3e7", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "3ac84485-321f-4e77-abfc-030dc77529e8", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d7e11250-5ebf-4d80-b153-b39fad8f6cad", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "14e1d17d-fa8b-464c-9832-eef4bd85d6b9", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "e0e7552b-41c5-4d6a-a4ed-bf90f1433735", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b0ad2965-b8ea-4a51-8856-e45d45e8eb60", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "4a5d6b91-e79e-4b4c-8216-8b6c9cff3f36", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "c382f1df-9077-4eae-984f-10702a4bc7a6", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "b7edaaf6-753f-4ec2-9aee-7f3412947284", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "72ce3252-9d35-44a6-abef-911444df1d03", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "f4100e5a-8b9e-4da2-a986-e83882c21e84", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0436c184-1a19-494c-8d2a-b79c36c5bc71", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "2fdb27a0-1e16-4cd1-913d-7ee9b6b78ce0", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "428ab500-bf84-4be0-aa20-e4e871b286d3", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "80098b5c-3b99-425f-aa21-c417d767693d", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "84d5284d-3ece-4eed-92f1-94d5fa84ab4e", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "5a099e6b-b9ca-4e97-88f3-ece9dec4586f", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "4470b4dd-caba-4dce-80c6-d33e1f07e888", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "f1a043cf-4964-425a-ab22-a652ee6533bf", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "37079540-b45a-4c8d-83ea-cb05f4716465", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "21.1.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } + }, + { + "id": "32e7a5de-88df-49ff-be58-a2358342aef0", + "realm": "conflictvisualizer", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "ff8062e9-a9c7-4a9c-90b7-758a86099e8b", + "name": "ADMIN", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0", + "attributes": {} + }, + { + "id": "9b5ab543-924c-46f6-a95c-e8aaa437d62b", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0", + "attributes": {} + }, + { + "id": "a53c1d5c-9187-45e6-ac0d-6be9218f805a", + "name": "USER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0", + "attributes": {} + }, + { + "id": "1b1de91b-e9ca-4e51-9082-537ef41dc8e4", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0", + "attributes": {} + }, + { + "id": "e25d782d-34cf-429d-98e8-d36bbc3bc08a", + "name": "default-roles-conflictvisualizer", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "7cf9af60-632e-4640-beea-4ef80881aa08", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "9bb81033-342f-4aa4-a128-b976963c4971", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "7abfe6d6-9708-4837-a457-02f9905be352", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "ede3127a-7ce2-42ba-8a7e-21cf67ceffdc", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "8731118d-2536-4b8f-ab1e-ff95afc40666", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "07d4a3be-7629-4f22-89bc-f0d07006f7b5", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "30f72814-fa03-4b37-8319-97315d1db553", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-clients", + "query-groups", + "view-realm", + "view-users", + "query-realms", + "create-client", + "query-users", + "manage-identity-providers", + "manage-users", + "view-identity-providers", + "manage-authorization", + "view-events", + "view-authorization", + "manage-clients", + "impersonation", + "manage-events", + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "64047262-8da6-448e-a64d-faa4af34bacf", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "fa1445c3-be1a-45d4-871a-575922b3d88a", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "bbf6d8f8-d404-4d53-961b-3409a90c01bd", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "f2f483e4-5099-462e-87c5-d6b873ec9d55", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "5040056e-df68-4163-b8e0-07748cd3c136", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "e9703c73-e759-46a5-8cdf-c2b8ff47d975", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "881661f7-664c-4c1b-9ccf-b2796e01f1ba", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "6cc17944-ede2-4e5f-9431-a6146c29edea", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "f8db450b-bb97-4379-a60f-40268ed2a04c", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "6e456740-86dd-484f-8cef-16669e7acce0", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "3004d1c7-6687-4357-9ff3-a518ba1a6e1b", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + }, + { + "id": "4cfaf664-c3cc-41d9-a54c-53370b9d2f91", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "attributes": {} + } + ], + "conflictvisualizer-gui": [], + "security-admin-console": [], + "conflictvisualizer-api": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "6f593404-ca94-4b2c-9d6b-bb1c1072cda8", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "0f9298ea-1f51-48e6-ab84-bbbebabdbdb6", + "attributes": {} + } + ], + "account": [ + { + "id": "ee24988c-2b22-4578-b919-83e915373ef2", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "29acaecb-1d89-462e-90f0-308fba35db2f", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "72fbc8d9-7950-4666-8dbe-10dea80b6a5e", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "1c6a822d-be97-47ee-b812-b020df451509", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "2431c553-320a-4d27-8fd0-1f834e8fd788", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "f2c5f9ee-56f7-4435-a435-755bf81f5d4e", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "050e6a94-adb7-44b3-84ff-47070141e58e", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + }, + { + "id": "4ebd2de4-88c0-40aa-ad8c-68f6070cd05e", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "a1e4361f-e254-4dca-b8bf-376dea06816e", + "name": "ADMIN", + "path": "/ADMIN", + "attributes": {}, + "realmRoles": [ + "ADMIN" + ], + "clientRoles": { + "realm-management": [ + "manage-realm", + "realm-admin" + ] + }, + "subGroups": [] + }, + { + "id": "b3a885d6-a999-423e-ac02-64ae4848a27e", + "name": "USER", + "path": "/USER", + "attributes": {}, + "realmRoles": [ + "USER" + ], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultRole": { + "id": "e25d782d-34cf-429d-98e8-d36bbc3bc08a", + "name": "default-roles-conflictvisualizer", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "32e7a5de-88df-49ff-be58-a2358342aef0" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppMicrosoftAuthenticatorName", + "totpAppGoogleName", + "totpAppFreeOTPName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "e910ad4a-8756-4df4-b133-704184e908c7", + "createdTimestamp": 1705365285833, + "username": "admin@cimms.com", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "admin", + "lastName": "cimms", + "attributes": { + "NotificationSettings": [ + "{\"receiveAnnouncements\":true,\"receiveCeaseBroadcastRecommendations\":true,\"receiveCriticalErrorMessages\":true,\"receiveNewUserRequests\":true,\"notificationFrequency\":\"ONCE_PER_DAY\"}" + ] + }, + "credentials": [ + { + "id": "372f2b3e-463d-4c20-aa26-26b4cced268e", + "type": "password", + "userLabel": "My password", + "createdDate": 1705424820649, + "secretData": "{\"value\":\"aBouNLWB+IvHULql5QNHgnn/pcscKfJBlgpmHTvei0I=\",\"salt\":\"BJGYXJU8QNp976NGBeLMRQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "ADMIN", + "default-roles-conflictvisualizer" + ], + "notBefore": 0, + "groups": [ + "/ADMIN" + ] + }, + { + "id": "a08a9efc-cd46-4111-836f-b665db815faa", + "createdTimestamp": 1705365443404, + "username": "user@cimms.com", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "User", + "lastName": "Cimms", + "credentials": [ + { + "id": "125bad35-14bc-4133-a220-f730903c1d5c", + "type": "password", + "userLabel": "My password", + "createdDate": 1705424843090, + "secretData": "{\"value\":\"AraI20HIp8EBEVGOaZ5nSqx2s1RZTsG1zDdtry+nLcI=\",\"salt\":\"aogYKoMBFGzMfFfIOQHmpg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "USER", + "default-roles-conflictvisualizer" + ], + "notBefore": 0, + "groups": [ + "/USER" + ] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "7462f4ed-3a69-49c5-8f42-2d8f50bb95a7", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/conflictvisualizer/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/conflictvisualizer/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "c7c9fb05-a4d2-460a-a0b5-ae22374035f4", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/conflictvisualizer/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/conflictvisualizer/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "28000da2-cdef-4669-831d-3e0a91834b0a", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a3dea47a-aa8e-4eb9-bd60-cc2ccc6c6b66", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0f9298ea-1f51-48e6-ab84-bbbebabdbdb6", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "bdbb88ca-d453-40c2-b547-5d5887e8c3ba", + "clientId": "conflictvisualizer-api", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "70254050-2a61-479d-93db-a962a2f558d2", + "clientId": "conflictvisualizer-gui", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "${KEYCLOAK_CM_API_CLIENT_SECRET_KEY}", + "redirectUris": [ + "http://localhost:3000/*", + "${WEBAPP_CM_ORIGIN}/*" + ], + "webOrigins": [ + "http://localhost:3000", + "${WEBAPP_CM_ORIGIN}" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1705424204", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "http://localhost:3000/*##${WEBAPP_CM_ORIGIN}/*", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a2076ecd-5345-490a-bac9-77a7d69c280b", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "35726e9e-f948-4775-a957-e54bf22f490e", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/conflictvisualizer/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/conflictvisualizer/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "e3bdea25-9917-4f2c-b1ab-9ce4ae7d5080", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "afc4b9f7-8162-4683-9c01-483bdd32e3b0", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e7a98982-6b25-4bfd-a367-bc426a1f7ec7", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "dceb87a9-e406-4f23-ab9e-0c75f447c8b5", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "bd86a56c-d6de-420e-af7e-0366a84bdf3a", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "782ff344-863f-4e47-85ba-ff67e7e676c7", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "8caace92-3c12-4879-9d22-93f3405d1d1d", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "8790f9ae-bfcb-4bb5-b0af-a16a01b2e98f", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "f01f6fb7-7206-41b3-85ed-22fda33bc2e1", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d6ff8794-9169-40c0-81c7-2a354febacae", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "f46c56bd-85e1-4574-9f07-b56652ae3318", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "2c029d6e-9fff-4f5d-af45-031f063373d5", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e062b152-29ab-4df2-8e20-08b2d1a889db", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "3c39ef03-b23e-4f1e-8259-5bd0bc09e42c", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "cc57b48d-407b-4a65-abbe-4a31431fb9ae", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "b01c008d-c51d-4f3f-ba0c-baf2259c29a5", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "0d065e4b-738f-4db1-875a-8d3ebaa1bb5b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "4cc45c4a-1481-4135-982e-259ee1d56938", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "10879ff3-f64c-4135-aa17-bd2b13a013f9", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "05f2fc83-711c-4e7c-a3e8-4291a6dd9013", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "fa336b4f-28c9-407c-895c-d6e26c9ab9f0", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "6fb72d71-9916-4766-a534-20b20450dcfe", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "a6b7c47b-d914-43b4-96de-030e54a82cba", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "960f599e-4ce0-4c71-8348-e4956df844c0", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "ebc80e46-d76a-4052-86ba-12327851dd3e", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "29538e65-2120-4e62-8f5d-1892994be75e", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "596dae52-af67-4e0a-b255-3304701246a6", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "8caf64c1-abe9-4d44-a089-728f67002907", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "0cc6535c-ba69-442b-ab52-c7a1e65291dc", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "af03d584-d5a6-4ec2-b647-716249f6d33e", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "71ee4057-f747-4cf9-bc25-0e339c3d7feb", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "94dd0fae-f007-4997-9b44-fc71553b1389", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "9c087e07-7bbb-426a-9156-eb3883b0f1db", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "f8a9e9d7-f802-4e23-8a71-346f24879343", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "21828bee-344d-4763-a401-c46a5e31ac99", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "f576d40b-85cf-4205-ad2b-0b21c4126847", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "4e69f929-4deb-4f1e-91f0-991fb33b8955", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "04c7068f-caa4-4430-a2d5-aad9cdc1b346", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "5e234b89-1b21-4ab0-9038-c623482ecb5c", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "3b11b4d6-d71b-4c1c-84b5-82372ec50663", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "1f3180ff-08a8-462e-ab97-ca062f4acfd9", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "9e025d02-4903-4aaf-9d99-2278b38acc82", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "e4da1fd6-e52c-45a0-b481-bf62f65cd8b8", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "76200858-af8d-48f2-907e-6f05afbd1607", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "d7276741-8eae-428f-8866-b8a12d2f78bb", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "808e8552-051f-41e5-abc9-cbc3efd0aeed", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "7d2ccb97-d7c1-4a11-8d7e-9cadf7f1b8b9", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEogIBAAKCAQEAy4jPC9qu4pLJF5i1GSPYPaStuGi0o9k5CfXvrRynZbaZMZWg3r+t9VWsSuGuRXWvN0Q6ChzClV0SJ2jYvCV7qhHEE+1NE7dgkL1wh8e58dsHeKx3lFOtCVg6iSNsL65MWLerbaW2pnswXy/lkwquX8Zof5DD20fM6WrReVSb6mlO60SZ5c3WoidgAAo4HaeyXS/0ASrCAdKQwSHph6oYLPSHpDshDsitRBuS8Hk5+Ex1ZCw1ym/Hbs8Z5NeqQok1KzTCwwJ/EOPfkXuicQnYDHxvFAtDim0kepH8xw1T5+SV3/X7ICYTTIwQGNn0TB5uoZcO2BRSzsZekKdt/dT6CwIDAQABAoIBACgl7u2AMBIqcTdR3Js+alfShaAEK2nMngc8d5A2wEB8keBpiweVWNu1kBxfQxCZg7wLncVD4hAzgTK76FDItgmYooxpuVQDzq0OaUWnXKL8GQ+xOY9NKCtZN4a4sY0APTgc1th0oUBauXJ1ULw+FaJ30UIkjLXTBnUeqH5d9bl5vQsgcCLxhUIGM8OvOaLFex9W0tg9AlmW2f7ZgybP6Fa4uJXSo02K4zwDAwtOQmnEvCcfI2qMSDGZbdeXUTZUkys66s1q6OrXc1bo77VSzA1ClHZmFM8AO+wHuVgiK9tTtipUJ4MuuOVA2jQJHBs7oSYvTV/azclNCkJci5Fwi2ECgYEA906N0jWEJQJSDqe+YxbKH0gBE4g8TaShuaT+ljNHQCPuT/7NnAIEMci63eIw0oseCM/MfJnjPmP5naFICqlTWLQYjlloxrzVkz2y/wrZM/YJUCFGIP26jMcJLPYHeLK9QD7utp+P6UJ9fMQ2kkD7BjErEFV548fscSptBh+uAiECgYEA0rBb0iCETlUeSQATP9U/V8HdN3Hf6ua8limu02/kFWGwnSoDPuy+IU1fAMuFkgMBiGpJjbBw6mUcuhj7YkvQybnQY6e8vpS+f9jNoFCQt2n+r61XPEqig6fbmYKiJk9Al9GjRtomVikIjmv4NIo3v2qe4ExH4haqzjZChriDzqsCgYA2KlxhmBsTSAjU8OSAK3Olmk2yC3q5vr81O/AO0bhfUf9WQgaijsaAaOiUxH/Q/Wtcnra467Ob7KW1Yqe2vhNlMDzYoLiUHrPghfj4Z1XfTZoIlOEZRLlhMA7QbCqCwxM0SRRbp2MLweZeN1OEgPr6BjbaYv5JZ3Zf6tzqJHImQQKBgCDW2kkDRnwLKmBIgbeWXnwoPHnS5wrvEf/52UUdkJiAlI26qazaK7x3GdK+5j/e9hM0Nei+0qrGPdcH487rcEyxCLkvwOyXtKWqvko5pITiIY9yXkGIhJIuzLy9rtZ3zeKcC24UvJr7ZFkGnTZbQNs2HDNr0Fx+GftwW6gyBGFnAoGAKIKCtasZKWOYeaSHDGEUSw8xLEnC7lXy0dJ/68X++06aMeQsjfa4nyEmSiuZ/IdqK1m8gHyQXXQueJigRyNiSBG7MWv3xzrLJ8Sr25k7AdTyU0vvbW/VXJnK+lX63NiE7V7fPGhWxLF8c01L4XpVcsGU3sAphDG+0+kYz5e8FQw=" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIICszCCAZsCBgGND5sWKTANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJjb25mbGljdHZpc3VhbGl6ZXIwHhcNMjQwMTE2MDAwODU4WhcNMzQwMTE2MDAxMDM4WjAdMRswGQYDVQQDDBJjb25mbGljdHZpc3VhbGl6ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLiM8L2q7ikskXmLUZI9g9pK24aLSj2TkJ9e+tHKdltpkxlaDev631VaxK4a5Fda83RDoKHMKVXRInaNi8JXuqEcQT7U0Tt2CQvXCHx7nx2wd4rHeUU60JWDqJI2wvrkxYt6ttpbamezBfL+WTCq5fxmh/kMPbR8zpatF5VJvqaU7rRJnlzdaiJ2AACjgdp7JdL/QBKsIB0pDBIemHqhgs9IekOyEOyK1EG5LweTn4THVkLDXKb8duzxnk16pCiTUrNMLDAn8Q49+Re6JxCdgMfG8UC0OKbSR6kfzHDVPn5JXf9fsgJhNMjBAY2fRMHm6hlw7YFFLOxl6Qp2391PoLAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAILnjnYEP3d+eIIEYfh3T0S3I6Vmf2DezOJV36u1obFvuE80QbdKsUka+0FimcKnXcQroB1ZVO761Zy3W4IefqHZvCKMaVMIHt6yG3qK9kGb28xKPAqN1Zal9BAJk/XdVlmqtshm0UZYQ49cBqLhhP8mqfIR+mv3Ict8PnVFGkBWkihW+Nvx0M5gHIYlncAjgRCoWXBlR75ZgLuVTtMVwE80NlRfp/hkgkaj1WagC3xdZ5f5CLVcwTvvLqpcZ8FbB5iNI3oQLAXgpHgh2EpDYvB4DViGjJ9N/Z1GYCPETOK1A+hf4PGA3wF4fujOJ8VN2HRXUS2aoZ+OOxMfwqgvFYA=" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "909597b8-9f1a-4739-87de-2ffba22d1f9a", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpQIBAAKCAQEAk45iaNteDDROult+hGyb91tKRFIYkapxqQe0Liv7qZorAe7OQrqU+r9TdFer1sbTLAyPYvUoblanWBgdoyd+atL/MhTdifE6F3G0HGzqHO62lgUaFr9nuMMjJojWOOUzVuOVATD8N6S8knW2y27JUyzS+cnIUB1iXHmZAY3RwU7a82+j2J+xT5DfF0MYIq8R/R9Uztmgz6W3PLQbvQ4C/QRy9kt1yxmQGWZUmAlIWgKTP1Bo7ID2/zdRjMEzr61X1P1wvIKg8+0+MgUUd1KqM1e8AC6UbY5N6ASOtIggnICkrmB9pG4bNsM383el69AxHagIttB79dH0+CeOdlmi0QIDAQABAoIBAAGd17oqXRdpI0urYPF2dyb4mxGwjJBb+67MZM70sGclRz9YLG5SuPseSX3G0B0kRIABCzkcUnsS+/ZdHUYCUsI82Yrk66BtQiOrnTuKfe4fN2ThXW3OXwaJLMNpUF+DM1LKX4GJ1dmONnqsS3pjFlWQ8ibGbSljiQZWVrTLpvalMEKXjxvdf4i3gTB7zBAUr55qv7PSskzftoK2HNooWpR36DlLjMJHLrYToOuaHpREgUprvbLLzC5Y17hMBlLUevczHIg0/av4xYnS/QYEqhEHTHwjY4gtjQ4EdwdE7xvX0rYCgQtYypyTCyySVbu18rAsedOxKu4yycPHw+10OAECgYEAxOGUBd5Omcdi6/ehrYt7LukC9ukPDql1OeUmrVpblQD7t6I2A0CP15JlVfaJoZG3nfy40lJasI9ZWhfv1Uz9D9AFDlYlBF7SILrlI8c9+lJP0QC1KQyTrXL72tXk5Sb3dQEJvQJxMfgWUrPgJvvnIexEAl67+5RUdC7ly2P+vkECgYEAv90nT74zWzLzgnWQG3sgBw2P2Fh09LWEJA3hu02WWyalXXwY/l5Y4LmDIo9M9RWiHNz3GZ6blkBwIGjJTa39Xj1G6454Cg4sZ2oZ7ucZpgFYq7vGjQxysybD33aKI6ZoL7yX+yjSI/HIy7baavRNdoxAXrUj97F09YfR6STX4JECgYEAtLKtXu14ip97ZUO0eknIV3e5JtEk5roVQ3vUEcsavxlV7mbM2BNLcfmFVG6gR1AsjK5FG5RGdEI9KflKQCUXJoov+caK1wYIKc4fPMVDVxpw80yI+RH4AHvGOEWUfdVTzWmUfItfRRODuYgqNN2Cd5oXmW1Fb1PmZ3QbqJ8wUYECgYEAnXWJYrds/Ga3VBTZnMQSh9dIezw1V/N0LAa8f/Rv9gSkaDGFbZTOijeVeJJ0jRsg/WEW5g62D7x4iRCWTMsDCgluH7m/qDjzljeMavV8pjGqrN4hV/akV4Tz8XweaJ2UGcFEVZqtw1QV/6HkZSx0OltmJJOyngAkRbEew6E6DhECgYEAoq52lDO+OBEqqrZ2i/j2BhveN7SPT+8gyf6Zb6EcnV8QuqJmIie3LEwJnBBDkrE8EhjK1DbxCFCd+7ObtPM4YAN5DuKs0CSuvDBoza70/ek112S6+87gZ1boyCKPsx8kdPyge3QICOFUwzxhmFjtKr6uzJMvTn3QzUgf5DtLRmI=" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIICszCCAZsCBgGND5sW7zANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJjb25mbGljdHZpc3VhbGl6ZXIwHhcNMjQwMTE2MDAwODU4WhcNMzQwMTE2MDAxMDM4WjAdMRswGQYDVQQDDBJjb25mbGljdHZpc3VhbGl6ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTjmJo214MNE66W36EbJv3W0pEUhiRqnGpB7QuK/upmisB7s5CupT6v1N0V6vWxtMsDI9i9ShuVqdYGB2jJ35q0v8yFN2J8ToXcbQcbOoc7raWBRoWv2e4wyMmiNY45TNW45UBMPw3pLySdbbLbslTLNL5ychQHWJceZkBjdHBTtrzb6PYn7FPkN8XQxgirxH9H1TO2aDPpbc8tBu9DgL9BHL2S3XLGZAZZlSYCUhaApM/UGjsgPb/N1GMwTOvrVfU/XC8gqDz7T4yBRR3UqozV7wALpRtjk3oBI60iCCcgKSuYH2kbhs2wzfzd6Xr0DEdqAi20Hv10fT4J452WaLRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQU/kDws30SkbpE4GXTHzPPpChJV7zTCdc05mg/bAQ4HEYPgX0UhfFRJsGiK9XnhmookGyPSbsf8xA9lbiHT3BDF5lW+pZ7trGv3c05/S0hpTBDFme+THJPva8yGgDXWTHyLB7L8GJA4WMeQtZ7+xU6A6o3W7RGnDRv2zfdoegbHXAD36xV387RoMhwiWC8IpGNRI1Vop2oVSskfnjKGSOxB60COCo/pCjbhRAC2pA2pad0a1d6vuKZVes0qPaoIbrbOnZqswzOt+7gbmufZ6pNG7gB4dYIdbrkwu5ykjHKXicAgTGPsom68+ODYJiEA6CzPLKsmD3VplBTPu7Huu8=" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "752b2585-d94f-414a-9bf0-49b376ac20f6", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "093421fa-ea07-4a61-bd3e-1e7f49f7bf2c" + ], + "secret": [ + "g5uGwWmIkYk-wYldnQ28zpvqB6v6HPrqwQCfuPPZvsI5chYFIQQdLZaPTUwBBNMiiGbuq3h4LUy3Nw1MY9mrug" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "8066a791-240b-41d3-aa13-ff0330205937", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "b1167c6a-732c-485b-9291-8e74be24ee5f" + ], + "secret": [ + "7O87ebN9g4E0a3Y25qmGZg" + ], + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "39680601-4c36-4b2f-9b05-03b00dd41685", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "bc02e99d-6003-4fb4-8138-e907e8b1098b", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "853a7f29-5072-40d7-abb7-7930249daaea", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b252cf7c-88d0-4335-8fcd-c82d41517fb6", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8c53e7e4-3a6b-428d-b880-8e8adb4450cf", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d551aa72-5e75-493d-9fdd-1523c24f7c94", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "23717180-913e-4b49-956d-077577995725", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "709d706e-7c13-4f23-bd1a-65835f3061e6", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "7b44d874-d678-4783-ab60-36aa41420fdd", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "576b18f2-3d51-42de-9a52-a2a44a819e95", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "0fe21948-a1fc-4541-9589-4000e9649965", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "589d48fb-dca3-440e-9bfb-e8e82f9279fa", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "5774c21e-0fd9-45de-9cf1-5f0edf1472b5", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ed0603cb-09d5-44c1-bf45-9f7345c07e75", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "cd76af5a-1cba-4518-a63f-c90fd5fdb17c", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "a85d862a-a3a9-4152-a431-ff170e6a1b25", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "1a5f19e1-ca5d-4996-b6ae-81e4fc9b285d", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "c145e3d3-f999-4d7b-9d45-bb6cade70777", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e67bed21-c82e-4d2f-8b57-e88ba75662b6", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "56cc58e4-0037-4e81-8bb9-ed43512a2089", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "90df4fa5-f207-4b3d-986e-35f1ed55a6d6", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "5ca72dc1-7875-4a8c-a157-3fc26a9be3a7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "21.1.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } + }, + { + "id": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", + "realm": "cvmanager", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 1800, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "998ccb10-fd0a-4e8a-9e56-e1c7962bf20c", + "name": "default-roles-cvmanager", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", + "attributes": {} + }, + { + "id": "b0a79e2b-c806-492e-8505-00588ff3432a", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", + "attributes": {} + }, + { + "id": "2a0612f5-fc06-4ccc-afd0-ea7e2191f310", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "31ec7c7d-244c-41be-9d4c-a5593383298a", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "ee4eb6b8-d92c-4cbb-bf3c-b1debe5c3ced", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "83a91028-27ab-44fb-9af5-4f20263182b9", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "d0da36a6-8b14-4465-8dc8-28437cc84a2a", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "bd8514af-c5d1-4465-b456-15f6879411ee", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "cd560ff0-c8f5-414e-8642-716c9e2efa5e", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "7062c7d0-944e-4c18-8919-d0b0f7379a71", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "e6df53d4-fd66-4918-8e9c-b8e4446bfee3", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "3fce0be6-6b1e-46b4-8632-ff35f545c435", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "e37406f2-43f3-4790-a4f7-e614003a9b62", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "376a674a-5a2d-4f3b-8158-17eea74e5b46", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "83f52ff4-7f65-464d-a59d-76543f3095b4", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "0c38752f-df92-4f48-b0db-70aa831e88ce", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "b6b227af-e6c2-4a8d-b59c-022c2ab83943", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "cc2bb729-2724-44da-b62a-c6669b1c8714", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "567cd8ab-e068-49c1-b70b-fdce835a828d", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "b42adb18-571c-40eb-a9e6-62c68850bdab", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "9900411e-06cc-4a2b-8d5f-646d5b718dee", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + }, + { + "id": "15a49a79-49d4-4a0a-a4e2-a95faca496b1", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "create-client", + "view-realm", + "view-authorization", + "view-users", + "impersonation", + "view-events", + "manage-events", + "manage-clients", + "view-clients", + "view-identity-providers", + "query-groups", + "query-users", + "manage-users", + "manage-identity-providers", + "manage-authorization", + "query-realms", + "manage-realm", + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "attributes": {} + } + ], + "cvmanager-api": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "cvmanager-gui": [], + "broker": [ + { + "id": "db2c3f89-dd8f-4f7a-8076-774548973a13", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "a48352c8-caf6-4175-8f10-b889a6815067", + "attributes": {} + } + ], + "account": [ + { + "id": "9d4f040d-96d8-44ce-a276-bd9bea962d64", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "a8b8c7c4-6677-4ff4-a080-d957de0e954f", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "3c8a806d-363d-40d3-a845-db1af46642d3", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "3d7e73b4-90fa-4c74-bc0f-9dd2c3e619e4", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "2c52f8b6-72fc-4662-af7e-26fb225011d7", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "574a3cd6-b084-4568-a7ef-ba3bbca1fb1f", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "e8ff12eb-3830-40ef-8de5-183de3af4d45", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + }, + { + "id": "73651ba8-a717-4599-b291-273776db88db", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "998ccb10-fd0a-4e8a-9e56-e1c7962bf20c", + "name": "default-roles-cvmanager", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "d0dc22ea-63fe-42ee-81d0-beed32b15ea7" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppMicrosoftAuthenticatorName", + "totpAppGoogleName", + "totpAppFreeOTPName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "140a5f80-f0fe-4aa7-981b-a07e35dfda03", + "createdTimestamp": 1684172188006, + "username": "test", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "test", + "lastName": "user", + "email": "test@gmail.com", + "credentials": [ + { + "id": "6a037273-410c-49a1-9cab-f5a93654d53c", + "type": "password", + "userLabel": "My password", + "createdDate": 1684172206165, + "secretData": "{\"value\":\"Z2Wxmk8PNk6LYa0a02Yhg/2Gqudz1bGxWrKWVgPZwWI=\",\"salt\":\"ZgC/WbN8ujMIvtTy2RiKlA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-cvmanager" + ], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "0783caa9-c00b-4a6b-8d5b-f4cf1a9d43cf", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/cvmanager/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/cvmanager/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "48417936-3262-4cc6-a53d-69809be217ec", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/cvmanager/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/cvmanager/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "dfe4414e-e166-4c56-bacd-e7bde8d3bca0", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "14c1acec-de65-4f71-a62e-dfc20eb42a11", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a48352c8-caf6-4175-8f10-b889a6815067", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "02c60f24-1909-4d39-bab5-1db615e1b224", + "clientId": "cvmanager-api", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "${KEYCLOAK_API_CLIENT_SECRET_KEY}", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1684172946", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "*", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "openid", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8232316f-728b-4e89-90ca-c91907d7718d", + "clientId": "cvmanager-gui", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "http://localhost:3000/*", + "${WEBAPP_ORIGIN}/*" + ], + "webOrigins": [ + "http://localhost:3000", + "${WEBAPP_ORIGIN}" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "http://localhost:3000/*##${WEBAPP_ORIGIN}/*", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "tls.client.certificate.bound.access.tokens": "false", + "require.pushed.authorization.requests": "false", + "acr.loa.map": "{}", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "openid", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b2cdce72-e870-4473-99c2-5dba6ebee37e", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "71948848-7dc7-4c8f-9437-ffb5e27662d6", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/cvmanager/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/cvmanager/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2e6d6ea3-769a-4685-823c-242e072fc360", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "db9cb18f-c262-4603-8902-c97221ccec56", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "9a717837-98ff-4984-8666-52f42af56200", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "8a3ead48-2b70-4a64-8aaf-425a6087333b", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "03a2abbc-8ff0-47b0-98ac-5fb35a694929", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "6ca30e3d-0ea0-4980-889c-64e3becc0db0", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "494e6946-b98d-481e-85dd-5d218f5240f9", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "2431e4d9-1082-4e99-97e0-972d13743be7", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "01752d8f-0747-43c4-8f11-b63aece6998c", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "1d61e415-2ffd-436d-af25-95a3eff9aa76", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "6c0e9870-0e7b-4687-9ac8-a53395661a90", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "923ead18-f830-47bc-8254-7a2e920f7f58", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "06263302-3360-4f6f-8f3d-b798c3963458", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "fecc8a79-84de-4971-a4ce-ddc895213dbf", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "b5044e26-8005-461b-91e5-54dbf10670c6", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "10adab8d-9a79-4cdc-ae56-98f7c3979a4e", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "0ac06716-e460-4845-99f0-dd410d676a66", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "6d513f34-166b-49ac-b4ad-aed59c0defc9", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4fbe0ed4-2344-4391-b203-5aa0df0ebb9b", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "7cd87530-e597-46d1-9ea5-93a8ea9f1eb1", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "e714fced-1ef7-4e9b-8f9d-f73cb7fc7470", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "01008bb2-32f8-4cdf-8c9e-8dfa9819c185", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "50214ed5-b9e0-467b-9730-6456851c9316", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "6280fb5e-6ee7-404a-96e6-102cb2d8d25b", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "98703b2c-a82e-4aab-9fad-a40b13d2b515", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "f6bb1b01-b013-4834-a966-bf0c43b77f25", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "5792b55d-7445-4fd2-a3b0-ea3f67242132", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "0df3576b-15b2-4dab-bcac-e6fbacb0fc6f", + "name": "openid", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "68a27666-77cc-45e0-8eaa-a91e19bab274", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "299e35d1-e917-4ed0-8402-ed023da2e2b5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "bc9abcb8-bca1-4abd-8b9c-5f4ba66092e0", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "f1eb307e-c865-4381-859a-4d72ae40f5b5", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "eb9bb791-6ec1-4d6e-894c-d1e43bb0d4b7", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "ee0d0b99-5309-41c8-972e-e9089d7a79f6", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "d953dfd5-95e0-4bfe-9fc6-ae1fc9cf627a", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "7c85f8fd-cb30-44e2-ae73-dc5fb179a0a0", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "0d0e748e-68db-4851-9bbb-da90a7553c88", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "f9c88be3-af53-4d7a-9128-c8a15b8955b9", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "c209c131-f033-4d55-bebd-fe8945d89900", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "loginTheme": "cv-manager", + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [ + { + "alias": "google", + "internalId": "770daf54-0400-4081-ac81-df57abb079ad", + "providerId": "google", + "enabled": true, + "updateProfileFirstLoginMode": "on", + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "linkOnly": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "hideOnLoginPage": "false", + "offlineAccess": "false", + "acceptsPromptNoneForwardFromClient": "false", + "clientId": "${GOOGLE_CLIENT_ID}", + "disableUserInfo": "false", + "syncMode": "IMPORT", + "userIp": "false", + "clientSecret": "${GOOGLE_CLIENT_SECRET}" + } + } + ], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "a207ede2-6937-450d-866a-7ae9e291a205", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "779312d6-304b-4dc7-8274-2cbe8f0b88f0", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "b44329a4-463a-4cc9-962d-5cf8ae2a578e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "623e3c2a-b428-4361-93eb-9710fedb66c8", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "a1954774-53b4-4696-b357-a11449d69198", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "8d6e98a6-42b1-4391-b371-bbcb3af9e896", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "e89a9a42-ea8d-4871-aeb7-1dfa9aad8e9b", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "585f6d70-c93e-461a-ac17-a9e0ed5c5be9", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "03ada59e-4b8e-43a7-90af-181d60bcbae5", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAsnxqBGGqOEoyQ0Dq3iWSf+H+7C/xbh/ROakF2+oEfGemWVjsfrDXBUZhm9wWPQnW/Ow8qioG0H//1Nv1dryzfScLm49luGfxbieUbRNDb+OvcDTf6i/8r1T+mCEW/vUDon7PNTpOckrP8rRUcbvTSqX0HGo9weMrYlnBcIZxYzkflYNmBrbj46Fp7wrmG1TzoeEIVBviq/jYBRdnCoJE2mLbCnXupJErnQeuhRezfw6uCMASjRkZb+HGk1mGQrykE3/U3eeGXLlVA0G+Fe34SoXStTCuENYFb/zXMskjC2L3EgqXatuPNt+pNZuJyFz8jKEtbgkBKGMLeDkm5NNr1QIDAQABAoIBAE+ZEoKvt4Tw+edqTRQS93mWpORaITZ2dA1d5qIDhEqiwtn3wUhivxG4KJGknjpMaBdVl1xf77gOTV51VcvFLdqzjgaq9bc+i7oPZq8aNynwBW5p9i3vhqX+pqfbofDD/gH6wZfAT/nCiWh4qWwrUnho+Cuv6ajNEa0D0DPJkUmpEP5r0Eq1ZBpkV6I7brKtCPoMHIbofm7oqshRYxvzSVhlYJUSU0aMVw/7Dww6zT5vrX6QJWHLY9mBR3YxS/uMG/vYMKEu2hXbsdjtBRDl/u4aiAIk+OIulcmAlDnxC0dFHgw7rsDpK5QI2SJ1m3MF3lZ4AiOZs/o900US+431ryUCgYEA8LiMGI5kdKvEnVbYAqEzoo5EHjx1l9ha6F8Dzy/bKKY6noPePCC24j27Zv25OjBcq8MxFvo7grqDG+ZtWzes17pxmpe3ttaXtuf9RRf8wLw323USRrwJu52xD5STfombRKHwpKEy/NRewTnX6qK6QvEki/9YbEbgPEuA8sOvUHsCgYEAvdCeE7MdiBZEinV3+voBZuCJzlN7d2gSoGWv/ZZUNBMqE3Wiyr5dx85TiBkIbghO+t5EOZRQFL403nIpDPLVXLSsZxdAbtOtSGcc7zjUITpuz6malr4Tzlp+4d0+8Vjc+ooDTnIanCq0bx1vpBTitx3qGHZR/OCVkQ57Lsl2C+8CgYAhCvQQGtunODzQ7C7SjZYs5iJrlBkAMu6nnwNC2WrX9ZluUOOclVEFVTv4MzPNzP2rhiui385zb2630bWJI+dR5YHamqDZNDO3I7kcVuKXAj8YnMVZeE5NtqOrY9WrNPBfR2tk7cu18ODg3TPKPXQb5EYEAZT9p+z32dVlfX7/KQKBgQClei+1YNuH/lG2m34DsNx0AaBh3WmvyW0jpELvQpUZ6PMvj8hiE9/SBs/PwHMW6etgzVCRGflOfBu/Kasb/L+BWIlMPnsPoz5X9nzFGLfmV/iu1V9Nt1uw9DfVVHpBEYVkbdlAFD2ak6hFjlX7p7GWjl+8/7muSWRa11MQkNV2xQKBgC1Fq/L0+pcNSNNKd323mKjZaGClN7Pz4FzivT2sM9qYjZsyhoX/N5rVPRqK1w3Sovq9sUaPJGAHkluBwcULYT0/wa3EAsGfOkwrHNh1bBqO39bfvhuoXuDhmxg5AkyXyuvz130SoCc9WXw+iWIA1sEy2xcnrqrzPCWsk39zCCa9" + ], + "keyUse": [ + "SIG" + ], + "certificate": [ + "MIICoTCCAYkCBgGIIHh1fTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAljdm1hbmFnZXIwHhcNMjMwNTE1MTczMDQ1WhcNMzMwNTE1MTczMjI1WjAUMRIwEAYDVQQDDAljdm1hbmFnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyfGoEYao4SjJDQOreJZJ/4f7sL/FuH9E5qQXb6gR8Z6ZZWOx+sNcFRmGb3BY9Cdb87DyqKgbQf//U2/V2vLN9Jwubj2W4Z/FuJ5RtE0Nv469wNN/qL/yvVP6YIRb+9QOifs81Ok5ySs/ytFRxu9NKpfQcaj3B4ytiWcFwhnFjOR+Vg2YGtuPjoWnvCuYbVPOh4QhUG+Kr+NgFF2cKgkTaYtsKde6kkSudB66FF7N/Dq4IwBKNGRlv4caTWYZCvKQTf9Td54ZcuVUDQb4V7fhKhdK1MK4Q1gVv/NcyySMLYvcSCpdq248236k1m4nIXPyMoS1uCQEoYwt4OSbk02vVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD+vpYJuJ9ZG+nXcttflEs+FvUg4sFvQZVxLwHxusLKb095K9/UTbOi13bp/XyB/04SGZDZkb/RRjCXfLGbwAhlLvta83Y7D9egmYL4qT+4aQrctOdjX7qIjec8nklWSm/E5zAxF6qZ4pB8msK1X1d9vWJPtTY+y4uWWE/SO01acRPInazEtkwAVYaquZqZaP0d9/G7+a2DZ+0OMInaPBNF/Vw2PM1hItnJG8HzrqUMm5Y8zH76OpTBvZ6OxzhX4k1Z+mGnD3qv97vgfk65fpWVTjzymIyjPuVMOYXSuC4GWhQJQHlfoxATE3Rqs78Spa+y/4lkNWGmqgFcHVHoS6jw=" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "9ea96f48-10a5-42fb-bf64-53df044df527", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "e3118a62-6142-4a6d-8676-77b02f80bc94" + ], + "secret": [ + "5XzP4ih0trhxh8R-lDT1gA" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "6641f8c2-f1fc-4b3c-8fda-fdb55c7755ce", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "7bd6a6e5-2dc7-4274-a1a5-a5983aa82444" + ], + "secret": [ + "9rFpOZF24rMuJGtf2eBLOYsPg73OJYYWMZn-MJsLSD06G6BaBeVx8q8i_kG_07qx5Dd2W33-Yst36qzkXOFP_Q" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "9201d1ec-6ff4-4130-b1ee-088eb113aa85", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEA0MQIbGxBHMkPp/yI6dSj3/SWWsyQrGe+ftdg8CCUMrlFRAq34wamnbP5k0GozWqAY6paTPrPGjcuXQ0EaMwL5OKdFaFWGXwLLjrmTicCXIEyb8lo3Ty8/izPxVlHLs+iZtZm5uP9xfL7Nw6Ja9gXhQStHfZPnt7Q31Ht4GfN08FrKAoGTsy+EyLv4o/4LoSUwRyuyvNpva3fuwTUoZPP7Y3Mmo/Ijk3L1R7cXelxNkh5ApN6whzhcec3yPQrveBed39mZ0WXyslSvVHCDQ5uvGj5fk45tRCXsOVTz01EOmAbm2W6/OOs8Ixw2Ll3l33+5U+M8uL+neEKg1aqDUew7wIDAQABAoIBACS4Bh9D3yP3/Uf3tAEkxHoUpAluZ5fbW3cl3Mf/gvF1AsjX9cX5mn6sdB5BczZGIDTndqCJkLm0sPPu4TKpiQIGFckDKoiq97B27aEbXV/13XAqBca78yXlrdmxPULvhEoANfMwcKdLeIITjXopdOGRk/1sIE76M9TDrUpGF77BufUrQUcg7DCDBOVjGTt83RMQpXVxobEu/PIYwyhUH6tOgdVl9dvcCfaShQUNlW8kYYNlMYW0Giude9Aeu8pZAUM6UXTUtkyyFdarerhWFVSTT0x1u7EbqhdeqANmDbkpQ/sNCBCF4qwj2ff7ZN9M/oqf6wyoHzOV0RTaHJtpawECgYEA8h7beSfOZ0q/QVYymfGGSEnl99twbX+y0IrrfaudX0410kXGrxRrplxq9UfOfnVCl/LmOqr36Ne1lqDiGPRQWJfNp74NIMzs8z0pLLZJYfhd2yiUh4wtpMh+dV8Hrnm6lLvNhT9QR7bnm0OhESv3N0MgMHsch7AxhV0l21cu9YECgYEA3Luw3L1opSXRHgyeyAFiDD1YD3rTM0HaJKO/DjmmC10AlVmnYYZ0rvlvnrIE82d1kxG0Std691ILnvHHw9Vh1mLprGFCszwgJGicNy2Tr/DeXuaXmifVtUFxNaxYTeFcPCJPL2BjmQ9Naxhin/g4w5maK5LduGJTUR8aDjd2Pm8CgYBMGeTT/O4ES1s39xbqih6x5ABTWnbJBAU5RSDlnCZXyWZjVCkx6JI5dPztYYeG+eZXijJRKGHJnttln+XRACGs5vHuEm9f6uljPssNUbJZB87ATs34mNfT3mzZCWiJr5s0mp7rjc327Id5ptUeZ5pJlWCtvFRoVboK+A8pFQsegQKBgQDJHF0NEanJZkY8iaUVd2Uc37tfBzpsZiBZ57NIQ7AMhGTmrnO5gKbJUUyom2u1VVsjbysEUYWg1ujtnT60J7NngGGFBGygHzTt1z4Va/o2gFAqyQ/xjT/CUGjUTT17X8wIof3hnYHBT9bqr6IUPDWDyWxVLQ/EUhm1PJAhydh7EwKBgQDNgKsBjTrTpDtcewfYarAlyLnmsJ1Fd7t4ICpKl0zwOcBacN+IqHwKZ/CTnYbpuwbURm6B0uQe6ApJ49RRFIa9Zdi9hQ4MkGEvDmAYRxuzqYPm65ssYVwcPPVYNpo8TfY3ulCPhR0zKKuZS968SKUlVWxhlqovZslFZD01OAYRrQ==" + ], + "keyUse": [ + "ENC" + ], + "certificate": [ + "MIICoTCCAYkCBgGIIHh17TANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAljdm1hbmFnZXIwHhcNMjMwNTE1MTczMDQ1WhcNMzMwNTE1MTczMjI1WjAUMRIwEAYDVQQDDAljdm1hbmFnZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQxAhsbEEcyQ+n/Ijp1KPf9JZazJCsZ75+12DwIJQyuUVECrfjBqads/mTQajNaoBjqlpM+s8aNy5dDQRozAvk4p0VoVYZfAsuOuZOJwJcgTJvyWjdPLz+LM/FWUcuz6Jm1mbm4/3F8vs3Dolr2BeFBK0d9k+e3tDfUe3gZ83TwWsoCgZOzL4TIu/ij/guhJTBHK7K82m9rd+7BNShk8/tjcyaj8iOTcvVHtxd6XE2SHkCk3rCHOFx5zfI9Cu94F53f2ZnRZfKyVK9UcINDm68aPl+Tjm1EJew5VPPTUQ6YBubZbr846zwjHDYuXeXff7lT4zy4v6d4QqDVqoNR7DvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGGOT5zqUXa3vj683lB77y81VszT/raGg/GOMxOuL+9jDRnqKukY4keqT+O0q8xHn4SxQ2r0P+1dQ4WOUL91Nmi5lAC31O411qgbRvNF2WjYiqwjYr4FxJxiCruCx0fnDjl/eCfQeojXt0jmOwkRdSeZW3oyxwSv1g2p4jVe5ICczDLAmCgF+3SCfudSMzdf533s9FiB2rYeslePYr9+/ukWAN9eZH7Gz7c6qEEpYmfs9IgK0MbYwmgjNIPvSu1R3Rg5O9Zbi3pC0NcLy5hTiFB2M68fsM/ZRz4xsh2K5bLDZdiqbZCgTbrw10yoZZ8YhqKcIYVtyizzs9zAqqNqBLk=" + ], + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "4689148e-de94-4e88-868b-b372c1903794", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "1e3160c8-aa62-410b-9738-772a7105ec4a", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0a62d54c-461d-439e-98b3-e0c3cb80d91c", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ba8d3a35-c9be-4e16-b08e-3f4cfd24db8e", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c5254378-0be6-4bd7-8608-3a0ea47a92f0", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "cd365a4e-ec37-43d5-adf2-55d584f8ea2e", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "d3693eb3-71a6-43f6-92a3-d2b0d8bb0766", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ef949287-7d40-4f51-8624-f7482c0f49dd", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "012dc79a-df7d-4e16-8a3b-99bc50c3787d", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "6014612b-5c73-49a2-9644-b10a01dd325e", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "3b401240-483a-4bf8-a00c-61ee5b6fc526", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6ed0653b-d12a-4711-a069-ae2533e2f83f", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "4b1643a7-2721-4794-867d-da29ec85b43a", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "efd91669-c825-4d4a-bb54-05807c724e6e", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "3ce46d63-d520-4c2a-bc7d-4e004469a948", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "280afc5a-1bbc-484f-964b-097308132b4e", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "217c1bfb-125c-4ccc-969d-aebd864055f7", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "00627c7a-3e3d-4535-8c73-6167c8be0db9", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "7915f9e8-547b-4a89-b933-eeb23b2ede44", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "c18c9716-c71a-4bae-aff3-6c2e42d2dd43", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "a75d1438-49e8-46cf-872c-a455338010f5", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "f000330c-7003-4872-b439-9885ac5c34ab", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "21.1.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } + } +] \ No newline at end of file diff --git a/resources/kubernetes/README.md b/resources/kubernetes/README.md index ba96f15d..75beaa69 100644 --- a/resources/kubernetes/README.md +++ b/resources/kubernetes/README.md @@ -8,7 +8,7 @@ The webapp and API both utilize a K8s Ingress to handle external access to the a The YAML files use GCP specific specifications for various values such as "networking.gke.io/managed-certificates". These values will not work on AWS and Azure but there should be equivalent fields that these specifications can be updated to if needing to deploy in another cloud environment. -The environment variables must be set according to the README documentation for each application. The iss-health-check application only supports GCP. +The environment variables must be set according to the README documentation for each application. The iss-health-check application supports GCP or postgres for storing keys. The environment variables for the iss-health-check application must be set according to the README documentation for the iss-health-check application. ## Useful Links diff --git a/resources/kubernetes/cv-manager-api.yaml b/resources/kubernetes/cv-manager-api.yaml index 922666df..e0c99eb0 100644 --- a/resources/kubernetes/cv-manager-api.yaml +++ b/resources/kubernetes/cv-manager-api.yaml @@ -103,10 +103,6 @@ spec: # Fill out the ENV vars with your own values - name: CORS_DOMAIN value: '' - - name: GOOGLE_APPLICATION_CREDENTIALS - value: '' - - name: GOOGLE_CLIENT_ID - value: "" - name: KEYCLOAK_ENDPOINT value: "" - name: KEYCLOAK_REALM @@ -132,13 +128,13 @@ spec: secretKeyRef: name: some-postgres-secret-password key: some-postgres-secret-key - - name: COUNTS_DB_TYPE + - name: MONGO_DB_URI value: "" - - name: COUNTS_MSG_TYPES + - name: MONGO_DB_NAME value: "" - - name: COUNTS_DB_NAME + - name: COUNTS_MSG_TYPES value: "" - - name: BSM_DB_NAME + - name: GEO_DB_NAME value: "" - name: SSM_DB_NAME value: "" @@ -170,6 +166,8 @@ spec: value: "" - name: LOGGING_LEVEL value: "" + - name: MAX_GEO_QUERY_RECORDS + value: "" volumeMounts: - name: cv-manager-service-key mountPath: /home/secret diff --git a/resources/kubernetes/cv-manager-postgres.yaml b/resources/kubernetes/cv-manager-postgres.yaml index 255ce635..2a8904c4 100644 --- a/resources/kubernetes/cv-manager-postgres.yaml +++ b/resources/kubernetes/cv-manager-postgres.yaml @@ -227,6 +227,7 @@ data: snmp_credential_id integer NOT NULL DEFAULT nextval('snmp_credentials_snmp_credential_id_seq'::regclass), username character varying(128) COLLATE pg_catalog.default NOT NULL, password character varying(128) COLLATE pg_catalog.default NOT NULL, + encrypt_password character varying(128) COLLATE pg_catalog.default, nickname character varying(128) COLLATE pg_catalog.default NOT NULL, CONSTRAINT snmp_credentials_pkey PRIMARY KEY (snmp_credential_id), CONSTRAINT snmp_credentials_nickname UNIQUE (nickname) diff --git a/resources/kubernetes/firmware-manager.yaml b/resources/kubernetes/firmware-manager.yaml index 9cad8682..d7246238 100644 --- a/resources/kubernetes/firmware-manager.yaml +++ b/resources/kubernetes/firmware-manager.yaml @@ -58,6 +58,8 @@ spec: ports: - containerPort: 8080 env: + - name: ACTIVE_UPGRADE_LIMIT + value: 20 - name: GOOGLE_APPLICATION_CREDENTIALS value: "/home/secret/cv_credentials.json" - name: GCP_PROJECT @@ -82,6 +84,22 @@ spec: value: "" - name: LOGGING_LEVEL value: "INFO" + - name: FW_EMAIL_RECIPIENTS + value: "" + - name: SMTP_SERVER_IP + value: "" + - name: SMTP_EMAIL + value: "" + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: some-smtp-secret-username + key: some-smtp-secret-key + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: some-smtp-secret-password + key: some-smtp-secret-key volumeMounts: - name: cv-manager-service-key mountPath: /home/secret diff --git a/resources/kubernetes/iss-health-check.yaml b/resources/kubernetes/iss-health-check.yaml index 24b842c9..7cd48b63 100644 --- a/resources/kubernetes/iss-health-check.yaml +++ b/resources/kubernetes/iss-health-check.yaml @@ -1,4 +1,4 @@ -# This deployment is only usable in a GCP environment due to the GCP Secret Manager dependency +# If GCP is being used to store keys, this deployment will only be usable in a GCP environment due to the GCP Secret Manager dependency apiVersion: 'apps/v1' kind: 'Deployment' metadata: @@ -27,6 +27,8 @@ spec: ports: - containerPort: 8080 env: + - name: STORAGE_TYPE + value: GCP - name: GOOGLE_APPLICATION_CREDENTIALS value: '/home/secret/cv_credentials.json' - name: PROJECT_ID @@ -41,6 +43,8 @@ spec: value: '' - name: ISS_SCMS_VEHICLE_REST_ENDPOINT value: '' + - name: ISS_KEY_TABLE_NAME + value: '' - name: DB_USER value: '' - name: DB_PASS diff --git a/resources/kubernetes/rsu-ping-fetch.yaml b/resources/kubernetes/rsu-ping-fetch.yaml deleted file mode 100644 index 86d989ff..00000000 --- a/resources/kubernetes/rsu-ping-fetch.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: 'apps/v1' -kind: 'Deployment' -metadata: - name: 'rsu-ping-fetch' - labels: - app: 'rsu-ping-fetch' -spec: - replicas: 1 - selector: - matchLabels: - app: 'rsu-ping-fetch' - template: - metadata: - labels: - app: 'rsu-ping-fetch' - spec: - containers: - - name: 'rsu-ping-fetch' - imagePullPolicy: Always - image: 'rsu-ping-fetch-image' - resources: - requests: - memory: '1Gi' - cpu: '0.5' - ports: - - containerPort: 8080 - env: - - name: ZABBIX_ENDPOINT - value: '' - - name: ZABBIX_USER - value: '' - - name: ZABBIX_PASSWORD - value: '' - - name: DB_USER - value: '' - - name: DB_PASS - value: '' - - name: DB_NAME - value: '' - - name: DB_HOST - value: '' - - name: STALE_PERIOD - value: '24' - - name: LOGGING_LEVEL - value: 'INFO' - tty: true - stdin: true diff --git a/resources/kubernetes/rsu-status-checker.yaml b/resources/kubernetes/rsu-status-checker.yaml new file mode 100644 index 00000000..f18d0c46 --- /dev/null +++ b/resources/kubernetes/rsu-status-checker.yaml @@ -0,0 +1,59 @@ +apiVersion: 'apps/v1' +kind: 'Deployment' +metadata: + name: 'rsu-status-checker' + labels: + app: 'rsu-status-checker' +spec: + replicas: 1 + selector: + matchLabels: + app: 'rsu-status-checker' + template: + metadata: + labels: + app: 'rsu-status-checker' + spec: + containers: + - name: 'rsu-status-checker-image-sha256-1' + imagePullPolicy: Always + image: '{{ .Values.images.rsu_status_checker.repository }}:{{ .Values.images.rsu_status_checker.tag }}' + securityContext: + capabilities: + add: ['NET_RAW'] + resources: + requests: + memory: 500Mi + cpu: 0.5 + limits: + memory: 1Gi + cpu: 1 + ports: + - containerPort: 8080 + env: + - name: RSU_PING + value: + - name: ZABBIX + value: + - name: RSU_SNMP_FETCH + value: + - name: PG_DB_HOST + value: + - name: PG_DB_NAME + value: + - name: PG_DB_USER + value: + - name: PG_DB_PASS + value: + - name: ZABBIX_ENDPOINT + value: + - name: ZABBIX_USER + value: + - name: ZABBIX_PASSWORD + value: + - name: STALE_PERIOD + value: 24 + - name: LOGGING_LEVEL + value: INFO + tty: true + stdin: true diff --git a/resources/sql_scripts/CVManager_CreateTables.sql b/resources/sql_scripts/CVManager_CreateTables.sql index a432333f..da02cf1d 100644 --- a/resources/sql_scripts/CVManager_CreateTables.sql +++ b/resources/sql_scripts/CVManager_CreateTables.sql @@ -113,6 +113,7 @@ CREATE TABLE IF NOT EXISTS public.snmp_credentials snmp_credential_id integer NOT NULL DEFAULT nextval('snmp_credentials_snmp_credential_id_seq'::regclass), username character varying(128) COLLATE pg_catalog.default NOT NULL, password character varying(128) COLLATE pg_catalog.default NOT NULL, + encrypt_password character varying(128) COLLATE pg_catalog.default, nickname character varying(128) COLLATE pg_catalog.default NOT NULL, CONSTRAINT snmp_credentials_pkey PRIMARY KEY (snmp_credential_id), CONSTRAINT snmp_credentials_nickname UNIQUE (nickname) @@ -324,6 +325,21 @@ SELECT ro.rsu_id, org.name FROM public.rsu_organization AS ro JOIN public.organizations AS org ON ro.organization_id = org.organization_id; +-- Create iss keys table (id, iss_key, creation_date, expiration_date) +CREATE SEQUENCE public.iss_keys_iss_key_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 2147483647 + CACHE 1; + +CREATE TABLE IF NOT EXISTS public.iss_keys +( + iss_key_id integer NOT NULL DEFAULT nextval('iss_keys_iss_key_id_seq'::regclass), + common_name character varying(128) COLLATE pg_catalog.default NOT NULL, + token character varying(128) COLLATE pg_catalog.default NOT NULL +); + -- Create scms_health table CREATE SEQUENCE public.scms_health_scms_health_id_seq INCREMENT 1 @@ -346,4 +362,43 @@ CREATE TABLE IF NOT EXISTS public.scms_health ON DELETE NO ACTION ); +-- Create snmp_msgfwd_type table +CREATE SEQUENCE public.snmp_msgfwd_type_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 2147483647 + CACHE 1; + +CREATE TABLE IF NOT EXISTS public.snmp_msgfwd_type +( + snmp_msgfwd_type_id integer NOT NULL DEFAULT nextval('snmp_msgfwd_type_id_seq'::regclass), + name character varying(128) COLLATE pg_catalog.default NOT NULL, + CONSTRAINT snmp_msgfwd_type_pkey PRIMARY KEY (snmp_msgfwd_type_id), + CONSTRAINT snmp_msgfwd_type_name UNIQUE (name) +); + +-- Create snmp_msgfwd_config table +CREATE TABLE IF NOT EXISTS public.snmp_msgfwd_config +( + rsu_id integer NOT NULL, + msgfwd_type integer NOT NULL, + snmp_index integer NOT NULL, + message_type character varying(128) COLLATE pg_catalog.default NOT NULL, + dest_ipv4 inet NOT NULL, + dest_port integer NOT NULL, + start_datetime timestamp without time zone NOT NULL, + end_datetime timestamp without time zone NOT NULL, + active bit(1) NOT NULL, + CONSTRAINT snmp_msgfwd_config_pkey PRIMARY KEY (rsu_id, msgfwd_type, snmp_index), + CONSTRAINT fk_rsu_id FOREIGN KEY (rsu_id) + REFERENCES public.rsus (rsu_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_msgfwd_type FOREIGN KEY (msgfwd_type) + REFERENCES public.snmp_msgfwd_type (snmp_msgfwd_type_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + CREATE SCHEMA IF NOT EXISTS keycloak; \ No newline at end of file diff --git a/resources/sql_scripts/CVManager_SampleData.sql b/resources/sql_scripts/CVManager_SampleData.sql index f28700c9..3d8af39b 100644 --- a/resources/sql_scripts/CVManager_SampleData.sql +++ b/resources/sql_scripts/CVManager_SampleData.sql @@ -20,8 +20,8 @@ INSERT INTO public.rsu_credentials( VALUES ('username', 'password', 'cred1'); INSERT INTO public.snmp_credentials( - username, password, nickname) - VALUES ('username', 'password', 'snmp1'); + username, password, encrypt_password, nickname) + VALUES ('username', 'password', 'encryption-pw', 'snmp1'); INSERT INTO public.snmp_versions( version_code, nickname) @@ -32,8 +32,8 @@ INSERT INTO public.snmp_versions( INSERT INTO public.rsus( geography, milepost, ipv4_address, serial_number, iss_scms_id, primary_route, model, credential_id, snmp_credential_id, snmp_version_id, firmware_version, target_firmware_version) - VALUES (ST_GeomFromText('POINT(-105.014182 39.740422)'), 1, '10.0.0.1', 'E5672', 'E5672', 'I999', 1, 1, 1, 1, 1, 1), - (ST_GeomFromText('POINT(-104.980496 40.087737)'), 2, '10.0.0.2', 'E5321', 'E5321', 'I999', 1, 1, 1, 1, 2, 2); + VALUES (ST_GeomFromText('POINT(-105.014182 39.740422)'), 1, '10.0.0.180', 'E5672', 'E5672', 'I999', 1, 1, 1, 1, 1, 1), + (ST_GeomFromText('POINT(-104.967723 39.918758)'), 2, '10.0.0.78', 'E5321', 'E5321', 'I999', 1, 1, 1, 2, 2, 2); INSERT INTO public.organizations( name) @@ -56,13 +56,16 @@ INSERT INTO public.user_organization( user_id, organization_id, role_id) VALUES (1, 1, 1); -INSERT INTO public.snmp_versions( - version_code, nickname) - VALUES ('4.1', '4.1'); -INSERT INTO public.snmp_versions( - version_code, nickname) - VALUES ('12.18', '12.18'); +INSERT INTO public.snmp_msgfwd_type( + name) + VALUES ('rsuDsrcFwd'), ('rsuReceivedMsg'), ('rsuXmitMsgFwding'); -ALTER TABLE public.rsus - ADD snmp_version_id integer NOT NULL - DEFAULT (1); \ No newline at end of file +INSERT INTO public.snmp_msgfwd_config( + rsu_id, msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active) + VALUES (1, 1, 1, 'BSM', '10.0.0.80', 46800, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (1, 1, 2, 'BSM', '10.0.0.81', 46800, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (1, 1, 3, 'BSM', '10.0.0.82', 46800, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (2, 2, 1, 'BSM', '10.0.0.80', 46800, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (2, 2, 2, 'BSM', '10.0.0.81', 46800, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (2, 3, 1, 'MAP', '10.0.0.80', 44920, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'), + (2, 3, 2, 'SPAT', '10.0.0.80', 44910, '2024/04/01T00:00:00', '2034/04/01T00:00:00', '1'); diff --git a/resources/sql_scripts/update_scripts/snmp_credentials_update.sql b/resources/sql_scripts/update_scripts/snmp_credentials_update.sql new file mode 100644 index 00000000..40e9435d --- /dev/null +++ b/resources/sql_scripts/update_scripts/snmp_credentials_update.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.snmp_credentials + ADD COLUMN encrypt_password character varying(128) COLLATE pg_catalog.default; \ No newline at end of file diff --git a/resources/sql_scripts/update_scripts/snmp_msgfwd.sql b/resources/sql_scripts/update_scripts/snmp_msgfwd.sql new file mode 100644 index 00000000..7663faaf --- /dev/null +++ b/resources/sql_scripts/update_scripts/snmp_msgfwd.sql @@ -0,0 +1,46 @@ +-- Run this SQL update script if you already have a deployed CV Manager PostgreSQL database prior to the SNMP snmp_msgfwd_config table +-- This file will create the 'snmp_msgfwd_type' and 'snmp_msgfwd_config' tables and add NTCIP 1218 and RSU 4.1 table names to the types table + +-- Create snmp_msgfwd_type table +CREATE SEQUENCE public.snmp_msgfwd_type_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 2147483647 + CACHE 1; + +CREATE TABLE IF NOT EXISTS public.snmp_msgfwd_type +( + snmp_msgfwd_type_id integer NOT NULL DEFAULT nextval('snmp_msgfwd_type_id_seq'::regclass), + name character varying(128) COLLATE pg_catalog.default NOT NULL, + CONSTRAINT snmp_msgfwd_type_pkey PRIMARY KEY (snmp_msgfwd_type_id), + CONSTRAINT snmp_msgfwd_type_name UNIQUE (name) +); + +-- Create snmp_msgfwd_config table +CREATE TABLE IF NOT EXISTS public.snmp_msgfwd_config +( + rsu_id integer NOT NULL, + msgfwd_type integer NOT NULL, + snmp_index integer NOT NULL, + message_type character varying(128) COLLATE pg_catalog.default NOT NULL, + dest_ipv4 inet NOT NULL, + dest_port integer NOT NULL, + start_datetime timestamp without time zone NOT NULL, + end_datetime timestamp without time zone NOT NULL, + active bit(1) NOT NULL, + CONSTRAINT snmp_msgfwd_config_pkey PRIMARY KEY (rsu_id, msgfwd_type, snmp_index), + CONSTRAINT fk_rsu_id FOREIGN KEY (rsu_id) + REFERENCES public.rsus (rsu_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + CONSTRAINT fk_msgfwd_type FOREIGN KEY (msgfwd_type) + REFERENCES public.snmp_msgfwd_type (snmp_msgfwd_type_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); + +-- Adding RSU 4.1 and NTCIP 1218 message forwarding tables +INSERT INTO public.snmp_msgfwd_type( + name) + VALUES ('rsuDsrcFwd'), ('rsuReceivedMsg'), ('rsuXmitMsgFwding'); diff --git a/sample.env b/sample.env index 436a3c24..3cfb926c 100644 --- a/sample.env +++ b/sample.env @@ -4,6 +4,7 @@ DOCKER_HOST_IP= WEBAPP_HOST_IP=${DOCKER_HOST_IP} # Note if using WEBAPP_DOMAIN for the docker-compose-webapp-deployment.yml file you will need to include http:// or https:// WEBAPP_DOMAIN=cvmanager.local.com +WEBAPP_CM_DOMAIN=cimms.local.com KC_HOST_IP=${DOCKER_HOST_IP} # Firmware Manager connectivity in the format 'http://endpoint:port' @@ -17,7 +18,7 @@ FIRMWARE_MANAGER_ENDPOINT=http://${DOCKER_HOST_IP}:8089 CORS_DOMAIN = * # PostgreSQL Database connection information -# this value may need to folow with the webapp host if debugging the applications +# this value may need to folow with the webapp host if debugging the applications PG_DB_HOST=${DOCKER_HOST_IP}:5432 PG_DB_NAME=postgres PG_DB_USER=postgres @@ -35,34 +36,35 @@ KEYCLOAK_ADMIN_PASSWORD= KEYCLOAK_REALM=cvmanager KEYCLOAK_API_CLIENT_ID=cvmanager-api KEYCLOAK_API_CLIENT_SECRET_KEY= +KEYCLOAK_CM_API_CLIENT_SECRET_KEY= KEYCLOAK_LOGIN_THEME_NAME=sample_theme # Note if using KEYCLOAK_DOMAIN for the docker-compose-webapp-deployment.yml file you will need to include http:// or https:// KEYCLOAK_DOMAIN=cvmanager.auth.com + # GCP OAuth2.0 client ID for SSO authentication in keycloak - if not specified the google SSO will not be functional GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= -# Set to either "BIGQUERY" or "MONGODB" depending on message count location. -COUNTS_DB_TYPE="BIGQUERY" - # If "BIGQUERY", set the location of the GCP service account key attached as a volume GOOGLE_APPLICATION_CREDENTIALS='./resources/google/sample_gcp_service_account.json' # If "MONGODB", set the MongoDB variables MONGO_DB_URI= -MONGO_DB_NAME="ODE" +MONGO_DB_NAME="ode" # Set these variables if using either "MONGODB" or "BIGQUERY" -# COUNTS_DB_NAME: Used for V2X message counts # COUNTS_MSG_TYPES: Comma seperated list of message types. -COUNTS_DB_NAME= # COUNTS_MSG_TYPES must be set for the counts menu to correctly populate when building an image for deployment COUNTS_MSG_TYPES='BSM,SSM,SPAT,SRM,MAP' -BSM_DB_NAME= +VIEWER_MSG_TYPES='BSM' +GEO_DB_NAME='V2XGeoJson' SSM_DB_NAME= SRM_DB_NAME= +# Specifies the maximum number of V2x messages returned from the geo_query_geo_data_mongo method before filtering occurs +MAX_GEO_QUERY_RECORDS= + # WZDx API key and endpoint for pulling WZDx data into the CV Manager WZDX_API_KEY= WZDX_ENDPOINT= @@ -75,6 +77,13 @@ CSM_EMAILS_TO_SEND_TO= CSM_TARGET_SMTP_SERVER_ADDRESS= CSM_TARGET_SMTP_SERVER_PORT= +# Email configuration for sending firmware-manager failure emails +SMTP_EMAIL= +SMTP_USERNAME= +SMTP_PASSWORD= +FW_EMAIL_RECIPIENTS= +SMTP_SERVER_IP= + # Python timezone for the CV Manager (You can list pytz timezones with the command 'pytz.all_timezones') TIMEZONE="US/Mountain" @@ -86,12 +95,94 @@ MAPBOX_INIT_LATITUDE="39.7392" MAPBOX_INIT_LONGITUDE="-104.9903" MAPBOX_INIT_ZOOM="10" -# Firmware Manager -BLOB_STORAGE_PROVIDER=GCP +GCP_PROJECT_ID = '' + +# --------------------------------------------------------------------- + +# Count Metric Addon: +ENABLE_EMAILER = 'True' + +# If ENABLE_EMAILER is 'True', set the following environment variables +DEPLOYMENT_TITLE = 'JPO-ODE' + +# SMTP REQUIRED VARIABLES +SMTP_SERVER_IP = '' +SMTP_USERNAME = '' +SMTP_PASSWORD = '' +SMTP_EMAIL = '' +# Multiple emails can be delimited by a ',' +SMTP_EMAIL_RECIPIENTS = 'test1@gmail.com,test2@gmail.com' + +# If ENABLE_EMAILER is 'False', set the following environment variables + +COUNT_MESSAGE_TYPES = 'bsm' +ODE_KAFKA_BROKERS = {DOCKER_HOST_IP}:9092 + +# EITHER "MONGODB" or "BIGQUERY" +COUNT_DESTINATION_DB = 'MONGODB' + +# MONGODB REQUIRED VARIABLES +INPUT_COUNTS_MONGO_COLLECTION_NAME = '' +OUTPUT_COUNTS_MONGO_COLLECTION_NAME = '' + +# BIGQUERY REQUIRED VARIABLES +KAFKA_BIGQUERY_TABLENAME = '' +# --------------------------------------------------------------------- + +# Firmware Manager Addon: +BLOB_STORAGE_PROVIDER=DOCKER BLOB_STORAGE_BUCKET= GCP_PROJECT= +## Docker volume mount point for BLOB storage (if using Docker) +HOST_BLOB_STORAGE_DIRECTORY=./local_blob_storage +# --------------------------------------------------------------------- + +# Geo-spatial message query Addon: +GEO_INPUT_COLLECTIONS='OdeBsmJson,OdePsmJson' +# TTL duration in days: +GEO_TTL_DURATION=90 +# --------------------------------------------------------------------- + +# ISS Health Check Addon +ISS_API_KEY= +ISS_API_KEY_NAME= +ISS_PROJECT_ID= +ISS_SCMS_TOKEN_REST_ENDPOINT= +ISS_SCMS_VEHICLE_REST_ENDPOINT= +# --------------------------------------------------------------------- + +# RSU Status Addon: + +# Services that can be toggled on or off +# 'True' or 'False' are the only legal values + +# Toggles monitoring of RSU online status +RSU_PING=True + +# Fetches ping data from Zabbix - alternatively the service will ping the RSUs on its own +# Only used when RSU_PING is 'True' +ZABBIX=False + +# Fetches SNMP configuration data for all RSUs +RSU_SNMP_FETCH=True + +# Zabbix endpoint and API authentication +# Only used when ZABBIX is 'True' +ZABBIX_ENDPOINT= +ZABBIX_USER= +ZABBIX_PASSWORD= + +# Customize the period at which the purger will determine a ping log is too old and will be deleted +# Number of hours +STALE_PERIOD=24 +# --------------------------------------------------------------------- # Levels are "DEBUG", "INFO", "WARNING", and "ERROR" API_LOGGING_LEVEL="INFO" +FIRMWARE_MANAGER_LOGGING_LEVEL="INFO" +GEO_LOGGING_LEVEL="INFO" +ISS_LOGGING_LEVEL="INFO" +RSU_STATUS_LOGGING_LEVEL="INFO" +COUNTS_LOGGING_LEVEL="INFO" # Supported log levels are "ALL", "DEBUG", "ERROR", "FATAL", "INFO", "OFF", "TRACE" and "WARN" KC_LOGGING_LEVEL="INFO" \ No newline at end of file diff --git a/services/Dockerfile.api b/services/Dockerfile.api index 47d1be61..fd072567 100644 --- a/services/Dockerfile.api +++ b/services/Dockerfile.api @@ -1,10 +1,10 @@ -FROM python:3.12.0-slim +FROM python:3.12.2-slim # Prepare the SNMP functionality for the image RUN apt-get update RUN apt-get install -y snmpd RUN apt-get install -y snmp -ADD api/resources/mibs/* /usr/share/snmp/mibs/ +ADD resources/mibs/* /usr/share/snmp/mibs/ # Allow statements and log messages to immediately appear in the Knative logs ENV PYTHONUNBUFFERED True diff --git a/services/Dockerfile.bsm_query b/services/Dockerfile.bsm_query deleted file mode 100644 index ec320316..00000000 --- a/services/Dockerfile.bsm_query +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.12.0-alpine3.18 - -WORKDIR /home - -ADD addons/images/bsm_query/requirements.txt . -ADD addons/images/bsm_query/*.py . - -RUN pip3 install -r requirements.txt - -CMD ["/home/bsm_query.py"] -ENTRYPOINT ["python3"] \ No newline at end of file diff --git a/services/Dockerfile.count_metric b/services/Dockerfile.count_metric index 1e09bad1..ab3f9501 100644 --- a/services/Dockerfile.count_metric +++ b/services/Dockerfile.count_metric @@ -1,12 +1,16 @@ -FROM python:3.12-slim +FROM python:3.12.2-alpine3.18 WORKDIR /home +ADD addons/images/count_metric/crontab . ADD addons/images/count_metric/requirements.txt . ADD addons/images/count_metric/*.py . ADD common/*.py ./common/ RUN pip3 install -r requirements.txt -CMD ["/home/driver.py"] -ENTRYPOINT ["python3"] \ No newline at end of file +# fix the line endings from windows +RUN dos2unix /home/crontab +RUN crontab /home/crontab + +CMD ["crond", "-f"] diff --git a/services/Dockerfile.firmware_manager b/services/Dockerfile.firmware_manager index 558642e1..28172fb9 100644 --- a/services/Dockerfile.firmware_manager +++ b/services/Dockerfile.firmware_manager @@ -1,4 +1,4 @@ -FROM python:3.12-slim +FROM python:3.12.2-slim WORKDIR /home diff --git a/services/Dockerfile.geo_msg_query b/services/Dockerfile.geo_msg_query new file mode 100644 index 00000000..8f6c34c4 --- /dev/null +++ b/services/Dockerfile.geo_msg_query @@ -0,0 +1,11 @@ +FROM python:3.12.2-alpine3.18 + +WORKDIR /home + +ADD addons/images/geo_msg_query/requirements.txt . +ADD addons/images/geo_msg_query/*.py . + +RUN pip3 install -r requirements.txt + +CMD ["/home/geo_msg_query.py"] +ENTRYPOINT ["python3"] \ No newline at end of file diff --git a/services/Dockerfile.iss_health_check b/services/Dockerfile.iss_health_check index 97854c1f..f15d42e4 100644 --- a/services/Dockerfile.iss_health_check +++ b/services/Dockerfile.iss_health_check @@ -1,4 +1,4 @@ -FROM python:3.12.0-alpine3.18 +FROM python:3.12.2-alpine3.18 WORKDIR /home diff --git a/services/Dockerfile.rsu_ping_fetch b/services/Dockerfile.rsu_ping_fetch deleted file mode 100644 index bdf569f4..00000000 --- a/services/Dockerfile.rsu_ping_fetch +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.12.0-alpine3.18 - -WORKDIR /home - -ADD addons/images/rsu_ping/crontab.rsu_ping_fetch ./crontab -ADD addons/images/rsu_ping/requirements.txt . -ADD addons/images/rsu_ping/rsu_ping_fetch.py . -ADD addons/images/rsu_ping/purger.py . -ADD common/*.py ./common/ - -RUN pip3 install -r requirements.txt -# fix the line endings from windows -RUN dos2unix /home/crontab - -RUN crontab /home/crontab - -CMD ["crond", "-f"] \ No newline at end of file diff --git a/services/Dockerfile.rsu_pinger b/services/Dockerfile.rsu_pinger deleted file mode 100644 index a8386e23..00000000 --- a/services/Dockerfile.rsu_pinger +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.12.0-alpine3.18 - -WORKDIR /home - -ADD addons/images/rsu_ping/crontab.rsu_pinger ./crontab -ADD addons/images/rsu_ping/requirements.txt . -ADD addons/images/rsu_ping/rsu_pinger.py . -ADD addons/images/rsu_ping/purger.py . -ADD common/*.py ./common/ - -RUN pip3 install -r requirements.txt -# fix the line endings from windows -RUN dos2unix /home/crontab - -RUN crontab /home/crontab - -CMD ["crond", "-f"] \ No newline at end of file diff --git a/services/Dockerfile.rsu_status_check b/services/Dockerfile.rsu_status_check new file mode 100644 index 00000000..2d49b606 --- /dev/null +++ b/services/Dockerfile.rsu_status_check @@ -0,0 +1,22 @@ +FROM python:3.12.2-alpine3.18 + +# Prepare the SNMP functionality for the image +RUN apk update +RUN apk add net-snmp +RUN apk add net-snmp-tools +ADD resources/mibs/* /usr/share/snmp/mibs/ + +WORKDIR /home + +ADD addons/images/rsu_status_check/crontab . +ADD addons/images/rsu_status_check/requirements.txt . +ADD addons/images/rsu_status_check/*.py . +ADD common/*.py ./common/ + +RUN pip3 install -r requirements.txt + +# fix the line endings from windows +RUN dos2unix /home/crontab +RUN crontab /home/crontab + +CMD ["crond", "-f"] diff --git a/services/README.md b/services/README.md index cea07ae2..07253984 100644 --- a/services/README.md +++ b/services/README.md @@ -2,6 +2,10 @@ The CV Manager has multiple backend services that are required to allow the CV Manager to operate at full capacity. +## Python Version + +These services are implemented using Python 3.12.2. It is recommended to use this version when developing, testing, and deploying the services. + ## CV Manager API The CV Manager API is the backend service for the CV Manager webapp. This API is required to be run in an accessible location for the web application to function. The API is a Python Flask REST service. @@ -12,11 +16,11 @@ To learn more of what the CV Manager API offers, refer to its [README](api/READM The CV Manager add-ons are services that are very useful in allowing a user to collect and create all of the required data to be inserted into the CV Manager PostgreSQL database to allow the CV Manager to function. None of these services are required to be run. Alternative data sources for the following services can be used. However, all of these services are Kubernetes ready and are easy to integrate. -### bsm_query +### geo_msg_query -The bsm_query service allows for BSM data to be geospatially queryable in a MongoDB collection. +The geo_msg_query service allows for V2X data to be geospatially queryable in a MongoDB collection. -Read more about the deployment process in the [bsm_query directory](addons/images/bsm_query/README.md). +Read more about the deployment process in the [geo_msg_query directory](addons/images/geo_msg_query/README.md). ### count_metric diff --git a/services/addons/images/bsm_query/README.md b/services/addons/images/bsm_query/README.md deleted file mode 100644 index a04af299..00000000 --- a/services/addons/images/bsm_query/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# BSM Query Utility - -Service that creates a geospatially queryable MongoDB collection for use with the CV manager. diff --git a/services/addons/images/bsm_query/bsm_query.py b/services/addons/images/bsm_query/bsm_query.py deleted file mode 100644 index c4143e3c..00000000 --- a/services/addons/images/bsm_query/bsm_query.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -from concurrent.futures import ThreadPoolExecutor -import logging -from pymongo import MongoClient -from datetime import datetime - - -def set_mongo_client(MONGO_DB_URI, MONGO_DB, MONGO_BSM_INPUT_COLLECTION): - client = MongoClient(MONGO_DB_URI) - db = client[MONGO_DB] - collection = db[MONGO_BSM_INPUT_COLLECTION] - return db, collection - - -def create_message(original_message): - new_message = { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - original_message["payload"]["data"]["coreData"]["position"][ - "longitude" - ], - original_message["payload"]["data"]["coreData"]["position"]["latitude"], - ], - }, - "properties": { - "id": original_message["metadata"]["originIp"], - "timestamp": datetime.strptime( - original_message["metadata"]["odeReceivedAt"], "%Y-%m-%dT%H:%M:%S.%fZ" - ), - }, - } - return new_message - - -def process_message(message, db, collection): - new_message = create_message(message) - db[collection].insert_one(new_message) - - -def run(): - MONGO_DB_URI = os.getenv("MONGO_DB_URI") - MONGO_DB = os.getenv("MONGO_DB_NAME") - MONGO_BSM_INPUT_COLLECTION = os.getenv("MONGO_BSM_INPUT_COLLECTION") - MONGO_GEO_OUTPUT_COLLECTION = os.getenv("MONGO_GEO_OUTPUT_COLLECTION") - - if ( - MONGO_DB_URI is None - or MONGO_BSM_INPUT_COLLECTION is None - or MONGO_DB is None - or MONGO_GEO_OUTPUT_COLLECTION is None - ): - logging.error("Environment variables are not set! Exiting.") - exit("Environment variables are not set! Exiting.") - - log_level = ( - "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] - ) - logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) - - executor = ThreadPoolExecutor(max_workers=5) - - db, collection = set_mongo_client( - MONGO_DB_URI, MONGO_DB, MONGO_BSM_INPUT_COLLECTION - ) - - count = 0 - with collection.watch() as stream: - for change in stream: - count += 1 - executor.submit( - process_message, change["fullDocument"], db, MONGO_GEO_OUTPUT_COLLECTION - ) - logging.info(count) - - -if __name__ == "__main__": - run() diff --git a/services/addons/images/bsm_query/docker-compose.yml b/services/addons/images/bsm_query/docker-compose.yml deleted file mode 100644 index ebd464dd..00000000 --- a/services/addons/images/bsm_query/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: '3' -services: - jpo_kafka_counter: - build: . - image: jpo_bsm_query:latest - restart: always - env_file: - - .env - logging: - options: - max-size: '10m' - max-file: '5' diff --git a/services/addons/images/bsm_query/sample.env b/services/addons/images/bsm_query/sample.env deleted file mode 100644 index ae432b24..00000000 --- a/services/addons/images/bsm_query/sample.env +++ /dev/null @@ -1,9 +0,0 @@ -# Mongo connection variables -MONGO_DB_URI = 'mongodb://:27017/' -MONGO_DB_NAME = '' - -# Name of the BSM Database for incoming BSM messages -MONGO_BSM_INPUT_COLLECTION = '' -# Name of output geospatial MongoDB collection -MONGO_GEO_OUTPUT_COLLECTION = '' -LOGGING_LEVEL = "INFO" \ No newline at end of file diff --git a/services/addons/images/count_metric/README.md b/services/addons/images/count_metric/README.md index 24140c4d..5e2375f7 100644 --- a/services/addons/images/count_metric/README.md +++ b/services/addons/images/count_metric/README.md @@ -1,39 +1,45 @@ -# CDOT GCP Count Metric Generator +# Count Metric Service -This directory contains the script for monitoring a Pub/Sub topic for incoming messages and counting them in regard to the IP/RSU they originated from. (For an example message format, see the last section of the README) Each record is then stored as a custom metric on GCP Monitoring. +## Daily Emailer (MongoDB) -To run the script, the following environment variables must be set: +The count_metric service provides a means of querying and processing the jpo-ode messages stored in mongoDB from your jpo-ode deployment environment. These processed messages are quantified and compiled into a message count summary email that includes a breakdown of all messages received for the past 24 hours. These counts are grouped by RSU that forwarded the data. Both "in" and "out" counts are collected to be able to determine if there has been any data loss during the processing within the jpo-ode. Any deviance greater than 5% will have its 'out' counts marked red in the generated email. Anything below 5% will be marked green. -LOGGING_LEVEL: The logging level of the deployment. Options are: 'critical', 'error', 'warning', 'info' and 'debug'. If not specified, will default to 'info'. Refer to Python's documentation for more info: [Python logging](https://docs.python.org/3/howto/logging.html). +It is important to note that the count_metric service assumes Map and TIM messages are deduplicated on the 'out' counts. It will normalize the deviance expectation to 1 unique Map or TIM per hour from a RSU. + +Specifically includes the following message types: ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] -MESSAGE_TYPES: Message types to generate counts for separated by commas, ex: 'bsm,tim,map'. +To run this service, the following environment variables must be set: + +LOGGING_LEVEL: The logging level of the deployment. Options are: 'critical', 'error', 'warning', 'info' and 'debug'. If not specified, will default to 'info'. Refer to Python's documentation for more info: [Python logging](https://docs.python.org/3/howto/logging.html). -PROJECT_ID: The name of the GCP project the Pub/Sub topics are located in. Must be the same for both the topic and the subscription in this iteration. +ENABLE_EMAILER: Set to 'True' to run the daily emailer or 'False' to use the now deprecated Kafka message counter. It is recommended to switch to mongoDB if you are still using the message counter in any environments. -ODE_KAFKA_BROKERS: The connection information for the kafka brokers that have the needed kafka streams. +DEPLOYMENT_TITLE: The name of the environment that the jpo-ode messages are relevant to. This can be 'DEV', 'PROD', or anything suitable to your jpo-ode deployment. -DB_HOST: The connection information for the Postgres database. +PG_DB_HOST: The connection information for the Postgres database. PG_DB_USER: Postgres database username. PG_DB_PASS: Postgres database password, surround in single quotes if this has any special characters. -DB_NAME: Postgres database name. - -DESTINATION_DB: Set this to either "MONGODB" or "BIGQUERY" depending on the desired output database. +PG_DB_NAME: Postgres database name. MONGO_DB_URI: Connection string uri for the MongoDB database, please refer to the following [documentation](https://www.mongodb.com/docs/manual/reference/connection-string/). MONGO_DB_NAME: MongoDB database name. -INPUT_COUNTS_MONGO_COLLECTION_NAME: MongoDB collection for ODD input message counts. +SMTP_SERVER_IP: The IP or domain of the SMTP server your organization uses. DOTs often have a self hosted SMTP server for security reasons. + +SMTP_USERNAME: The username for the SMTP server account. + +SMTP_PASSWORD: The password for the SMTP server account. -OUTPUT_COUNTS_MONGO_COLLECTION_NAME: MongoDB collection for ODD output message counts. +SMTP_EMAIL: The origin email that the count_metric will send the email from. This is usually associated with the SMTP server authentication. -KAFKA_BIGQUERY_TABLENAME: This is the name of the BigQuery counts table that will be written to for daily kafka counts. +SMTP_EMAIL_RECIPIENTS: Recipient emails, delimited by ','. -## Expected Message Content +## Daily Counter (MongoDB) -### Kafka Out Message Example (BSM) +The daily counter is a feature that aggregates JPO-ODE mongoDB message type counts for BSM, PSM, TIM, Map, SPaT, SRM and SSM and inserts them into a new collection in mongoDB. This new collection is named "CVCounts". This new collection is useful for the CV Manager to query the message counts in a performant manner. This script runs on a cron every 24 hours. -`{"metadata":{"originIp":"172.16.28.41","bsmSource":"RV","logFileName":"","recordType":"bsmTx","securityResultCode":"success","receivedMessageDetails":{"locationData":{"latitude":"","longitude":"","elevation":"","speed":"","heading":""},"rxSource":"RV"},"payloadType":"us.dot.its.jpo.ode.model.OdeBsmPayload","serialId":{"streamId":"3e15a15f-378a-4d41-bef9-a8605059cb3f","bundleSize":1,"bundleId":0,"recordId":0,"serialNumber":0},"odeReceivedAt":"2021-07-21T18:03:16.462Z","schemaVersion":6,"maxDurationTime":0,"odePacketID":"","odeTimStartDateTime":"","recordGeneratedAt":"","sanitized":false},"payload":{"dataType":"us.dot.its.jpo.ode.plugin.j2735.J2735Bsm","data":{"coreData":{"msgCnt":122,"id":"23010C4E","secMark":34600,"position":{"latitude":39.8086235,"longitude":-104.7807546,"elevation":1613.5},"accelSet":{"accelLat":0.00,"accelLong":0.00,"accelVert":0.00,"accelYaw":0.00},"accuracy":{"semiMajor":2.00,"semiMinor":2.00,"orientation":44.9951935489},"transmission":"NEUTRAL","speed":0.00,"heading":0.0000,"brakes":{"wheelBrakes":{"leftFront":false,"rightFront":false,"unavailable":true,"leftRear":false,"rightRear":false},"traction":"unavailable","abs":"unavailable","scs":"unavailable","brakeBoost":"unavailable","auxBrakes":"unavailable"},"size":{"width":200,"length":500}},"partII":[{"id":"VehicleSafetyExtensions","value":{"pathHistory":{"crumbData":[{"elevationOffset":3.1,"heading":0.0,"latOffset":0.0000119,"lonOffset":-0.0000085,"timeOffset":0.01}]},"pathPrediction":{"confidence":0.0,"radiusOfCurve":0.0}}},{"id":"SupplementalVehicleExtensions","value":{}}]}}}` +It is not recommended to change the frequency of this counter to allow the CV Manager's API to properly query for counts. If the daily emailer has been configured properly, this script will automatically run and maintain itself. diff --git a/services/addons/images/count_metric/crontab b/services/addons/images/count_metric/crontab new file mode 100644 index 00000000..15271055 --- /dev/null +++ b/services/addons/images/count_metric/crontab @@ -0,0 +1,3 @@ +PYTHONPATH=/home +0 7 * * * /usr/local/bin/python3 /home/daily_emailer.py +0 0 * * * /usr/local/bin/python3 /home/mongo_counter.py diff --git a/services/addons/images/count_metric/daily_emailer.py b/services/addons/images/count_metric/daily_emailer.py new file mode 100644 index 00000000..5f3dcb8e --- /dev/null +++ b/services/addons/images/count_metric/daily_emailer.py @@ -0,0 +1,174 @@ +import os +import logging +import gen_email +from common.emailSender import EmailSender +import common.pgquery as pgquery +from datetime import datetime, timedelta +from pymongo import MongoClient + +message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +# Modify the rsu_dict with the specified date range's mongoDB "in" counts for each message type +# The rsu_dict is modified in place +def query_mongo_in_counts(rsu_dict, start_dt, end_dt, mongo_db): + for type in message_types: + collection = mongo_db[f"OdeRawEncoded{type.upper()}Json"] + # Perform mongoDB aggregate query + agg_result = collection.aggregate( + [ + { + "$match": { + "recordGeneratedAt": { + "$gte": start_dt, + "$lt": end_dt, + } + } + }, + { + "$group": { + "_id": "$metadata.originIp", + "count": {"$sum": 1}, + } + }, + ] + ) + for record in agg_result: + if not record["_id"]: + continue + rsu_ip = record["_id"] + count = record["count"] + + logging.debug(f"{type.title()} In count received for {rsu_ip}: {count}") + + # If a RSU that is not in PostgreSQL has counts recorded, add it to the rsu_dict and populate zeroes + if rsu_ip not in rsu_dict: + rsu_dict[rsu_ip] = { + "primary_route": "Unknown", + "counts": {}, + } + for t in message_types: + rsu_dict[rsu_ip]["counts"][t] = {"in": 0, "out": 0} + + rsu_dict[rsu_ip]["counts"][type]["in"] = count + + +# Modify the rsu_dict with the specified date range's mongoDB "out" counts for each message type +# The rsu_dict is modified in place +def query_mongo_out_counts(rsu_dict, start_dt, end_dt, mongo_db): + for type in message_types: + collection = mongo_db[f"Ode{type.title()}Json"] + # Perform mongoDB aggregate query + agg_result = collection.aggregate( + [ + { + "$match": { + "recordGeneratedAt": { + "$gte": start_dt, + "$lt": end_dt, + } + } + }, + { + "$group": { + "_id": "$metadata.originIp", + "count": {"$sum": 1}, + } + }, + ] + ) + for record in agg_result: + if not record["_id"]: + continue + rsu_ip = record["_id"] + count = record["count"] + + logging.debug(f"{type.title()} Out count received for {rsu_ip}: {count}") + + # If a RSU that is not in PostgreSQL has counts recorded, add it to the rsu_dict and populate zeroes + if rsu_ip not in rsu_dict: + rsu_dict[rsu_ip] = { + "primary_route": "Unknown", + "counts": {}, + } + for t in message_types: + rsu_dict[rsu_ip]["counts"][t] = {"in": 0, "out": 0} + + rsu_dict[rsu_ip]["counts"][type]["out"] = count + + +def prepare_rsu_dict(): + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT ipv4_address, primary_route " + "FROM public.rsus " + "ORDER BY primary_route ASC, milepost ASC" + ") as row" + ) + + # Query PostgreSQL for the list of SNMP message forwarding configurations tracked in PostgreSQL + data = pgquery.query_db(query) + + rsu_dict = {} + for row in data: + row = dict(row[0]) + rsu_dict[row["ipv4_address"]] = { + "primary_route": row["primary_route"], + "counts": {}, + } + for type in message_types: + rsu_dict[row["ipv4_address"]]["counts"][type] = {"in": 0, "out": 0} + logging.debug(f"Created RSU dictionary: {rsu_dict}") + + return rsu_dict + + +def email_daily_counts(email_body): + logging.info("Attempting to send the count emails...") + try: + email_addresses = os.environ["SMTP_EMAIL_RECIPIENTS"].split(",") + + for email_address in email_addresses: + emailSender = EmailSender( + os.environ["SMTP_SERVER_IP"], + 587, + ) + emailSender.send( + sender=os.environ["SMTP_EMAIL"], + recipient=email_address, + subject=f"{str(os.environ['DEPLOYMENT_TITLE']).upper()} Counts", + message=email_body, + replyEmail="", + username=os.environ["SMTP_USERNAME"], + password=os.environ["SMTP_PASSWORD"], + pretty=True, + ) + except Exception as e: + logging.error(e) + + +def run_daily_emailer(): + rsu_dict = prepare_rsu_dict() + + # Grab today's date and yesterday's date for a 24 hour range + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + client = MongoClient(os.getenv("MONGO_DB_URI")) + mongo_db = client[os.getenv("MONGO_DB_NAME")] + # Populate rsu_dict with counts from mongoDB + query_mongo_in_counts(rsu_dict, start_dt, end_dt, mongo_db) + query_mongo_out_counts(rsu_dict, start_dt, end_dt, mongo_db) + + # Generate the email content with the populated rsu_dict + email_body = gen_email.generate_email_body( + rsu_dict, start_dt, end_dt, message_types + ) + email_daily_counts(email_body) + + +if __name__ == "__main__": + run_daily_emailer() diff --git a/services/addons/images/count_metric/docker-compose.yml b/services/addons/images/count_metric/docker-compose.yml deleted file mode 100644 index 85109fe4..00000000 --- a/services/addons/images/count_metric/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: '3' -services: - jpo_kafka_counter: - build: . - image: jpo_kafka_counter:latest - restart: always - env_file: - - .env - logging: - options: - max-size: '10m' - max-file: '5' diff --git a/services/addons/images/count_metric/driver.py b/services/addons/images/count_metric/driver.py deleted file mode 100644 index 56974b0c..00000000 --- a/services/addons/images/count_metric/driver.py +++ /dev/null @@ -1,102 +0,0 @@ -import os -import copy -import threading -import logging -import common.pgquery as pgquery - -from kafka_counter import KafkaMessageCounter - -# Set based on project and subscription, set these outside of the script if deployed - -thread_pool = [] -rsu_location_dict = {} -rsu_count_dict = {} - - -# Query for RSU data from CV Manager PostgreSQL database -def get_rsu_list(): - result = [] - - # Execute the query and fetch all results - query = "SELECT to_jsonb(row) FROM (SELECT ipv4_address, primary_route FROM public.rsus ORDER BY ipv4_address) as row" - data = pgquery.query_db(query) - - logging.debug("Parsing results...") - for row in data: - row = dict(row[0]) - result.append(row) - - return result - - -# Create template dictionaries for RSU roads and counts using HTTP JSON data -def populateRsuDict(): - rsu_list = get_rsu_list() - for rsu in rsu_list: - rsu_ip = rsu["ipv4_address"] - p_route = rsu["primary_route"] - - rsu_location_dict[rsu_ip] = p_route - # Add IP to dict if the road exists in the dict already - if p_route in rsu_count_dict: - rsu_count_dict[p_route][rsu_ip] = 0 - else: - rsu_count_dict[p_route] = {rsu_ip: 0} - - rsu_count_dict["Unknown"] = {} - - -def run(): - # Pull list of message types to run counts for from environment variable - messageTypesString = os.getenv("MESSAGE_TYPES", "") - if messageTypesString == "": - logging.error("MESSAGE_TYPES environment variable not set! Exiting.") - exit("MESSAGE_TYPES environment variable not set! Exiting.") - message_types = [ - msgtype.strip().lower() for msgtype in messageTypesString.split(",") - ] - - # Configure logging based on ENV var or use default if not set - log_level = os.getenv("LOGGING_LEVEL", "INFO") - logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) - - logging.debug("Creating RSU and count dictionaries...") - populateRsuDict() - - logging.info("Creating Data-In Kafka count threads...") - # Start the Kafka counters on their own threads - for message_type in message_types: - counter = KafkaMessageCounter( - f"KAFKA_IN_{message_type.upper()}", - message_type, - copy.deepcopy(rsu_location_dict), - copy.deepcopy(rsu_count_dict), - copy.deepcopy(rsu_count_dict), - 0, - ) - new_thread = threading.Thread(target=counter.start_counter) - new_thread.start() - thread_pool.append(new_thread) - - logging.info("Creating Data-Out Kafka count threads...") - # Start the Kafka counters on their own threads - for message_type in message_types: - counter = KafkaMessageCounter( - f"KAFKA_OUT_{message_type.upper()}", - message_type, - copy.deepcopy(rsu_location_dict), - copy.deepcopy(rsu_count_dict), - copy.deepcopy(rsu_count_dict), - 1, - ) - new_thread = threading.Thread(target=counter.start_counter) - new_thread.start() - thread_pool.append(new_thread) - - for thread in thread_pool: - thread.join() - logging.debug("Closed thread") - - -if __name__ == "__main__": - run() diff --git a/services/addons/images/count_metric/gen_email.py b/services/addons/images/count_metric/gen_email.py new file mode 100644 index 00000000..955647b1 --- /dev/null +++ b/services/addons/images/count_metric/gen_email.py @@ -0,0 +1,102 @@ +import logging +import os +from datetime import datetime + + +def diff_to_color(val): + return "#ff7373" if val > 5 else "#a4ffa1" + + +def generate_table_header(message_type_list): + html = ( + "\n" + '\n' + 'RSU\n' + 'Road\n' + ) + + for type in message_type_list: + html += f'{type} In\n' + html += f'{type} Out\n' + + html += "\n\n" + return html + + +def generate_table_row(rsu_ip, data, row_style, message_type_list): + html = ( + f'\n' + f"{rsu_ip}\n" + f'{data["primary_route"]}\n' + ) + + for type in message_type_list: + html += f'{data["counts"][type]["in"]}\n' + html += f'{data["counts"][type]["out"]}\n' + + html += "\n" + return html + + +def generate_count_table(rsu_dict, message_type_list): + logging.info(f"Creating count table...") + + # If the RSU dictionary is completely empty, return nothing to indicate an issue has occurred somewhere + if not rsu_dict: + logging.error("RSU dictionary is empty. Most likely an issue with PostgreSQL") + return "" + + html = f'\n{generate_table_header(message_type_list)}\n' + + style_switch = False + for rsu_ip, value in rsu_dict.items(): + row_style = ( + "text-align: center;background-color: #f2f2f2;" + if style_switch + else "text-align: center;" + ) + style_switch = not style_switch + + # Calculate differences between In and Out counts (%) + for type in value["counts"]: + in_count = value["counts"][type]["in"] + out_count = value["counts"][type]["out"] + + # Normalize the diff_percent depending on message types that are deduplicated to 1/hour + x = 3600 if type.lower() == "map" or type.lower() == "tim" else 1 + value["counts"][type]["diff_percent"] = ( + abs(out_count / -(-(in_count / x) // 1) - 1) * 100 + if in_count != 0 + else (5 if out_count > in_count else 0) + ) + + html += generate_table_row(rsu_ip, value, row_style, message_type_list) + + html += "\n
" + + return html + + +def generate_email_body(rsu_dict, start_dt, end_dt, message_type_list): + start = datetime.strftime(start_dt, "%Y-%m-%d 00:00:00") + end = datetime.strftime(end_dt, "%Y-%m-%d 00:00:00") + + # DEPLOYMENT_TITLE is a contextual title for where these counts apply. ie. "GCP prod" + # This is generalized to support any deployment environment + html = ( + f'

{str(os.environ["DEPLOYMENT_TITLE"]).upper()} Count Report {start} UTC - {end} UTC

' + "

This is an automated email to report yesterday's ODE message counts for J2735 messages going in and out of the ODE. " + "In counts are the number of encoded messages received by the ODE from the load balancer. " + "Out counts are the number of decoded messages that have come out of the ODE in JSON form and " + "are available for querying in mongoDB. Ideally, these two counts should be identical. " + "Although, some deviation is expected due to count recording timings. Outbound counts exceeding " + "5% deviation with their corresponding inbound counts will be marked red. Outbound counts within the 5% deviation will be marked " + "green. Map and TIM Out counts are deduplicated so these are going to be lower at 1 per hour. The deviation is normalized with this in mind. " + 'Any RSUs with a road name of "Unknown" are not recorded in the PostgreSQL database and might need to be added.

' + "

RSU Message Counts

" + ) + + table = generate_count_table(rsu_dict, message_type_list) + html += table + + return html diff --git a/services/addons/images/count_metric/kafka_counter.py b/services/addons/images/count_metric/kafka_counter.py deleted file mode 100644 index eb3c639f..00000000 --- a/services/addons/images/count_metric/kafka_counter.py +++ /dev/null @@ -1,264 +0,0 @@ -from confluent_kafka import Consumer, KafkaError, KafkaException -from google.cloud import bigquery -from apscheduler.schedulers.background import BackgroundScheduler -from datetime import timedelta -from datetime import datetime -from dateutil import parser -import pymongo -import os -import json -import copy -import logging - - -# Class for reading messages of a Kafka topic and counting them based off -# their originIp. -# - thread_id: ID used for logging to print out readable logs -# - message_type: Message type used to create Kafka topic name -# - rsu_location_dict: Dictionary containing all RSU IPs with associated road names -# - rsu_count_dict: Dictionary for keeping track of the counts pre-mapped with RSU IPs -# - rsu_count_dict_zero: What an empty dictionary looks like for proper memory resetting -# - type: Which Kafka topic to listen to, 0: data in, 1: data out -class KafkaMessageCounter: - def __init__( - self, - thread_id, - message_type, - rsu_location_dict, - rsu_count_dict, - rsu_count_dict_zero, - type, - ): - self.thread_id = thread_id - self.message_type = message_type - self.rsu_location_dict = rsu_location_dict - self.rsu_count_dict = rsu_count_dict - self.rsu_count_dict_zero = rsu_count_dict_zero - self.type = type - if os.getenv("DESTINATION_DB") == "BIGQUERY": - self.bq_client = bigquery.Client() - elif os.getenv("DESTINATION_DB") == "MONGODB": - if os.getenv("MONGO_DB_URI"): - self.mongo_client = pymongo.MongoClient(os.getenv("MONGO_DB_URI")) - else: - logging.error( - 'Database is set to MongoDB however, the "MONGO_DB_URI" environment variable is not specified.' - ) - - # Pushes a count to the bigquery RSU count table - def write_bigquery(self, query_values): - if self.type == 0: - tablename = os.getenv("KAFKA_BIGQUERY_TABLENAME") - else: - tablename = os.getenv("PUBSUB_BIGQUERY_TABLENAME") - - query = ( - f"INSERT INTO `{tablename}`(RSU, Road, Date, Type, Count) " - f"VALUES {query_values}" - ) - - query_job = self.bq_client.query(query) - # .result() ensures the Python script waits for this request to finish before moving on - query_job.result() - logging.info( - f"{self.thread_id}: Kafka insert for {self.message_type} succeeded" - ) - - # Pushes a count to the mongo RSU count collection - def write_mongo(self, documents): - if self.type == 0: - collection_name = os.getenv("INPUT_COUNTS_MONGO_COLLECTION_NAME") - else: - collection_name = os.getenv("OUTPUT_COUNTS_MONGO_COLLECTION_NAME") - - collection = self.mongo_client[os.getenv("MONGO_DB_NAME")][collection_name] - - result = collection.insert_many(documents) - - result.acknowledged() - - logging.info( - f"{self.thread_id}: Kafka insert for {self.message_type} succeeded" - ) - - def push_metrics(self): - current_counts = copy.deepcopy(self.rsu_count_dict) - self.rsu_count_dict = copy.deepcopy(self.rsu_count_dict_zero) - period = datetime.now() - timedelta(hours=1) - period = datetime.strftime(period, "%Y-%m-%d %H:%M:%S") - - logging.info(f"{self.thread_id}: Creating metrics...") - if os.getenv("DESTINATION_DB") == "BIGQUERY": - query_values = "" - for road, rsu_counts in current_counts.items(): - for ip, count in rsu_counts.items(): - query_values += f"('{ip}', '{road}', '{period}', '{self.message_type.upper()}', {count}), " - - try: - if len(query_values) > 0: - self.write_bigquery(query_values[:-2]) - else: - logging.warning( - f"{self.thread_id}: No values found to push for Kafka {self.message_type}" - ) - except Exception as e: - logging.error( - f"{self.thread_id}: The metric publish to BigQuery failed for {self.message_type.upper()}: {e}" - ) - return - elif os.getenv("DESTINATION_DB") == "MONGODB": - time = parser.parse(period) - count_list = [] - for road, rsu_counts in current_counts.items(): - for ip, count in rsu_counts.items(): - document = { - "ip": ip, - "road": road, - "timestamp": time, - "message_type": self.message_type.upper(), - "count": count, - } - logging.debug(document) - count_list.append(document) - - logging.info(f"Mongo db publishing messages : {str(count_list)}") - try: - if len(count_list) > 0: - self.write_mongo(count_list) - else: - logging.warning( - f"{self.thread_id}: No values found to push for Kafka {self.message_type}" - ) - except Exception as e: - logging.error( - f"{self.thread_id}: The metric publish to MongoDB failed for {self.message_type.upper()}: {e}" - ) - return - - logging.info(f"{self.thread_id}: Metrics published") - - # Called for every message that the subscription receives - def process_message(self, message): - try: - # Check if malformed message - jsonmsg = json.loads(message.value().decode("utf8")) - - if self.type == 0: - contentKey = self.message_type.capitalize() + "MessageContent" - item_arr = jsonmsg[contentKey] - - for msg_content in item_arr: - if "originRsu" in msg_content["metadata"]: - originIp = str(msg_content["metadata"]["originRsu"]) - else: - logging.warning( - f"{self.thread_id}: Malformed message detected. No source IP." - ) - originIp = "noIP" - - if originIp in self.rsu_location_dict: - self.rsu_count_dict[self.rsu_location_dict[originIp]][ - originIp - ] += 1 - else: - self.rsu_location_dict[originIp] = "Unknown" - self.rsu_count_dict["Unknown"][originIp] = 1 - else: - if "originIp" in jsonmsg["metadata"]: - originIp = str(jsonmsg["metadata"]["originIp"]) - else: - logging.warning( - f"{self.thread_id}: Malformed message detected. No source IP." - ) - originIp = "noIP" - - if originIp in self.rsu_location_dict: - self.rsu_count_dict[self.rsu_location_dict[originIp]][originIp] += 1 - else: - self.rsu_location_dict[originIp] = "Unknown" - self.rsu_count_dict["Unknown"][originIp] = 1 - except Exception as e: - logging.error( - f"{self.thread_id}: A Kafka message failed to be processed with the following error: {e}" - ) - - def should_run(self): - return True - - def listen_for_message_and_process(self, topic, bootstrap_server): - logging.debug( - f"{self.thread_id}: Listening for messages on Kafka topic {topic}..." - ) - - if os.getenv("KAFKA_TYPE", "") == "CONFLUENT": - username = os.getenv("CONFLUENT_KEY") - password = os.getenv("CONFLUENT_SECRET") - conf = { - "bootstrap.servers": bootstrap_server, - "security.protocol": "SASL_SSL", - "sasl.mechanism": "PLAIN", - "sasl.username": username, - "sasl.password": password, - "group.id": f"{self.thread_id}-counter", - "auto.offset.reset": "latest", - } - else: - conf = { - "bootstrap.servers": bootstrap_server, - "group.id": f"{self.thread_id}-counter", - "auto.offset.reset": "latest", - } - - consumer = Consumer(conf) - try: - consumer.subscribe([topic]) - - while self.should_run(): - msg = consumer.poll(timeout=1.0) - if msg is None: - continue - - if msg.error(): - if msg.error().code() == KafkaError._PARTITION_EOF: - # End of partition event - logging.warning( - "Topic %s [%d] reached end at offset %d\n" - % (msg.topic(), msg.partition(), msg.offset()) - ) - elif msg.error(): - raise KafkaException(msg.error()) - else: - self.process_message(msg) - finally: - # Close down consumer to commit final offsets. - consumer.close() - logging.warning( - f"{self.thread_id}: Disconnected from Kafka topic, reconnecting..." - ) - - def get_topic_from_type(self): - # 0 - in metric - # 1 - out metric - if self.type == 0: - topic = f"topic.OdeRawEncoded{self.message_type.upper()}Json" - else: - topic = f"topic.Ode{self.message_type.capitalize()}Json" - return topic - - # Read from Kafka topic indefinitely - def read_topic(self): - topic = self.get_topic_from_type() - bootstrap_server = os.getenv("ODE_KAFKA_BROKERS") - - self.listen_for_message_and_process(topic, bootstrap_server) - - def start_counter(self): - # Setup scheduler for async metric uploads - scheduler = BackgroundScheduler({"apscheduler.timezone": "UTC"}) - scheduler.add_job(self.push_metrics, "cron", minute="0") - scheduler.start() - - logging.info( - f"{self.thread_id}: Starting up {self.message_type.upper()} Kafka Metric thread..." - ) - self.read_topic() diff --git a/services/addons/images/count_metric/mongo_counter.py b/services/addons/images/count_metric/mongo_counter.py new file mode 100644 index 00000000..bb18a49e --- /dev/null +++ b/services/addons/images/count_metric/mongo_counter.py @@ -0,0 +1,72 @@ +import os +import logging +from pymongo import MongoClient +from datetime import datetime, timedelta + +message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM", "PSM"] + +def write_counts(mongo_db, counts): + output_collection = mongo_db["CVCounts"] + output_collection.insert_many(counts) + + +def count_query(mongo_db, message_type, start_dt, end_dt): + collection = mongo_db[f"Ode{message_type.capitalize()}Json"] + # Perform mongoDB aggregate query + agg_result = collection.aggregate( + [ + { + "$match": { + "recordGeneratedAt": { + "$gte": start_dt, + "$lt": end_dt, + } + } + }, + { + "$group": { + "_id": "$metadata.originIp", + "count": {"$sum": 1}, + } + }, + ] + ) + + counts = [] + for record in agg_result: + if not record["_id"]: + continue + count_record = { + "messageType": message_type, + "rsuIp": record["_id"], + "timestamp": start_dt, + "count": record["count"], + } + counts.append(count_record) + + return counts + + +def run_mongo_counter(mongo_db): + start_dt = (datetime.now() - timedelta(days=1)).replace( + minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(minute=0, second=0, microsecond=0) + + logging.info(f"Making counts for time period: {start_dt.strftime("%Y-%m-%d %H:%M:%S")} to {end_dt.strftime("%Y-%m-%d %H:%M:%S")}") + + rsu_counts = [] + for message_type in message_types: + # Append counts to list so they can be written to MongoDB in one request + rsu_counts = rsu_counts + count_query(mongo_db, message_type, start_dt, end_dt) + + logging.info("Writing counts to MongoDB") + write_counts(mongo_db, rsu_counts) + + +if __name__ == "__main__": + logging.info("Starting the MongoDB counter") + client = MongoClient(os.getenv("MONGO_DB_URI")) + mongo_db = client[os.getenv("MONGO_DB_NAME")] + run_mongo_counter(mongo_db) + logging.info("MongoDB counter has finished") diff --git a/services/addons/images/count_metric/requirements.txt b/services/addons/images/count_metric/requirements.txt index 5d6324f0..5ed19ebd 100644 --- a/services/addons/images/count_metric/requirements.txt +++ b/services/addons/images/count_metric/requirements.txt @@ -1,6 +1,3 @@ -confluent-kafka==2.3.0 -google-cloud-bigquery==3.14.1 -APScheduler==3.10.4 DateTime==5.2 requests==2.31.0 pymongo==4.5.0 diff --git a/services/addons/images/count_metric/sample.env b/services/addons/images/count_metric/sample.env index b7d691cd..f266b3fd 100644 --- a/services/addons/images/count_metric/sample.env +++ b/services/addons/images/count_metric/sample.env @@ -1,4 +1,31 @@ -LOGGING_LEVEL = "INFO" +LOGGING_LEVEL = 'INFO' + +ENABLE_EMAILER = 'True' + +# If ENABLE_EMAILER is 'True', set the following environment variables + +DEPLOYMENT_TITLE = 'JPO-ODE' + +# POSTGRES DATABASE VARIABLES +PG_DB_HOST = ':5432' +PG_DB_USER = +PG_DB_PASS = '' +PG_DB_NAME = + +# MONGODB REQUIRED VARIABLES +MONGO_DB_URI = 'mongodb://:27017/' +MONGO_DB_NAME = '' + +# SMTP REQUIRED VARIABLES +SMTP_SERVER_IP = '' +SMTP_USERNAME = '' +SMTP_PASSWORD = '' +SMTP_EMAIL = '' +# Multiple emails can be delimited by a ',' +SMTP_EMAIL_RECIPIENTS = 'test1@gmail.com,test2@gmail.com' + +# --------------------------------------------------------------------- +# If ENABLE_EMAILER is 'False', set the following environment variables MESSAGE_TYPES = 'bsm' PROJECT_ID = '' @@ -15,9 +42,9 @@ DESTINATION_DB = 'MONGODB' # MONGODB REQUIRED VARIABLES MONGO_DB_URI = 'mongodb://:27017/' -MONGO_DB_NAME = 'ODE' +MONGO_DB_NAME = '' INPUT_COUNTS_MONGO_COLLECTION_NAME = '' OUTPUT_COUNTS_MONGO_COLLECTION_NAME = '' # BIGQUERY REQUIRED VARIABLES -KAFKA_BIGQUERY_TABLENAME = '' \ No newline at end of file +KAFKA_BIGQUERY_TABLENAME = '' diff --git a/services/addons/images/firmware_manager/README.md b/services/addons/images/firmware_manager/README.md index 2d390ce2..8842cfd0 100644 --- a/services/addons/images/firmware_manager/README.md +++ b/services/addons/images/firmware_manager/README.md @@ -16,7 +16,7 @@ This directory contains a microservice that runs within the CV Manager GKE Clust An RSU is determined to be ready for upgrade if its entry in the "rsus" table in PostgreSQL has its "target_firmware_version" set to be different than its "firmware_version". The Firmware Manager will ignore all devices with incompatible firmware upgrades set as their target firmware based on the "firmware_upgrade_rules" table. The CV Manager API will only offer CV Manager webapp users compatible options so this generally is a precaution. -Hosting firmware files is recommended to be done via the cloud. GCP cloud storage is the currently supported method. Alternatives can be added via the [download_blob.py](download_blob.py) script. Firmware storage must be organized by: `vendor/rsu-model/firmware-version/install_package`. +Hosting firmware files is recommended to be done via the cloud. GCP cloud storage is the currently supported method, but a directory mounted as a docker volume can also be used. Alternative cloud support can be added via the [download_blob.py](download_blob.py) script. Firmware storage must be organized by: `vendor/rsu-model/firmware-version/install_package`. Firmware upgrades have unique procedures based on RSU vendor/manufacturer. To avoid requiring a unique bash script for every single firmware upgrade, the Firmware Manager has been written to use vendor based upgrade scripts that have been thoroughly tested. An interface-like abstract class, [base_upgrader.py](base_upgrader.py), has been made for helping create upgrade scripts for vendors not yet supported. The Firmware Manager selects the script to use based off the RSU's "model" column in the "rsus" table. These scripts report back to the Firmware Manager on completion with a status of whether the upgrade was a success or failure. Regardless, the Firmware Manager will remove the process from its tracking and update the PostgreSQL database accordingly. @@ -40,13 +40,14 @@ Available REST endpoints: To properly run the firmware_manager microservice the following services are also required: -- Cloud based blob storage +- Blob storage (cloud-based or otherwise) - Firmware storage must be organized by: `vendor/rsu-model/firmware-version/install_package`. - CV Manager PostgreSQL database with data in the "rsus", "rsu_models", "manufacturers", "firmware_images", and "firmware_upgrade_rules" tables - Network connectivity from the environment the firmware_manager is deployed into to the blob storage and the RSUs The firmware_manager microservice expects the following environment variables to be set: +- ACTIVE_UPGRADE_LIMIT - The number of concurrent upgrades that are allowed to be running at any given moment. Any upgrades requested beyond this limit will wait on the upgrade queue. - BLOB_STORAGE_PROVIDER - Host for the blob storage. Default is GCP. - BLOB_STORAGE_BUCKET - Cloud blob storage bucket for firmware storage. - PG_DB_USER - PostgreSQL access username. @@ -55,17 +56,31 @@ The firmware_manager microservice expects the following environment variables to - PG_DB_HOST - PostgreSQL hostname, make sure to include port number. - LOGGING_LEVEL (optional, defaults to 'info') +The Firmware Manager is capable of sending an email to the support team in the event that an online RSU experiences a firmware upgrade failure. +To do so the following environment variables must be set: + +- SMTP_EMAIL - Email to send from. +- SMTP_USERNAME - SMTP username for SMTP_EMAIL. +- SMTP_PASSWORD - SMTP password for SMTP_EMAIL. +- FW_EMAIL_RECIPIENTS - Comma-separated list of emails to send failure notifications to. +- SMTP_SERVER_IP - Address of the SMTP server. + GCP Required environment variables: - GCP_PROJECT - GCP project for the firmware cloud storage bucket - GOOGLE_APPLICATION_CREDENTIALS - Service account location. Recommended to attach as a volume. +Docker volume required environment variables: +- HOST_BLOB_STORAGE_DIRECTORY - Directory mounted as a docker volume for firmware storage. A relative path can be specified here. + ## Vendor Specific Requirements ### Commsignia Each upgrade requires just one firmware file. Upload target firmware to a cloud storage bucket or alternative hosting service according to the `vendor/rsu-model/firmware-version/install_package` directory path format. +The Firmware Manager is also able to run a bash script on Commsignia RSUs after the firmware update has been completed. If uploading a script to a cloud storage bucket or alternative hosting service do so at the directory path `vendor/rsu-model/firmware-version/post_upgrade.sh`. Additionally, the post_upgrade.sh script will need to output "ALL OK" to stdout to notify the Firmware Manager that it has completed successfully. + ### Yunex Each upgrade requires 4 total files tarred up into a single TAR file: diff --git a/services/addons/images/firmware_manager/commsignia_upgrader.py b/services/addons/images/firmware_manager/commsignia_upgrader.py index b5b12ffb..e3b572a4 100644 --- a/services/addons/images/firmware_manager/commsignia_upgrader.py +++ b/services/addons/images/firmware_manager/commsignia_upgrader.py @@ -1,3 +1,4 @@ +import time from paramiko import SSHClient, WarningPolicy from scp import SCPClient import upgrader @@ -9,13 +10,72 @@ class CommsigniaUpgrader(upgrader.UpgraderAbstractClass): def __init__(self, upgrade_info): + # set file/blob location for post_upgrade script + self.post_upgrade_file_name = f"/home/{upgrade_info['ipv4_address']}/post_upgrade.sh" + self.post_upgrade_blob_name = f"{upgrade_info['manufacturer']}/{upgrade_info['model']}/{upgrade_info['target_firmware_version']}/post_upgrade.sh" super().__init__(upgrade_info) def upgrade(self): - try: - # Download firmware installation package - self.download_blob() + if (self.check_online()): + try: + # Download firmware installation package + self.download_blob() + + # Make connection with the target device + logging.info("Making SSH connection with the device...") + ssh = SSHClient() + ssh.set_missing_host_key_policy(WarningPolicy) + ssh.connect( + self.rsu_ip, + username=self.ssh_username, + password=self.ssh_password, + look_for_keys=False, + allow_agent=False, + ) + + # Make SCP client to copy over the firmware installation package to the /tmp/ directory on the remote device + logging.info("Copying installation package to the device...") + scp = SCPClient(ssh.get_transport()) + scp.put(self.local_file_name, remote_path="/tmp/") + scp.close() + + # Run firmware upgrade and reboot + logging.info("Running firmware upgrade...") + _stdin, _stdout, _stderr = ssh.exec_command( + f"signedUpgrade.sh /tmp/{self.install_package}" + ) + decoded_stdout = _stdout.read().decode() + logging.info(decoded_stdout) + if "ALL OK" not in decoded_stdout: + ssh.close() + # Notify Firmware Manager of failed firmware upgrade completion + self.notify_firmware_manager(success=False) + return + ssh.exec_command("reboot") + ssh.close() + + # If post_upgrade script exists execute it + if (self.download_blob(self.post_upgrade_blob_name, self.post_upgrade_file_name)): + self.post_upgrade() + # Delete local installation package and its parent directory so it doesn't take up storage space + self.cleanup() + + # Notify Firmware Manager of successful firmware upgrade completion + self.notify_firmware_manager(success=True) + except Exception as err: + # If something goes wrong, cleanup anything left and report failure if possible + logging.error(f"Failed to perform firmware upgrade: {err}") + self.cleanup() + self.notify_firmware_manager(success=False) + # send email to support team with the rsu and error + self.send_error_email("Firmware Upgrader", err) + + def post_upgrade(self): + if self.wait_until_online() == -1: + raise Exception("RSU offline for too long after firmware upgrade") + try: + time.sleep(60) # Make connection with the target device logging.info("Making SSH connection with the device...") ssh = SSHClient() @@ -28,38 +88,32 @@ def upgrade(self): allow_agent=False, ) - # Make SCP client to copy over the firmware installation package to the /tmp/ directory on the remote device - logging.info("Copying installation package to the device...") + # Make SCP client to copy over the post upgrade script to the /tmp/ directory on the remote device + logging.info("Copying post upgrade script to the device...") scp = SCPClient(ssh.get_transport()) - scp.put(self.local_file_name, remote_path="/tmp/") + scp.put(self.post_upgrade_file_name, remote_path="/tmp/") scp.close() - # Delete local installation package and its parent directory so it doesn't take up storage space - self.cleanup() - - # Run firmware upgrade and reboot - logging.info("Running firmware upgrade...") + # Change permissions and execute post upgrade script + logging.info("Running post upgrade script...") + ssh.exec_command( + f"chmod +x /tmp/post_upgrade.sh" + ) _stdin, _stdout, _stderr = ssh.exec_command( - f"signedUpgrade.sh /tmp/{self.install_package}" + f"/tmp/post_upgrade.sh" ) decoded_stdout = _stdout.read().decode() logging.info(decoded_stdout) if "ALL OK" not in decoded_stdout: ssh.close() - # Notify Firmware Manager of failed firmware upgrade completion - self.notify_firmware_manager(success=False) + logging.error(f"Failed to execute post upgrade script for rsu {self.rsu_ip}: {decoded_stdout}") return - ssh.exec_command("reboot") ssh.close() - - # Notify Firmware Manager of successful firmware upgrade completion - self.notify_firmware_manager(success=True) + logging.info(f"Post upgrade script executed successfully for rsu: {self.rsu_ip}.") except Exception as err: - # If something goes wrong, cleanup anything left and report failure if possible - logging.error(f"Failed to perform firmware upgrade: {err}") - self.cleanup() - self.notify_firmware_manager(success=False) - + logging.error(f"Failed to execute post upgrade script for rsu {self.rsu_ip}: {err}") + # send email to support team with the rsu and error + self.send_error_email("Post-Upgrade Script", err) # sys.argv[1] - JSON string with the following key-values: # - ipv4_address diff --git a/services/addons/images/firmware_manager/download_blob.py b/services/addons/images/firmware_manager/download_blob.py index 487ffbcf..36901273 100644 --- a/services/addons/images/firmware_manager/download_blob.py +++ b/services/addons/images/firmware_manager/download_blob.py @@ -3,14 +3,67 @@ import os -# Only supports GCP Bucket Storage for downloading blobs def download_gcp_blob(blob_name, destination_file_name): + """Download a file from a GCP Bucket Storage bucket to a local file. + + Args: + blob_name (str): The name of the file in the bucket. + destination_file_name (str): The name of the local file to download the bucket file to. + """ + + if not validate_file_type(blob_name): + return False + gcp_project = os.environ.get("GCP_PROJECT") bucket_name = os.environ.get("BLOB_STORAGE_BUCKET") storage_client = storage.Client(gcp_project) bucket = storage_client.get_bucket(bucket_name) blob = bucket.blob(blob_name) - blob.download_to_filename(destination_file_name) + + if blob.exists(): + blob.download_to_filename(destination_file_name) + logging.info( + f"Downloaded storage object {blob_name} from bucket {bucket_name} to local file {destination_file_name}." + ) + return True + return False + + +def download_docker_blob(blob_name, destination_file_name): + """Copy a file from a directory mounted as a volume in a Docker container to a local file. + + Args: + blob_name (str): The name of the file in the directory. + destination_file_name (str): The name of the local file to copy the directory file to. + """ + + if not validate_file_type(blob_name): + return False + + directory = "/mnt/blob_storage" + source_file_name = f"{directory}/{blob_name}" + os.system(f"cp {source_file_name} {destination_file_name}") logging.info( - f"Downloaded storage object {blob_name} from bucket {bucket_name} to local file {destination_file_name}." + f"Copied storage object {blob_name} from directory {directory} to local file {destination_file_name}." ) + return True + + +def validate_file_type(file_name): + """Validate the file type of the file to be downloaded. + + Args: + file_name (str): The name of the file to be downloaded. + """ + if not file_name.endswith(".tar"): + logging.error( + f"Unsupported file type for storage object {file_name}. Only .tar files are supported." + ) + return False + return True + + +class UnsupportedFileTypeException(Exception): + def __init__(self, message="Unsupported file type. Only .tar files are supported."): + self.message = message + super().__init__(self.message) diff --git a/services/addons/images/firmware_manager/firmware_manager.py b/services/addons/images/firmware_manager/firmware_manager.py index 21de87e4..e390bbbc 100644 --- a/services/addons/images/firmware_manager/firmware_manager.py +++ b/services/addons/images/firmware_manager/firmware_manager.py @@ -1,5 +1,6 @@ from apscheduler.schedulers.background import BackgroundScheduler from common import pgquery +from collections import deque from flask import Flask, jsonify, request from subprocess import Popen, DEVNULL from threading import Lock @@ -13,6 +14,7 @@ log_level = os.environ.get("LOGGING_LEVEL", "INFO") logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) +ACTIVE_UPGRADE_LIMIT = os.environ.get("ACTIVE_UPGRADE_LIMIT", 20) manufacturer_upgrade_scripts = { "Commsignia": "commsignia_upgrader.py", @@ -32,6 +34,8 @@ # - target_firmware_version # - install_package active_upgrades = {} +upgrade_queue = deque([]) +upgrade_queue_info = {} active_upgrades_lock = Lock() @@ -65,6 +69,34 @@ def get_rsu_upgrade_data(rsu_ip="all"): return return_list +def start_tasks_from_queue(): + # Start the next process in the queue if there are less than ACTIVE_UPGRADE_LIMIT number of active upgrades occurring + while len(active_upgrades) < ACTIVE_UPGRADE_LIMIT and len(upgrade_queue) > 0: + rsu_to_upgrade = upgrade_queue.popleft() + try: + rsu_upgrade_info = upgrade_queue_info[rsu_to_upgrade] + del upgrade_queue_info[rsu_to_upgrade] + p = Popen( + [ + "python3", + f'/home/{manufacturer_upgrade_scripts[rsu_upgrade_info["manufacturer"]]}', + f"'{json.dumps(rsu_upgrade_info)}'", + ], + stdout=DEVNULL, + ) + rsu_upgrade_info["process"] = p + # Remove redundant ipv4_address from rsu since it is the key for active_upgrades + del rsu_upgrade_info["ipv4_address"] + active_upgrades[rsu_to_upgrade] = rsu_upgrade_info + except Exception as err: + # If this case occurs, only log it since there may not be a listener. + # Since the upgrade_queue and upgrade_queue_info will no longer have the RSU present, + # the hourly check_for_upgrades() will pick up the firmware upgrade again to retry the upgrade. + logging.error( + f"Encountered error of type {type(err)} while starting automatic upgrade process for {rsu_to_upgrade}: {err}" + ) + + # REST endpoint to manually start firmware upgrades for targeted RSUs. # Required request body values: # - rsu_ip: Target device IP @@ -76,14 +108,17 @@ def init_firmware_upgrade(): # Acquire lock and check if an upgrade is already occurring for the device logging.info( - f"Checking if existing upgrade is running for '{request_args['rsu_ip']}'" + f"Checking if existing upgrade is running or queued for '{request_args['rsu_ip']}'" ) with active_upgrades_lock: - if request_args["rsu_ip"] in active_upgrades: + if ( + request_args["rsu_ip"] in active_upgrades + or request_args["rsu_ip"] in upgrade_queue + ): return ( jsonify( { - "error": f"Firmware upgrade failed to start for '{request_args['rsu_ip']}': an upgrade is already underway for the target device" + "error": f"Firmware upgrade failed to start for '{request_args['rsu_ip']}': an upgrade is already underway or queued for the target device" } ), 500, @@ -103,34 +138,15 @@ def init_firmware_upgrade(): ) rsu_to_upgrade = rsu_to_upgrade[0] - # Start upgrade process - logging.info(f"Initializing firmware upgrade for '{request_args['rsu_ip']}'") - try: - p = Popen( - [ - "python3", - f'/home/{manufacturer_upgrade_scripts[rsu_to_upgrade["manufacturer"]]}', - f"'{json.dumps(rsu_to_upgrade)}'", - ], - stdout=DEVNULL, - ) - rsu_to_upgrade["process"] = p - except Exception as err: - logging.error( - f"Encountered error of type {type(err)} while starting automatic upgrade process for {request_args['rsu_ip']}: {err}" - ) - return ( - jsonify( - { - "error": f"Firmware upgrade failed to start for '{request_args['rsu_ip']}': upgrade process failed to run" - } - ), - 500, - ) + # Add the RSU to the upgrade queue and record the necessary upgrade information + logging.info( + f"Adding '{request_args['rsu_ip']}' to the firmware manager upgrade queue" + ) + upgrade_queue.extend([request_args["rsu_ip"]]) + upgrade_queue_info[request_args["rsu_ip"]] = rsu_to_upgrade - # Remove redundant ipv4_address from rsu_to_upgrade since it is the key for active_upgrades - del rsu_to_upgrade["ipv4_address"] - active_upgrades[request_args["rsu_ip"]] = rsu_to_upgrade + # Start any processes that can be started + start_tasks_from_queue() return ( jsonify( { @@ -199,6 +215,9 @@ def firmware_upgrade_completed(): ) del active_upgrades[request_args["rsu_ip"]] + # Start any processes that can be started + start_tasks_from_queue() + return jsonify({"message": "Firmware upgrade successfully marked as complete"}), 204 @@ -216,7 +235,7 @@ def list_active_upgrades(): "target_firmware_version": value["target_firmware_version"], "install_package": value["install_package"], } - return jsonify({"active_upgrades": sanitized_active_upgrades}), 200 + return jsonify({"active_upgrades": sanitized_active_upgrades, "upgrade_queue": list(upgrade_queue)}), 200 # Scheduled firmware upgrade checker @@ -225,38 +244,26 @@ def check_for_upgrades(): # Get all RSUs that need to be upgraded from the PostgreSQL database rsus_to_upgrade = get_rsu_upgrade_data() - # Start upgrade scripts for any results - for rsu in rsus_to_upgrade: - # Check if an upgrade is already occurring for the device - with active_upgrades_lock: - if rsu["ipv4_address"] in active_upgrades: + with active_upgrades_lock: + # Start upgrade scripts for any results + for rsu in rsus_to_upgrade: + # Check if an upgrade is already occurring for the device + if ( + rsu["ipv4_address"] in active_upgrades + or rsu["ipv4_address"] in upgrade_queue + ): continue - # Start upgrade script + # Add the RSU to the upgrade queue and record the necessary upgrade information logging.info( - f"Running automated firmware upgrade for '{rsu['ipv4_address']}'" + f"Adding '{rsu["ipv4_address"]}' to the firmware manager upgrade queue" ) - try: - p = Popen( - [ - "python3", - f'/home/{manufacturer_upgrade_scripts[rsu["manufacturer"]]}', - f"'{json.dumps(rsu)}'", - ], - stdout=DEVNULL, - ) - rsu["process"] = p - except Exception as err: - logging.error( - f"Encountered error of type {type(err)} while starting automatic upgrade process for {rsu['ipv4_address']}: {err}" - ) - continue + upgrade_queue.extend([rsu["ipv4_address"]]) + upgrade_queue_info[rsu["ipv4_address"]] = rsu + logging.info(f"Firmware upgrade successfully started for '{rsu["ipv4_address"]}'") - # Remove redundant ipv4_address from rsu since it is the key for active_upgrades - rsu_ip = rsu["ipv4_address"] - del rsu["ipv4_address"] - active_upgrades[rsu_ip] = rsu - logging.info(f"Firmware upgrade successfully started for '{rsu_ip}'") + # Start any processes that can be started + start_tasks_from_queue() def serve_rest_api(): diff --git a/services/addons/images/firmware_manager/sample.env b/services/addons/images/firmware_manager/sample.env index 03c9661d..8f045a07 100644 --- a/services/addons/images/firmware_manager/sample.env +++ b/services/addons/images/firmware_manager/sample.env @@ -1,4 +1,5 @@ LOGGING_LEVEL="INFO" +ACTIVE_UPGRADE_LIMIT=20 # PostgreSQL database variables PG_DB_HOST="" @@ -6,10 +7,19 @@ PG_DB_NAME="" PG_DB_USER="" PG_DB_PASS="" -# Blob storage variables -BLOB_STORAGE_PROVIDER="GCP" -BLOB_STORAGE_BUCKET="" +# Blob storage variables (only 'GCP' and 'DOCKER' are supported at this time) +BLOB_STORAGE_PROVIDER=DOCKER +BLOB_STORAGE_BUCKET= +## Docker volume mount point for BLOB storage (if using DOCKER) +HOST_BLOB_STORAGE_DIRECTORY=./local_blob_storage # For users using GCP cloud storage GCP_PROJECT="" -GOOGLE_APPLICATION_CREDENTIALS="" \ No newline at end of file +GOOGLE_APPLICATION_CREDENTIALS="" + +# For sending failure emails +FW_EMAIL_RECIPIENTS= +SMTP_SERVER_IP= +SMTP_EMAIL= +SMTP_USERNAME= +SMTP_PASSWORD= \ No newline at end of file diff --git a/services/addons/images/firmware_manager/upgrader.py b/services/addons/images/firmware_manager/upgrader.py index fd76cc36..88d871ad 100644 --- a/services/addons/images/firmware_manager/upgrader.py +++ b/services/addons/images/firmware_manager/upgrader.py @@ -1,10 +1,13 @@ from pathlib import Path import abc +import subprocess +import time import download_blob import logging import os import requests import shutil +from common.emailSender import EmailSender class UpgraderAbstractClass(abc.ABC): @@ -27,17 +30,26 @@ def cleanup(self): shutil.rmtree(path) # Downloads firmware install package blob to /home/rsu_ip/ - def download_blob(self): + def download_blob(self, blob_name=None, local_file_name=None): # Create parent rsu_ip directory path = self.local_file_name[: self.local_file_name.rfind("/")] Path(path).mkdir(exist_ok=True) + blob_name = self.blob_name if blob_name is None else blob_name + local_file_name = ( + self.local_file_name if local_file_name is None else local_file_name + ) # Download blob, defaults to GCP blob storage - bsp = os.environ.get("BLOB_STORAGE_PROVIDER", "GCP") - if bsp == "GCP": - download_blob.download_gcp_blob(self.blob_name, self.local_file_name) + bspCaseInsensitive = os.environ.get( + "BLOB_STORAGE_PROVIDER", "DOCKER" + ).casefold() + if bspCaseInsensitive == "gcp": + return download_blob.download_gcp_blob(blob_name, local_file_name) + elif bspCaseInsensitive == "docker": + return download_blob.download_docker_blob(blob_name, local_file_name) else: logging.error("Unsupported blob storage provider") + raise StorageProviderNotSupportedException # Notifies the firmware manager of the completion status for the upgrade # success is a boolean @@ -54,7 +66,68 @@ def notify_firmware_manager(self, success): f"Failed to connect to the Firmware Manager API for '{self.rsu_ip}': {err}" ) + def wait_until_online(self): + iter = 0 + # Ping once every second for 3 minutes until online + while iter < 180: + time.sleep(1) + code = subprocess.run( + ["ping", "-n", "-c1", self.rsu_ip], capture_output=True + ).returncode + if code == 0: + return 0 + iter += 1 + # 3 minutes pass with no response + return -1 + + def check_online(self): + iter = 0 + # Ping once every second for 5 seconds to verify RSU is online + while iter < 5: + code = subprocess.run( + ["ping", "-n", "-c1", self.rsu_ip], capture_output=True + ).returncode + if code == 0: + return True + iter += 1 + time.sleep(1) + # 5 seconds pass with no response + return False + + def send_error_email(self, type="Firmware Upgrader", err=""): + try: + email_addresses = os.environ.get("FW_EMAIL_RECIPIENTS").split(",") + + subject = ( + f"{self.rsu_ip} Firmware Upgrader Failure" + if type == "Firmware Upgrader" + else f"{self.rsu_ip} Firmware Upgrader Post Upgrade Script Failure" + ) + + for email_address in email_addresses: + emailSender = EmailSender( + os.environ["SMTP_SERVER_IP"], + 587, + ) + emailSender.send( + sender=os.environ["SMTP_EMAIL"], + recipient=email_address, + subject=subject, + message=f"{type}: Failed to perform update on RSU {self.rsu_ip} due to the following error: {err}", + replyEmail="", + username=os.environ["SMTP_USERNAME"], + password=os.environ["SMTP_PASSWORD"], + pretty=True, + ) + except Exception as e: + logging.error(e) + # This needs to be defined for each implementation @abc.abstractclassmethod def upgrade(self): pass + + +class StorageProviderNotSupportedException(Exception): + def __init__(self): + super().__init__("Unsupported blob storage provider") diff --git a/services/addons/images/firmware_manager/yunex_upgrader.py b/services/addons/images/firmware_manager/yunex_upgrader.py index f5987808..2c520734 100644 --- a/services/addons/images/firmware_manager/yunex_upgrader.py +++ b/services/addons/images/firmware_manager/yunex_upgrader.py @@ -41,77 +41,66 @@ def run_xfer_upgrade(self, file_name): # If everything goes as expected, the XFER upgrade was complete return 0 - def wait_until_online(self): - iter = 0 - # Ping once every second for 3 minutes until online - while iter < 180: - time.sleep(1) - code = subprocess.run( - ["ping", "-n", "-c1", self.rsu_ip], capture_output=True - ).returncode - if code == 0: - return 0 - iter += 1 - # 3 minutes pass with no response - return -1 - def upgrade(self): - try: - # Download firmware installation package TAR file - self.download_blob() - - # Unpack TAR file which must contain the following: - # - Core upgrade file - # - SDK upgrade file - # - Application provision file - # - upgrade_info.json which defines the files as a single JSON object - logging.info("Unpacking TAR file...") - with tarfile.open(self.local_file_name, "r") as tar: - tar.extractall(self.root_path) - - # Obtain upgrade info in the following format: - # { "core": "core-file-name", "sdk": "sdk-file-name", "provision": "provision-file-name"} - with open(f"{self.root_path}/upgrade_info.json") as json_file: - upgrade_info = json.load(json_file) - - # Run Core upgrade - logging.info("Running Core firmware upgrade...") - code = self.run_xfer_upgrade(f"{self.root_path}/{upgrade_info['core']}") - if code == -1: - raise Exception("Yunex RSU Core upgrade failed") - if self.wait_until_online() == -1: - raise Exception("RSU offline for too long after Core upgrade") - # Wait an additional 60 seconds after the Yunex RSU is online - needs time to initialize - time.sleep(60) - - # Run SDK upgrade - logging.info("Running SDK firmware upgrade...") - code = self.run_xfer_upgrade(f"{self.root_path}/{upgrade_info['sdk']}") - if code == -1: - raise Exception("Yunex RSU SDK upgrade failed") - if self.wait_until_online() == -1: - raise Exception("RSU offline for too long after SDK upgrade") - # Wait an additional 60 seconds after the Yunex RSU is online - needs time to initialize - time.sleep(60) - - # Run application provision image - logging.info("Running application provisioning...") - code = self.run_xfer_upgrade( - f"{self.root_path}/{upgrade_info['provision']}" - ) - if code == -1: - raise Exception("Yunex RSU application provisioning upgrade failed") - - # Notify Firmware Manager of successful firmware upgrade completion - self.cleanup() - self.notify_firmware_manager(success=True) - except Exception as err: - # If something goes wrong, cleanup anything left and report failure if possible. - # Yunex RSUs can handle having the same firmware upgraded over again. - # There is no issue with starting from the beginning even with a partially complete upgrade. - logging.error(f"Failed to perform firmware upgrade: {err}") - self.cleanup() - self.notify_firmware_manager(success=False) + if (self.check_online()): + try: + # Download firmware installation package TAR file + self.download_blob() + + # Unpack TAR file which must contain the following: + # - Core upgrade file + # - SDK upgrade file + # - Application provision file + # - upgrade_info.json which defines the files as a single JSON object + logging.info("Unpacking TAR file...") + with tarfile.open(self.local_file_name, "r") as tar: + tar.extractall(self.root_path) + + # Obtain upgrade info in the following format: + # { "core": "core-file-name", "sdk": "sdk-file-name", "provision": "provision-file-name"} + with open(f"{self.root_path}/upgrade_info.json") as json_file: + upgrade_info = json.load(json_file) + + # Run Core upgrade + logging.info("Running Core firmware upgrade...") + code = self.run_xfer_upgrade(f"{self.root_path}/{upgrade_info['core']}") + if code == -1: + raise Exception("Yunex RSU Core upgrade failed") + if self.wait_until_online() == -1: + raise Exception("RSU offline for too long after Core upgrade") + # Wait an additional 60 seconds after the Yunex RSU is online - needs time to initialize + time.sleep(60) + + # Run SDK upgrade + logging.info("Running SDK firmware upgrade...") + code = self.run_xfer_upgrade(f"{self.root_path}/{upgrade_info['sdk']}") + if code == -1: + raise Exception("Yunex RSU SDK upgrade failed") + if self.wait_until_online() == -1: + raise Exception("RSU offline for too long after SDK upgrade") + # Wait an additional 60 seconds after the Yunex RSU is online - needs time to initialize + time.sleep(60) + + # Run application provision image + logging.info("Running application provisioning...") + code = self.run_xfer_upgrade( + f"{self.root_path}/{upgrade_info['provision']}" + ) + if code == -1: + raise Exception("Yunex RSU application provisioning upgrade failed") + + # Notify Firmware Manager of successful firmware upgrade completion + self.cleanup() + self.notify_firmware_manager(success=True) + except Exception as err: + # If something goes wrong, cleanup anything left and report failure if possible. + # Yunex RSUs can handle having the same firmware upgraded over again. + # There is no issue with starting from the beginning even with a partially complete upgrade. + logging.error(f"Failed to perform firmware upgrade: {err}") + self.cleanup() + self.notify_firmware_manager(success=False) + # send email to support team with the rsu and error + self.send_error_email("Firmware Upgrader", err) # sys.argv[1] - JSON string with the following key-values: diff --git a/services/addons/images/geo_msg_query/README.md b/services/addons/images/geo_msg_query/README.md new file mode 100644 index 00000000..b60477b6 --- /dev/null +++ b/services/addons/images/geo_msg_query/README.md @@ -0,0 +1,17 @@ +# GeoSpatial Message Query Utility + +Service that creates a geospatially queryable MongoDB collection for use with the CV manager. + +To run the script, the following environment variables must be set: + +LOGGING_LEVEL: The logging level of the deployment. Options are: 'critical', 'error', 'warning', 'info' and 'debug'. If not specified, will default to 'info'. Refer to Python's documentation for more info: [Python logging](https://docs.python.org/3/howto/logging.html). + +MONGO_DB_URI: Connection string uri for the MongoDB database, please refer to the following [documentation](https://www.mongodb.com/docs/manual/reference/connection-string/). + +MONGO_DB_NAME: MongoDB database name. + +MONGO_INPUT_COLLECTIONS: MongoDB collection for the input of the service, eg: 'OdeBsmJson,OdePsmJson' + +MONGO_GEO_OUTPUT_COLLECTION: MongoDB collection that will be created by the bsm_query script. It will also create an index for better geospatial query performance. + +MONGO_TTL: Time to live in days for messages produced by this service. This will create a TTL index in the output Mongo collection. diff --git a/services/addons/images/bsm_query/__init__.py b/services/addons/images/geo_msg_query/__init__.py similarity index 100% rename from services/addons/images/bsm_query/__init__.py rename to services/addons/images/geo_msg_query/__init__.py diff --git a/services/addons/images/geo_msg_query/geo_msg_query.py b/services/addons/images/geo_msg_query/geo_msg_query.py new file mode 100644 index 00000000..3fc77780 --- /dev/null +++ b/services/addons/images/geo_msg_query/geo_msg_query.py @@ -0,0 +1,183 @@ +import os +from concurrent.futures import ThreadPoolExecutor +import logging +from pymongo import MongoClient, DESCENDING, GEOSPHERE +from datetime import datetime +import traceback + + +def set_mongo_client(MONGO_DB_URI, MONGO_DB): + client = MongoClient(MONGO_DB_URI) + db = client[MONGO_DB] + return db + + +def create_message(original_message, msg_type): + try: + latitude = None + longitude = None + if msg_type == "Bsm": + longitude = original_message["payload"]["data"]["coreData"]["position"][ + "longitude" + ] + latitude = original_message["payload"]["data"]["coreData"]["position"][ + "latitude" + ] + elif msg_type == "Psm": + longitude = original_message["payload"]["data"]["position"]["longitude"] + latitude = original_message["payload"]["data"]["position"]["latitude"] + if latitude and longitude: + timestamp_str = original_message["metadata"]["odeReceivedAt"] + # checking if the timestamp is using nanoseconds and then truncating + # to milliseconds to avoid exceptions when creating the datetime object. + if len(timestamp_str) > 26: + timestamp_str = timestamp_str[:26] + "Z" + new_message = { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + longitude, + latitude, + ], + }, + "properties": { + "id": original_message["metadata"]["originIp"], + "timestamp": datetime.strptime( + timestamp_str, + "%Y-%m-%dT%H:%M:%S.%fZ", + ), + "msg_type": msg_type, + }, + } + return new_message + else: + logging.warn( + f"create_message: Could not create a message for type: {msg_type}" + ) + return None + except Exception as e: + logging.error(f"create_message: Exception occurred: {str(e)}") + logging.error(traceback.format_exc()) + return None + + +def process_message(message, db, collection, msg_type): + new_message = create_message(message, msg_type) + if new_message: + db[collection].insert_one(new_message) + else: + logging.error( + f"process_message: Could not create a message from the input {msg_type} message: {message}" + ) + + +def set_indexes(db, collection, mongo_ttl): + logging.info("Creating indexes for the output collection") + output_collection_obj = db[collection] + index_info = output_collection_obj.index_information() + + if "timestamp_geosphere_index" not in index_info: + logging.info("Creating timestamp_geosphere_index") + output_collection_obj.create_index( + [ + ("properties.timestamp", DESCENDING), + ("properties.msg_type", DESCENDING), + ("geometry", GEOSPHERE), + ], + name="timestamp_geosphere_index", + ) + else: + logging.info("timestamp_geosphere_index already exists") + if "ttl_index" not in index_info: + logging.info("Creating ttl_index") + output_collection_obj.create_index( + [("properties.timestamp", DESCENDING)], + name="ttl_index", + expireAfterSeconds=int(mongo_ttl) * 24 * 60 * 60, + ) + else: + existing_ttl = index_info["ttl_index"]["expireAfterSeconds"] + wanted_ttl = int(mongo_ttl) * 24 * 60 * 60 + if existing_ttl != wanted_ttl: + logging.info("ttl_index exists but with different TTL value. Recreating...") + output_collection_obj.drop_index("ttl_index") + output_collection_obj.create_index( + [("properties.timestamp", DESCENDING)], + name="ttl_index", + expireAfterSeconds=wanted_ttl, + ) + else: + logging.info("ttl_index already exists with the correct TTL value") + + +def watch_collection(db, input_collection, output_collection): + try: + msg_type = input_collection.replace("Ode", "").replace("Json", "") + logging.info(f"Watching collection: {input_collection}") + count = 0 + with db[input_collection].watch(full_document="updateLookup") as stream: + for change in stream: + if change.get("operationType") in ["insert"]: + count += 1 + logging.debug(f"Change: {change}") + process_message( + change["fullDocument"], db, output_collection, msg_type + ) + logging.debug(f"{msg_type} Count: {count}") + else: + logging.debug( + f"Ignoring change with operationType: {change.get('operationType')}" + ) + except Exception as e: + logging.error( + f"An error occurred while watching collection: {input_collection}" + ) + logging.error(str(e)) + logging.error(traceback.format_exc()) + + +def run(): + MONGO_DB_URI = os.getenv("MONGO_DB_URI") + MONGO_DB = os.getenv("MONGO_DB_NAME") + MONGO_INPUT_COLLECTIONS = os.getenv("MONGO_INPUT_COLLECTIONS") + MONGO_GEO_OUTPUT_COLLECTION = os.getenv("MONGO_GEO_OUTPUT_COLLECTION") + MONGO_TTL = os.getenv("MONGO_TTL") # in days + + if ( + MONGO_DB_URI is None + or MONGO_INPUT_COLLECTIONS is None + or MONGO_DB is None + or MONGO_GEO_OUTPUT_COLLECTION is None + or MONGO_TTL is None + ): + logging.error("Environment variables are not set! Exiting.") + exit("Environment variables are not set! Exiting.") + + log_level = ( + "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] + ) + logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + + logging.debug("Starting the service with environment variables: ") + logging.debug(f"MONGO_DB: {MONGO_DB}") + logging.debug(f"MONGO_INPUT_COLLECTIONS: {MONGO_INPUT_COLLECTIONS}") + logging.debug(f"MONGO_GEO_OUTPUT_COLLECTION: {MONGO_GEO_OUTPUT_COLLECTION}") + logging.debug(f"MONGO_TTL: {MONGO_TTL}") + + db = set_mongo_client(MONGO_DB_URI, MONGO_DB) + set_indexes(db, MONGO_GEO_OUTPUT_COLLECTION, MONGO_TTL) + input_collections = MONGO_INPUT_COLLECTIONS.split(",") + + with ThreadPoolExecutor(max_workers=5) as executor: + for collection in input_collections: + executor.submit( + watch_collection, + db, + collection.strip(), + MONGO_GEO_OUTPUT_COLLECTION, + ) + + +if __name__ == "__main__": + run() diff --git a/services/addons/images/bsm_query/requirements.txt b/services/addons/images/geo_msg_query/requirements.txt similarity index 100% rename from services/addons/images/bsm_query/requirements.txt rename to services/addons/images/geo_msg_query/requirements.txt diff --git a/services/addons/images/geo_msg_query/sample.env b/services/addons/images/geo_msg_query/sample.env new file mode 100644 index 00000000..319cc884 --- /dev/null +++ b/services/addons/images/geo_msg_query/sample.env @@ -0,0 +1,9 @@ +# Mongo connection variables +MONGO_DB_URI = 'mongodb://:27017/?directConnection=true' +MONGO_DB_NAME = '' +MONGO_INPUT_COLLECTIONS = 'OdeBsmJson,OdePsmJson' +MONGO_GEO_OUTPUT_COLLECTION = '' +# TTL duration in days: +MONGO_TTL= 30 + +LOGGING_LEVEL = "INFO" \ No newline at end of file diff --git a/services/addons/images/iss_health_check/README.md b/services/addons/images/iss_health_check/README.md index 7a8f762b..b1472790 100644 --- a/services/addons/images/iss_health_check/README.md +++ b/services/addons/images/iss_health_check/README.md @@ -11,15 +11,15 @@ This directory contains a microservice that runs within the CV Manager GKE Cluster. The iss_health_checker application populates the CV Manager PostGreSQL database's 'scms_health' table with the current ISS SCMS statuses of all RSUs recorded in the 'rsus' table. These statuses are queried by this application from a provided ISS Green Hills SCMS API endpoint. -The application schedules the iss_health_checker script to run every 6 hours. A new SCMS API access key is generated every run of the script to ensure the access never expires. This is due to a limitation of the SCMS API not allowing permanent access keys. Access keys are stored in GCP Secret Manager to allow for versioning and encrypted storage. The application removes the previous access key from the SCMS API after runtime to reduce clutter of access keys on the API service account. +The application schedules the iss_health_checker script to run every 6 hours. A new SCMS API access key is generated every run of the script to ensure the access never expires. This is due to a limitation of the SCMS API not allowing permanent access keys. Access keys can be stored in GCP Secret Manager to allow for versioning and encrypted storage. The application removes the previous access key from the SCMS API after runtime to reduce clutter of access keys on the API service account. -Currently only GCP is supported to run this application due to a reliance on the GCP Secret Manager. Storing the access keys on a local volume is not recommended due to security vulnerabilities. Feel free to contribute to this application for secret manager equivalent support for other cloud environments. +Currently GCP & Postgres are the only supported storage solutions to run this application. Storing the access keys on a local volume is not recommended due to security vulnerabilities. Feel free to contribute to this application to support other storage solutions. ## Requirements To properly run the iss_health_checker microservice the following services are also required: -- GCP project and service account with GCP Secret Manager access +- GCP project and service account with GCP Secret Manager access (only required if STORAGE_TYPE is set to 'gcp') - CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table - Service agreement with ISS Green Hills to have access to the SCMS API REST service endpoint - iss_health_checker must be deployed in the same environment or K8s cluster as the PostgreSQL database @@ -27,7 +27,9 @@ To properly run the iss_health_checker microservice the following services are a The iss_health_checker microservice expects the following environment variables to be set: -- GOOGLE_APPLICATION_CREDENTIALS - file location for GCP JSON service account key. +- STORAGE_TYPE - Storage solution for the SCMS API access keys. Currently only 'gcp' & 'postgres' are supported. +- GOOGLE_APPLICATION_CREDENTIALS - File location for GCP JSON service account key. Only required if STORAGE_TYPE is set to 'gcp'. +- ISS_KEY_TABLE_NAME - Postgres table name for the ISS SCMS API access keys. Only required if STORAGE_TYPE is set to 'postgres'. - PROJECT_ID - GCP project ID. - ISS_API_KEY - Initial ISS SCMS API access key to perform the first run of the script. This access key must not expire before the first runtime. - ISS_API_KEY_NAME - Human readable reference for the access key within ISS SCMS API. Generated access keys will utilize this same name. diff --git a/services/addons/images/iss_health_check/iss_health_checker.py b/services/addons/images/iss_health_check/iss_health_checker.py index 32a69b12..ec2d5a3d 100644 --- a/services/addons/images/iss_health_check/iss_health_checker.py +++ b/services/addons/images/iss_health_check/iss_health_checker.py @@ -1,118 +1,173 @@ -from datetime import datetime -import requests -import logging -import os -import iss_token -import common.pgquery as pgquery - - -def get_rsu_data(): - result = {} - query = ( - "SELECT jsonb_build_object('rsu_id', rsu_id, 'iss_scms_id', iss_scms_id) " - "FROM public.rsus " - "WHERE iss_scms_id IS NOT NULL " - "ORDER BY rsu_id" - ) - data = pgquery.query_db(query) - - logging.debug("Parsing results...") - for point in data: - point_dict = dict(point[0]) - result[point_dict["iss_scms_id"]] = {"rsu_id": point_dict["rsu_id"]} - - return result - - -def get_scms_status_data(): - rsu_data = get_rsu_data() - - # Create GET request headers - iss_headers = {} - iss_headers["x-api-key"] = iss_token.get_token() - - # Create the GET request string - iss_base = os.environ["ISS_SCMS_VEHICLE_REST_ENDPOINT"] - project_id = os.environ["ISS_PROJECT_ID"] - page_size = 200 - page = 0 - messages_processed = 0 - - # Loop through all pages of enrolled devices - while True: - iss_request = iss_base + "?pageSize={}&page={}&project_id={}".format( - page_size, page, project_id - ) - logging.debug("GET: " + iss_request) - response = requests.get(iss_request, headers=iss_headers) - enrollment_list = response.json()["data"] - - if len(enrollment_list) == 0: - break - - # Loop through each device on current page - for enrollment_status in enrollment_list: - if enrollment_status["_id"] in rsu_data: - rsu_data[enrollment_status["_id"]][ - "provisionerCompany" - ] = enrollment_status["provisionerCompany_id"] - rsu_data[enrollment_status["_id"]]["entityType"] = enrollment_status[ - "entityType" - ] - rsu_data[enrollment_status["_id"]]["project_id"] = enrollment_status[ - "project_id" - ] - rsu_data[enrollment_status["_id"]]["deviceHealth"] = enrollment_status[ - "deviceHealth" - ] - - # If the device has yet to download its first set of certs, set the expiration time to when it was enrolled - if "authorizationCertInfo" in enrollment_status["enrollments"][0]: - rsu_data[enrollment_status["_id"]][ - "expiration" - ] = enrollment_status["enrollments"][0]["authorizationCertInfo"][ - "expireTimeOfLatestDownloadedCert" - ] - else: - rsu_data[enrollment_status["_id"]]["expiration"] = None - - messages_processed = messages_processed + 1 - - page = page + 1 - - logging.info("Processed {} messages".format(messages_processed)) - return rsu_data - - -def insert_scms_data(data): - logging.info("Inserting SCMS data into PostgreSQL...") - now_ts = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%S.000Z") - - query = ( - 'INSERT INTO public.scms_health("timestamp", health, expiration, rsu_id) VALUES' - ) - for value in data.values(): - health = "1" if value["deviceHealth"] == "Healthy" else "0" - if value["expiration"]: - query = ( - query - + f" ('{now_ts}', '{health}', '{value['expiration']}', {value['rsu_id']})," - ) - else: - query = query + f" ('{now_ts}', '{health}', NULL, {value['rsu_id']})," - - pgquery.write_db(query[:-1]) - logging.info( - "SCMS data inserted {} messages into PostgreSQL...".format(len(data.values())) - ) - - -if __name__ == "__main__": - # Configure logging based on ENV var or use default if not set - log_level = ( - "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] - ) - logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) - - scms_statuses = get_scms_status_data() - insert_scms_data(scms_statuses) +from datetime import datetime +import requests +import logging +import os +import iss_token +import common.pgquery as pgquery +from dataclasses import dataclass, field +from typing import Dict + + +# Set up logging +logger = logging.getLogger(__name__) + +@dataclass +class RsuDataWrapper: + rsu_data: Dict[str, Dict[str, str]] = field(default_factory=dict) + + def __init__(self, rsu_data): + self.rsu_data = rsu_data + + def get_dict(self): + return self.rsu_data + + def set_provisioner_company(self, scms_id, provisioner_company): + self.rsu_data[scms_id]["provisionerCompany"] = provisioner_company + + def set_entity_type(self, scms_id, entity_type): + self.rsu_data[scms_id]["entityType"] = entity_type + + def set_project_id(self, scms_id, project_id): + self.rsu_data[scms_id]["project_id"] = project_id + + def set_device_health(self, scms_id, device_health): + self.rsu_data[scms_id]["deviceHealth"] = device_health + + def set_expiration(self, scms_id, expiration): + self.rsu_data[scms_id]["expiration"] = expiration + + +def get_rsu_data() -> RsuDataWrapper: + """Get RSU data from PostgreSQL and return it in a wrapper object""" + + result = {} + query = ( + "SELECT jsonb_build_object('rsu_id', rsu_id, 'iss_scms_id', iss_scms_id) " + "FROM public.rsus " + "WHERE iss_scms_id IS NOT NULL " + "ORDER BY rsu_id" + ) + data = pgquery.query_db(query) + + logger.debug("Parsing results...") + for point in data: + point_dict = dict(point[0]) + result[point_dict["iss_scms_id"]] = {"rsu_id": point_dict["rsu_id"]} + + return RsuDataWrapper(result) + + +def get_scms_status_data(): + """Get SCMS status data from ISS and return it as a dictionary""" + + rsu_data = get_rsu_data() + + # Create GET request headers + iss_headers = {} + iss_headers["x-api-key"] = iss_token.get_token() + + # Create the GET request string + iss_base = os.environ["ISS_SCMS_VEHICLE_REST_ENDPOINT"] + project_id = os.environ["ISS_PROJECT_ID"] + page_size = 200 + page = 0 + messages_processed = 0 + + # Loop through all pages of enrolled devices + while True: + iss_request = iss_base + "?pageSize={}&page={}&project_id={}".format( + page_size, page, project_id + ) + logger.debug("GET: " + iss_request) + response = requests.get(iss_request, headers=iss_headers) + enrollment_list = response.json()["data"] + + if len(enrollment_list) == 0: + break + + # Loop through each device on current page + for enrollment_status in enrollment_list: + es_id = enrollment_status["_id"] + if es_id in rsu_data.get_dict(): + rsu_data.set_provisioner_company(es_id, enrollment_status["provisionerCompany_id"]) + rsu_data.set_entity_type(es_id, enrollment_status["entityType"]) + rsu_data.set_project_id(es_id, enrollment_status["project_id"]) + rsu_data.set_device_health(es_id, enrollment_status["deviceHealth"]) + + # If the device has yet to download its first set of certs, set the expiration time to when it was enrolled + if "authorizationCertInfo" in enrollment_status["enrollments"][0]: + rsu_data.set_expiration(es_id, enrollment_status["enrollments"][0]["authorizationCertInfo"]["expireTimeOfLatestDownloadedCert"]) + else: + rsu_data.set_expiration(es_id, None) + + messages_processed = messages_processed + 1 + + page = page + 1 + + logger.info("Processed {} messages".format(messages_processed)) + return rsu_data.get_dict() + + +def insert_scms_data(data): + logger.info("Inserting SCMS data into PostgreSQL...") + now_ts = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%S.000Z") + + query = ( + 'INSERT INTO public.scms_health("timestamp", health, expiration, rsu_id) VALUES' + ) + for value in data.values(): + if validate_scms_data(value) is False: + continue + + health = "1" if value["deviceHealth"] == "Healthy" else "0" + if value["expiration"]: + query = ( + query + + f" ('{now_ts}', '{health}', '{value['expiration']}', {value['rsu_id']})," + ) + else: + query = query + f" ('{now_ts}', '{health}', NULL, {value['rsu_id']})," + + query = query[:-1] # remove comma + pgquery.write_db(query) + logger.info( + "SCMS data inserted {} messages into PostgreSQL...".format(len(data.values())) + ) + +def validate_scms_data(value): + """Validate the SCMS data + + Args: + value (dict): SCMS data + """ + + try: + value["rsu_id"] + except KeyError as e: + logger.warning("rsu_id not found in data, is it real data? exception: {}".format(e)) + return False + + try: + value["deviceHealth"] + except KeyError as e: + logger.warning("deviceHealth not found in data for RSU with id {}, is it real data? exception: {}".format(value["rsu_id"], e)) + return False + + try: + value["expiration"] + except KeyError as e: + logger.warning("expiration not found in data for RSU with id {}, is it real data? exception: {}".format(value["rsu_id"], e)) + return False + + return True + + +if __name__ == "__main__": + # Configure logging based on ENV var or use default if not set + log_level = ( + "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] + ) + logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + + scms_statuses = get_scms_status_data() + insert_scms_data(scms_statuses) \ No newline at end of file diff --git a/services/addons/images/iss_health_check/iss_token.py b/services/addons/images/iss_health_check/iss_token.py index 084b6067..37cf9536 100644 --- a/services/addons/images/iss_health_check/iss_token.py +++ b/services/addons/images/iss_health_check/iss_token.py @@ -1,116 +1,213 @@ -from google.cloud import secretmanager -import requests -import os -import json -import uuid -import logging - - -def create_secret(client, secret_id, parent): - """Create a new GCP secret in GCP Secret Manager - client: GCP Security Manager client - secret_id: ID of the secret being created - parent: GCP secret manager parent ID for the GCP project - """ - client.create_secret( - request={ - "parent": parent, - "secret_id": secret_id, - "secret": {"replication": {"automatic": {}}}, - } - ) - logging.debug("New secret created") - - -def check_if_secret_exists(client, secret_id, parent): - """Check if a secret exists in GCP Secret Manager - client: GCP Security Manager client - secret_id: ID of the secret being checked - parent: GCP secret manager parent ID for the GCP project - """ - for secret in client.list_secrets( - request=secretmanager.ListSecretsRequest(parent=parent) - ): - # secret names are in the form of "projects/project_id/secrets/secret_id" - if secret.name.split("/")[-1] == secret_id: - logging.debug(f"Secret {secret_id} exists") - return True - return False - - -def get_latest_secret_version(client, secret_id, parent): - """Get latest value of a secret from GCP Secret Manager - client: GCP Security Manager client - secret_id: ID for the secret being retrieved - parent: GCP secret manager parent ID for the GCP project - """ - response = client.access_secret_version( - request={"name": f"{parent}/secrets/{secret_id}/versions/latest"} - ) - return json.loads(response.payload.data.decode("UTF-8")) - - -def add_secret_version(client, secret_id, parent, data): - """Add a new version to an existing secret - client: GCP Security Manager client - secret_id: ID for the secret - parent: GCP secret manager parent ID for the GCP project - data: String value for the new version of the secret - """ - client.add_secret_version( - request={ - "parent": f"{parent}/secrets/{secret_id}", - "payload": {"data": str.encode(json.dumps(data))}, - } - ) - logging.debug("New version added") - - -def get_token(): - client = secretmanager.SecretManagerServiceClient() - secret_id = "iss-token-secret" - parent = f"projects/{os.environ['PROJECT_ID']}" - - # Check to see if the GCP secret exists - secret_exists = check_if_secret_exists(client, secret_id, parent) - - if secret_exists: - # Grab the latest token data - value = get_latest_secret_version(client, secret_id, parent) - friendly_name = value["name"] - token = value["token"] - logging.debug(f"Received token: {friendly_name}") - else: - # If there is no available ISS token secret, create secret - logging.debug("Secret does not exist, creating secret") - create_secret(client, secret_id, parent) - # Use environment variable for first run with new secret - token = os.environ["ISS_API_KEY"] - - # Pull new ISS SCMS API token - iss_base = os.environ["ISS_SCMS_TOKEN_REST_ENDPOINT"] - - # Create HTTP request headers - iss_headers = {"x-api-key": token} - - # Create the POST body - new_friendly_name = f"{os.environ['ISS_API_KEY_NAME']}_{str(uuid.uuid4())}" - iss_post_body = {"friendlyName": new_friendly_name, "expireDays": 1} - - # Create new ISS SCMS API Token to ensure its freshness - logging.debug("POST: " + iss_base) - response = requests.post(iss_base, json=iss_post_body, headers=iss_headers) - new_token = response.json()["Item"] - logging.debug(f"Received new token: {new_friendly_name}") - - if secret_exists: - # If exists, delete previous API key to prevent key clutter - iss_delete_body = {"friendlyName": friendly_name} - requests.delete(iss_base, json=iss_delete_body, headers=iss_headers) - logging.debug(f"Old token has been deleted from ISS SCMS: {friendly_name}") - - version_data = {"name": new_friendly_name, "token": new_token} - - add_secret_version(client, secret_id, parent, version_data) - - return new_token +from google.cloud import secretmanager +import common.pgquery as pgquery +import requests +import os +import json +import uuid +import logging + + +# Set up logging +logger = logging.getLogger(__name__) + +# Get storage type from environment variable +def get_storage_type(): + """Get the storage type for the ISS SCMS API token + """ + try : + os.environ["STORAGE_TYPE"] + except KeyError: + logger.error("STORAGE_TYPE environment variable not set, exiting") + exit(1) + + storageTypeCaseInsensitive = os.environ["STORAGE_TYPE"].casefold() + if storageTypeCaseInsensitive == "gcp": + return "gcp" + elif storageTypeCaseInsensitive == "postgres": + return "postgres" + else: + logger.error("STORAGE_TYPE environment variable not set to a valid value, exiting") + exit(1) + + +# GCP Secret Manager functions +def create_secret(client, secret_id, parent): + """Create a new GCP secret in GCP Secret Manager + client: GCP Security Manager client + secret_id: ID of the secret being created + parent: GCP secret manager parent ID for the GCP project + """ + client.create_secret( + request={ + "parent": parent, + "secret_id": secret_id, + "secret": {"replication": {"automatic": {}}}, + } + ) + logger.debug("New secret created") + + +def check_if_secret_exists(client, secret_id, parent): + """Check if a secret exists in GCP Secret Manager + client: GCP Security Manager client + secret_id: ID of the secret being checked + parent: GCP secret manager parent ID for the GCP project + """ + for secret in client.list_secrets( + request=secretmanager.ListSecretsRequest(parent=parent) + ): + # secret names are in the form of "projects/project_id/secrets/secret_id" + if secret.name.split("/")[-1] == secret_id: + logger.debug(f"Secret {secret_id} exists") + return True + return False + + +def get_latest_secret_version(client, secret_id, parent): + """Get latest value of a secret from GCP Secret Manager + client: GCP Security Manager client + secret_id: ID for the secret being retrieved + parent: GCP secret manager parent ID for the GCP project + """ + response = client.access_secret_version( + request={"name": f"{parent}/secrets/{secret_id}/versions/latest"} + ) + return json.loads(response.payload.data.decode("UTF-8")) + + +def add_secret_version(client, secret_id, parent, data): + """Add a new version to an existing secret + client: GCP Security Manager client + secret_id: ID for the secret + parent: GCP secret manager parent ID for the GCP project + data: String value for the new version of the secret + """ + client.add_secret_version( + request={ + "parent": f"{parent}/secrets/{secret_id}", + "payload": {"data": str.encode(json.dumps(data))}, + } + ) + logger.debug("New version added") + + +# Postgres functions +def check_if_data_exists(table_name): + """Check if data exists in the table + table_name: name of the table + """ + # create the query + query = f"SELECT * FROM {table_name}" + # execute the query + data = pgquery.query_db(query) + # check if data exists + if len(data) > 0: + return True + else: + return False + + +def get_latest_data(table_name): + """Get latest value of a token from the table + table_name: name of the table + """ + # create the query + query = f"SELECT * FROM {table_name} ORDER BY iss_key_id DESC LIMIT 1" + # execute the query + data = pgquery.query_db(query) + # return the data + toReturn = {} + toReturn["id"] = data[0][0] # id + toReturn["name"] = data[0][1] # common_name + toReturn["token"] = data[0][2] # token + logger.debug(f"Received token: {toReturn['name']} with id {toReturn['id']}") + return toReturn + + +def add_data(table_name, common_name, token): + """Add a new token to the table + table_name: name of the table + data: String value for the new token + """ + # create the query + query = f"INSERT INTO {table_name} (common_name, token) VALUES ('{common_name}', '{token}')" + # execute the query + pgquery.write_db(query) + + +# Main function +def get_token(): + storage_type = get_storage_type() + if storage_type == "gcp": + client = secretmanager.SecretManagerServiceClient() + secret_id = "iss-token-secret" + parent = f"projects/{os.environ['PROJECT_ID']}" + + # Check to see if the GCP secret exists + data_exists = check_if_secret_exists(client, secret_id, parent) + + if data_exists: + # Grab the latest token data + value = get_latest_secret_version(client, secret_id, parent) + friendly_name = value["name"] + token = value["token"] + logger.debug(f"Received token: {friendly_name}") + else: + # If there is no available ISS token secret, create secret + logger.debug("Secret does not exist, creating secret") + create_secret(client, secret_id, parent) + # Use environment variable for first run with new secret + token = os.environ["ISS_API_KEY"] + elif storage_type == "postgres": + key_table_name = os.environ["ISS_KEY_TABLE_NAME"] + + # check to see if data exists in the table + data_exists = check_if_data_exists(key_table_name) + + if data_exists: + # grab the latest token data + value = get_latest_data(key_table_name) + id = value["id"] + friendly_name = value["name"] + token = value["token"] + logger.debug(f"Received token: {friendly_name} with id {id}") + else: + # if there is no data, use environment variable for first run + token = os.environ["ISS_API_KEY"] + + # Pull new ISS SCMS API token + iss_base = os.environ["ISS_SCMS_TOKEN_REST_ENDPOINT"] + + # Create HTTP request headers + iss_headers = {"x-api-key": token} + + # Create the POST body + new_friendly_name = f"{os.environ['ISS_API_KEY_NAME']}_{str(uuid.uuid4())}" + iss_post_body = {"friendlyName": new_friendly_name, "expireDays": 1} + + # Create new ISS SCMS API Token to ensure its freshness + logger.debug("POST: " + iss_base) + response = requests.post(iss_base, json=iss_post_body, headers=iss_headers) + try: + new_token = response.json()["Item"] + except requests.JSONDecodeError: + logger.error("Failed to decode JSON response from ISS SCMS API. Response: " + response.text) + exit(1) + logger.debug(f"Received new token: {new_friendly_name}") + + if data_exists: + # If exists, delete previous API key to prevent key clutter + iss_delete_body = {"friendlyName": friendly_name} + requests.delete(iss_base, json=iss_delete_body, headers=iss_headers) + logger.debug(f"Old token has been deleted from ISS SCMS: {friendly_name}") + + version_data = {"name": new_friendly_name, "token": new_token} + + if get_storage_type() == "gcp": + # Add new version to the secret + add_secret_version(client, secret_id, parent, version_data) + elif get_storage_type() == "postgres": + # add new entry to the table + add_data(key_table_name, new_friendly_name, new_token) + + return new_token diff --git a/services/addons/images/iss_health_check/sample.env b/services/addons/images/iss_health_check/sample.env index 6fc35f8c..9edad030 100644 --- a/services/addons/images/iss_health_check/sample.env +++ b/services/addons/images/iss_health_check/sample.env @@ -1,21 +1,31 @@ -# ISS Account Authentication -ISS_API_KEY= -ISS_API_KEY_NAME= -ISS_PROJECT_ID= -ISS_SCMS_TOKEN_REST_ENDPOINT= -ISS_SCMS_VEHICLE_REST_ENDPOINT= - -# PostgreSQL connection information -# Host port must be specified -PG_DB_HOST=:5432 -PG_DB_NAME= -PG_DB_USER= -PG_DB_PASS= - -# GCP Project ID and service account JSON key file location (mount as volume or secret) -PROJECT_ID= -GOOGLE_APPLICATION_CREDENTIALS= - -# Customize the logging level, defaults to INFO -# Options: DEBUG, INFO, WARN, ERROR (case sensitive) +# ISS Account Authentication +ISS_API_KEY= +ISS_API_KEY_NAME= +ISS_PROJECT_ID= +ISS_SCMS_TOKEN_REST_ENDPOINT= +ISS_SCMS_VEHICLE_REST_ENDPOINT= + +# PostgreSQL connection information +# Host port must be specified +PG_DB_HOST=:5432 +PG_DB_NAME= +PG_DB_USER= +PG_DB_PASS= + +# Key Storage +## Type of key storage, options: gcp, postgres +STORAGE_TYPE= + +## GCP Storage (Required if STORAGE_TYPE=gcp) +### GCP Project ID +PROJECT_ID= +### Service account JSON key file location (mount as volume or secret) +GOOGLE_APPLICATION_CREDENTIALS= + +## Postgres Storage (Required if STORAGE_TYPE=postgres) +### Table name to store keys +ISS_KEY_TABLE_NAME= + +# Customize the logging level, defaults to INFO +# Options: DEBUG, INFO, WARN, ERROR (case sensitive) LOGGING_LEVEL= \ No newline at end of file diff --git a/services/addons/images/rsu_ping/README.md b/services/addons/images/rsu_ping/README.md deleted file mode 100644 index 66fb4098..00000000 --- a/services/addons/images/rsu_ping/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# RSU Ping Services - -## Table of Contents - -- [RSU Ping Services](#rsu-ping-services) - - [Table of Contents](#table-of-contents) - - [About ](#about-) - - [Requirements ](#requirements-) - - [rsu_ping_fetch](#rsu_ping_fetch) - - [rsu_pinger](#rsu_pinger) - -## About - -This directory contains two microservices that run within the CV Manager GKE Cluster. Both the 'rsu_ping_fetch' and 'rsu_pinger' applications populate the CV Manager PostGreSQL database's 'ping' table with the current online statuses of all RSUs recorded in the 'rsus' table. For 'rsu_ping_fetch', these statuses are retrieved directly from a [Zabbix server](https://www.zabbix.com/). For 'rsu_pinger', these statuses are generated by pinging every RSU in the 'rsus' table every 1 minute. - -If you have access to a Zabbix server that is tracking RSUs, it is recommended to use the 'rsu_ping_fetch' application. However, the 'rsu_pinger' is an effective alternative. - -Another feature both applications provide is a ping data purger that will remove stale ping data from the CV Manager PostgreSQL database to allow for high performance RSU ping queries. The amount of time a message needs to be in the database to be considered stale is configurable with the STALE_PERIOD environment variable. This purger will run once every 24 hours to check for stale ping data in the database. - -## Requirements - -### rsu_ping_fetch - -To properly run the rsu_ping_fetch microservice the following services are also required: - -- CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table -- Zabbix server with the REST API enabled -- rsu_ping_fetch must be deployed in the same environment or K8s cluster as the PostgreSQL database -- Network rules must be in place to allow proper routing between the rsu_ping_fetch microservice and the Zabbix server - -The rsu_ping_fetch microservice expects the following environment variables to be set: - -- ZABBIX_ENDPOINT - Zabbix API access endpoint. -- ZABBIX_USER - Zabbix API access username. -- ZABBIX_PASSWORD - Zabbix API access password. -- DB_USER - PostgreSQL access username. -- DB_PASS - PostgreSQL access password. -- DB_NAME - PostgreSQL database name. -- DB_HOST - PostgreSQL hostname, make sure to include port number. -- STALE_PERIOD - Number of hours a ping log needs to be around in the PostgreSQL database to be considered stale. -- LOGGING_LEVEL (optional, defaults to 'info') - -### rsu_pinger - -To properly run the rsu_pinger microservice the following services are also required: - -- CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table -- rsu_pinger must be deployed in the same environment or K8s cluster as the PostgreSQL database -- Network rules must be in place to allow proper routing between the rsu_pinger microservice and deployed RSUs - -The rsu_pinger microservice expects the following environment variables to be set: - -- DB_USER - PostgreSQL access username. -- DB_PASS - PostgreSQL access password. -- DB_NAME - PostgreSQL database name. -- DB_HOST - PostgreSQL hostname, make sure to include port number. -- STALE_PERIOD - Number of hours a ping log needs to be around in the PostgreSQL database to be considered stale. -- LOGGING_LEVEL (optional, defaults to 'info') diff --git a/services/addons/images/rsu_ping/crontab.rsu_ping_fetch b/services/addons/images/rsu_ping/crontab.rsu_ping_fetch deleted file mode 100644 index 2056248f..00000000 --- a/services/addons/images/rsu_ping/crontab.rsu_ping_fetch +++ /dev/null @@ -1,3 +0,0 @@ -PYTHONPATH=/home -*/1 * * * * /usr/local/bin/python3 /home/rsu_ping_fetch.py -0 0 * * * /usr/local/bin/python3 /home/purger.py diff --git a/services/addons/images/rsu_ping/purger.py b/services/addons/images/rsu_ping/purger.py deleted file mode 100644 index 36d83366..00000000 --- a/services/addons/images/rsu_ping/purger.py +++ /dev/null @@ -1,66 +0,0 @@ -from datetime import datetime, timedelta -import os -import logging -import common.pgquery as pgquery - - -def get_last_online_rsu_records(): - result = [] - - query = ( - "SELECT a.ping_id, a.rsu_id, a.timestamp " - "FROM (" - "SELECT pd.ping_id, pd.rsu_id, pd.timestamp, ROW_NUMBER() OVER (PARTITION BY pd.rsu_id order by pd.timestamp DESC) AS row_id " - "FROM public.ping AS pd " - "WHERE pd.result = '1'" - ") AS a " - "WHERE a.row_id <= 1 ORDER BY rsu_id" - ) - data = pgquery.query_db(query) - - # Create list of RSU last online ping records - # Tuple in the format of (ping_id, rsu_id, timestamp (UTC)) - result = [value for value in data] - - return result - - -def purge_ping_data(stale_period): - last_online_list = get_last_online_rsu_records() - - stale_point = datetime.utcnow() - timedelta(hours=stale_period) - stale_point_str = stale_point.strftime("%Y/%m/%dT%H:%M:%S") - - for record in last_online_list: - logging.debug(f"Cleaning up rsu_id: {str(record[1])}") - # Check if the RSU has been offline longer than the stale period - if record[2] < stale_point: - logging.debug( - f"Latest record of rsu_id {str(record[1])} is a stale RSU ping record (ping_id: {str(record[0])})" - ) - # Create query to delete all records of the stale ping data besides the latest record - purge_query = ( - "DELETE FROM public.ping " - f"WHERE rsu_id = {str(record[1])} AND ping_id != {str(record[0])}" - ) - else: - # Create query to delete all records before the stale_point - purge_query = ( - "DELETE FROM public.ping " - f"WHERE rsu_id = {str(record[1])} AND timestamp < '{stale_point_str}'::timestamp" - ) - - pgquery.write_db(purge_query) - - logging.info("Ping data purging successfully completed") - - -if __name__ == "__main__": - # Configure logging based on ENV var or use default if not set - log_level = ( - "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] - ) - logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) - - stale_period = int(os.environ["STALE_PERIOD"]) - purge_ping_data(stale_period) diff --git a/services/addons/images/rsu_status_check/README.md b/services/addons/images/rsu_status_check/README.md new file mode 100644 index 00000000..85ea1f81 --- /dev/null +++ b/services/addons/images/rsu_status_check/README.md @@ -0,0 +1,85 @@ +# RSU Status Fetch services + +## Table of Contents + +- [RSU Status Fetch services](#rsu-status-fetch-services) + - [Table of Contents](#table-of-contents) + - [About ](#about-) + - [Requirements ](#requirements-) + - [rsu_ping_fetch](#rsu_ping_fetch) + - [rsu_pinger](#rsu_pinger) + - [rsu_snmp_fetch](#rsu_snmp_fetch) + +## About + +This directory contains three services that run within the CV Manager GKE Cluster as a single microservice called the 'rsu_status_check'. + +Both the 'rsu_ping_fetch' and 'rsu_pinger' services populate the CV Manager PostGreSQL database's 'ping' table with the current online statuses of all RSUs recorded in the 'rsus' table. For 'rsu_ping_fetch', these statuses are retrieved directly from a [Zabbix server](https://www.zabbix.com/). For 'rsu_pinger', these statuses are generated by pinging every RSU in the 'rsus' table every 1 minute. + +If you have access to a Zabbix server that is tracking RSUs, it is recommended to use the 'rsu_ping_fetch' service. However, the 'rsu_pinger' is an effective alternative. + +Another feature the 'rsu_status_check' application provides is a ping data purger that will remove stale ping data from the CV Manager PostgreSQL database to allow for high performance RSU ping queries. The amount of time a message needs to be in the database to be considered stale is configurable with the STALE_PERIOD environment variable. This purger will run once every 24 hours to check for stale ping data in the database. + +The 'rsu_snmp_fetch' service is used to monitor the current state of deployed RSUs' SNMP configurations. This information is stored in PostgreSQL to allow for the CV Manager to have a historic record of the last configuration a RSU was known to have. This helps with offline RSUs and to allow a consistent performance behavior for users instead of waiting on a SNMPwalk to return. This data is monitored every 4 hours. Configurations are added, removed or left intact in the PostgreSQL database based on the results of the SNMPwalks performed by the 'rsu_snmp_fetch' service. + +## Requirements + +### rsu_ping_fetch + +To properly run the rsu_ping_fetch service the following additional services are also required: + +- CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table +- Zabbix server with the REST API enabled +- rsu_status_check must be deployed in the same environment or K8s cluster as the PostgreSQL database +- Network rules must be in place to allow proper routing between the rsu_status_check microservice and the Zabbix server + +The rsu_ping_fetch service expects the following environment variables to be set: + +- RSU_PING = True +- ZABBIX = True +- ZABBIX_ENDPOINT - Zabbix API access endpoint. +- ZABBIX_USER - Zabbix API access username. +- ZABBIX_PASSWORD - Zabbix API access password. +- DB_USER - PostgreSQL access username. +- DB_PASS - PostgreSQL access password. +- DB_NAME - PostgreSQL database name. +- DB_HOST - PostgreSQL hostname, make sure to include port number. +- STALE_PERIOD - Number of hours a ping log needs to be around in the PostgreSQL database to be considered stale. +- LOGGING_LEVEL (optional, defaults to 'info') + +### rsu_pinger + +To properly run the rsu_pinger service the following additional services are also required: + +- CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table +- rsu_status_check must be deployed in the same environment or K8s cluster as the PostgreSQL database +- Network rules must be in place to allow proper routing between the rsu_status_check microservice and deployed RSUs + +The rsu_pinger service expects the following environment variables to be set: + +- RSU_PING = True +- ZABBIX = False +- DB_USER - PostgreSQL access username. +- DB_PASS - PostgreSQL access password. +- DB_NAME - PostgreSQL database name. +- DB_HOST - PostgreSQL hostname, make sure to include port number. +- STALE_PERIOD - Number of hours a ping log needs to be around in the PostgreSQL database to be considered stale. +- LOGGING_LEVEL (optional, defaults to 'info') + +### rsu_snmp_fetch + +To properly run the rsu_snmp_fetch service the following additional services are also required: + +- CV Manager PostgreSQL database with at least one RSU inserted into the 'rsus' table +- rsu_status_check must be deployed in the same environment or K8s cluster as the PostgreSQL database +- Network rules must be in place to allow proper routing between the rsu_status_check microservice and deployed RSUs + +The rsu_snmp_fetch service expects the following environment variables to be set: + +- RSU_SNMP_FETCH = True +- DB_USER - PostgreSQL access username. +- DB_PASS - PostgreSQL access password. +- DB_NAME - PostgreSQL database name. +- DB_HOST - PostgreSQL hostname, make sure to include port number. +- STALE_PERIOD - Number of hours a ping log needs to be around in the PostgreSQL database to be considered stale. +- LOGGING_LEVEL (optional, defaults to 'info') diff --git a/services/addons/images/rsu_ping/crontab.rsu_pinger b/services/addons/images/rsu_status_check/crontab similarity index 50% rename from services/addons/images/rsu_ping/crontab.rsu_pinger rename to services/addons/images/rsu_status_check/crontab index ac4257a4..41ec4d6b 100644 --- a/services/addons/images/rsu_ping/crontab.rsu_pinger +++ b/services/addons/images/rsu_status_check/crontab @@ -1,3 +1,5 @@ -PYTHONPATH=/home -* * * * * /usr/local/bin/python3 /home/rsu_pinger.py -0 0 * * * /usr/local/bin/python3 /home/purger.py +PYTHONPATH=/home +* * * * * /usr/local/bin/python3 /home/rsu_ping_fetch.py +* * * * * /usr/local/bin/python3 /home/rsu_pinger.py +0 0 * * * /usr/local/bin/python3 /home/purger.py +0 */4 * * * /usr/local/bin/python3 /home/rsu_snmp_fetch.py diff --git a/services/addons/images/rsu_status_check/purger.py b/services/addons/images/rsu_status_check/purger.py new file mode 100644 index 00000000..7b1ec0a3 --- /dev/null +++ b/services/addons/images/rsu_status_check/purger.py @@ -0,0 +1,99 @@ +from datetime import datetime, timedelta, timezone +import os +import logging +import common.pgquery as pgquery + + +def get_all_rsus(): + query = "SELECT to_jsonb(row) FROM (SELECT rsu_id FROM public.rsus) AS row ORDER BY rsu_id" + data = pgquery.query_db(query) + + rsu_obj = {} + for row in data: + row = dict(row[0]) + rsu_obj[row["rsu_id"]] = None + + return rsu_obj + + +def get_last_online_rsu_records(rsu_dict): + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT a.ping_id, a.rsu_id, a.timestamp " + "FROM (" + "SELECT pd.ping_id, pd.rsu_id, pd.timestamp, ROW_NUMBER() OVER (PARTITION BY pd.rsu_id order by pd.timestamp DESC) AS row_id " + "FROM public.ping AS pd " + "WHERE pd.result = '1'" + ") AS a " + "WHERE a.row_id <= 1 ORDER BY rsu_id" + ") as row" + ) + data = pgquery.query_db(query) + + # Create list of RSU last online ping records + # Tuple in the format of (ping_id, rsu_id, timestamp (UTC)) + for row in data: + row = dict(row[0]) + + if row["rsu_id"] not in rsu_dict: + # If there is ping data for a RSU not in the PostgreSQL 'rsus' table, it is most likely old and no longer tracked + # This will allow for all of the stale ping data to be removed + rsu_dict[row["rsu_id"]] = None + else: + rsu_dict[row["rsu_id"]] = { + "ping_id": row["ping_id"], + "timestamp": datetime.strptime(row["timestamp"], "%Y-%m-%dT%H:%M:%S"), + } + return rsu_dict + + +def purge_ping_data(stale_period): + rsu_dict = get_all_rsus() + online_rsu_dict = get_last_online_rsu_records(rsu_dict) + + stale_point = datetime.now() - timedelta(hours=stale_period) + stale_point_str = stale_point.strftime("%Y-%m-%dT%H:%M:%S") + + logging.info(f"Purging all ping data before {stale_point_str}") + + for key, value in online_rsu_dict.items(): + logging.debug(f"Cleaning up rsu_id: {str(key)}") + + purge_query = "" + if value is not None: + if value["timestamp"] < stale_point: + # Create query to delete all records of the stale ping data besides the latest record + purge_query = ( + "DELETE FROM public.ping " + f"WHERE rsu_id = {str(key)} AND ping_id != {str(value["ping_id"])}" + ) + + # If the RSU is no longer tracked or the last ping was within the last 24 hours, purge all data beyond the stale point + if purge_query == "": + # Create query to delete all records before the stale_point + purge_query = ( + "DELETE FROM public.ping " + f"WHERE rsu_id = {str(key)} AND timestamp < '{stale_point_str}'::timestamp" + ) + + pgquery.write_db(purge_query) + + logging.info("Ping data purging successfully completed") + + +if __name__ == "__main__": + # Configure logging based on ENV var or use default if not set + log_level = os.environ.get("LOGGING_LEVEL", "INFO") + logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + + run_service = ( + os.environ.get("RSU_PING", "False").lower() == "true" + or os.environ.get("ZABBIX", "False").lower() == "true" + ) + if not run_service: + logging.info("The purger service is disabled and will not run") + exit() + + stale_period = int(os.environ["STALE_PERIOD"]) + purge_ping_data(stale_period) diff --git a/services/addons/images/rsu_ping/requirements.txt b/services/addons/images/rsu_status_check/requirements.txt similarity index 100% rename from services/addons/images/rsu_ping/requirements.txt rename to services/addons/images/rsu_status_check/requirements.txt diff --git a/services/addons/images/rsu_ping/rsu_ping_fetch.py b/services/addons/images/rsu_status_check/rsu_ping_fetch.py similarity index 93% rename from services/addons/images/rsu_ping/rsu_ping_fetch.py rename to services/addons/images/rsu_status_check/rsu_ping_fetch.py index f9c4da77..5393e25f 100644 --- a/services/addons/images/rsu_ping/rsu_ping_fetch.py +++ b/services/addons/images/rsu_status_check/rsu_ping_fetch.py @@ -145,10 +145,17 @@ def run(self): if __name__ == "__main__": # Configure logging based on ENV var or use default if not set - log_level = ( - "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"] - ) + log_level = os.environ.get("LOGGING_LEVEL", "INFO") + log_level = "INFO" if log_level == "" else log_level logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + run_service = ( + os.environ.get("RSU_PING", "False").lower() == "true" + and os.environ.get("ZABBIX", "False").lower() == "true" + ) + if not run_service: + logging.info("The rsu-ping-fetch service is disabled and will not run") + exit() + rsf = RsuStatusFetch() rsf.run() diff --git a/services/addons/images/rsu_ping/rsu_pinger.py b/services/addons/images/rsu_status_check/rsu_pinger.py similarity index 85% rename from services/addons/images/rsu_ping/rsu_pinger.py rename to services/addons/images/rsu_status_check/rsu_pinger.py index 96621676..76c839cc 100644 --- a/services/addons/images/rsu_ping/rsu_pinger.py +++ b/services/addons/images/rsu_status_check/rsu_pinger.py @@ -85,6 +85,15 @@ def run_rsu_pinger(): if __name__ == "__main__": # Configure logging based on ENV var or use default if not set log_level = os.environ.get("LOGGING_LEVEL", "INFO") + log_level = "INFO" if log_level == "" else log_level logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + run_service = ( + os.environ.get("RSU_PING", "False").lower() == "true" + and os.environ.get("ZABBIX", "False").lower() == "false" + ) + if not run_service: + logging.info("The rsu-pinger service is disabled and will not run") + exit() + run_rsu_pinger() diff --git a/services/addons/images/rsu_status_check/rsu_snmp_fetch.py b/services/addons/images/rsu_status_check/rsu_snmp_fetch.py new file mode 100644 index 00000000..0412b559 --- /dev/null +++ b/services/addons/images/rsu_status_check/rsu_snmp_fetch.py @@ -0,0 +1,41 @@ +import os +import logging +import common.pgquery as pgquery +import common.update_rsu_snmp_pg as update_rsu_snmp_pg + + +def get_rsu_list(): + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rd.rsu_id, rd.ipv4_address, snmp.username AS snmp_username, snmp.password AS snmp_password, sver.version_code AS snmp_version " + "FROM public.rsus AS rd " + "LEFT JOIN public.snmp_credentials AS snmp ON snmp.snmp_credential_id = rd.snmp_credential_id " + "LEFT JOIN public.snmp_versions AS sver ON sver.snmp_version_id = rd.snmp_version_id" + ") as row" + ) + + # Query PostgreSQL for the list of RSU IPs with SNMP credentials and version + data = pgquery.query_db(query) + + rsu_list = [] + for row in data: + rsu_list.append(dict(row[0])) + + return rsu_list + + +if __name__ == "__main__": + # Configure logging based on ENV var or use default if not set + log_level = os.environ.get("LOGGING_LEVEL", "INFO") + log_level = "INFO" if log_level == "" else log_level + logging.basicConfig(format="%(levelname)s:%(message)s", level=log_level) + + run_service = os.environ.get("RSU_SNMP_FETCH", "False").lower() == "true" + if not run_service: + logging.info("The rsu-snmp-fetch service is disabled and will not run") + exit() + + rsu_list = get_rsu_list() + configs = update_rsu_snmp_pg.get_snmp_configs(rsu_list) + update_rsu_snmp_pg.update_postgresql(configs) diff --git a/services/addons/images/rsu_ping/sample.env b/services/addons/images/rsu_status_check/sample.env similarity index 54% rename from services/addons/images/rsu_ping/sample.env rename to services/addons/images/rsu_status_check/sample.env index d9c1b9e0..10efd2e7 100644 --- a/services/addons/images/rsu_ping/sample.env +++ b/services/addons/images/rsu_status_check/sample.env @@ -1,8 +1,16 @@ -# Zabbix endpoint and API authentication -# Only used for rsu_ping_fetch -ZABBIX_ENDPOINT= -ZABBIX_USER= -ZABBIX_PASSWORD= +# Services that can be toggled on or off +# 'True' or 'False' are the only legal values + +# Toggles monitoring of RSU online status +RSU_PING=True + +# Fetches ping data from Zabbix - alternatively the service will ping the RSUs on its own +# Only used when RSU_PING is 'True' +ZABBIX=False + +# Fetches SNMP configuration data for all RSUs +RSU_SNMP_FETCH=True + # PostgreSQL connection information # Host port must be specified @@ -11,6 +19,12 @@ PG_DB_NAME= PG_DB_USER= PG_DB_PASS= +# Zabbix endpoint and API authentication +# Only used when ZABBIX is 'True' +ZABBIX_ENDPOINT= +ZABBIX_USER= +ZABBIX_PASSWORD= + # Customize the period at which the purger will determine a ping log is too old and will be deleted # Number of hours STALE_PERIOD=24 diff --git a/services/addons/tests/bsm_query/test_bsm_query.py b/services/addons/tests/bsm_query/test_bsm_query.py deleted file mode 100644 index 53feca93..00000000 --- a/services/addons/tests/bsm_query/test_bsm_query.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -from pymongo import MongoClient -from datetime import datetime -from unittest.mock import MagicMock, patch - -import pytest -from addons.images.bsm_query import bsm_query - -from addons.images.bsm_query.bsm_query import create_message, process_message, run - - -@pytest.fixture -def mock_mongo_client(): - mock_client = MagicMock(spec=MongoClient) - mock_db = MagicMock() - mock_collection = MagicMock() - mock_client.__getitem__.return_value = mock_db - mock_db.__getitem__.return_value = mock_collection - return mock_client - - -def test_create_message(): - original_message = { - "payload": { - "data": {"coreData": {"position": {"longitude": 123.45, "latitude": 67.89}}} - }, - "metadata": { - "originIp": "127.0.0.1", - "odeReceivedAt": "2022-01-01T12:00:00.000Z", - }, - } - - expected_result = { - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [123.45, 67.89]}, - "properties": {"id": "127.0.0.1", "timestamp": datetime(2022, 1, 1, 12, 0, 0)}, - } - - assert create_message(original_message) == expected_result - - -def test_process_message(mock_mongo_client): - message = { - "payload": { - "data": {"coreData": {"position": {"longitude": 123.45, "latitude": 67.89}}} - }, - "metadata": { - "originIp": "127.0.0.1", - "odeReceivedAt": "2022-01-01T12:00:00.000Z", - }, - } - collection_name = "test_collection" - - process_message(message, mock_mongo_client, collection_name) - - mock_collection = mock_mongo_client.__getitem__.return_value - mock_collection.insert_one.assert_called_once_with( - { - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [123.45, 67.89]}, - "properties": { - "id": "127.0.0.1", - "timestamp": datetime(2022, 1, 1, 12, 0, 0), - }, - } - ) - - -@patch.dict( - os.environ, - { - "MONGO_DB_URI": "mongodb://localhost:27017", - "MONGO_DB_NAME": "test_db", - "MONGO_BSM_INPUT_COLLECTION": "bsm_input", - "MONGO_GEO_OUTPUT_COLLECTION": "geo_output", - }, -) -@patch("addons.images.bsm_query.bsm_query.ThreadPoolExecutor") -def test_run(mock_thread_pool_executor, mock_mongo_client): - mock_collection = mock_mongo_client.__getitem__.return_value - bsm_query.set_mongo_client = MagicMock( - return_value=[mock_mongo_client, mock_collection] - ) - - mock_stream = MagicMock() - - mock_stream.return_value = "hi" - - mock_stream.__iter__.return_value = [ - {"fullDocument": "document1"}, - {"fullDocument": "document2"}, - {"fullDocument": "document3"}, - ] - - mock_collection.watch.return_value.__enter__.return_value = mock_stream - - bsm_query.run() - - mock_thread_pool_executor.assert_called_once_with(max_workers=5) - - -if __name__ == "__main__": - pytest.main() diff --git a/services/addons/tests/count_metric/test_count_metric_driver.py b/services/addons/tests/count_metric/test_count_metric_driver.py deleted file mode 100644 index af0923a2..00000000 --- a/services/addons/tests/count_metric/test_count_metric_driver.py +++ /dev/null @@ -1,102 +0,0 @@ -from os import environ -from addons.images.count_metric import driver -from mock import MagicMock -from unittest.mock import patch - - -@patch("addons.images.count_metric.driver.pgquery.query_db") -def test_get_rsu_list(mock_query_db): - # mock - mock_query_db.return_value = [ - ( - { - "ipv4_address": "192.168.0.10", - "primary_route": "I-80", - }, - ), - ] - - # run - result = driver.get_rsu_list() - - expected_result = [{"ipv4_address": "192.168.0.10", "primary_route": "I-80"}] - mock_query_db.assert_called_once() - assert result == expected_result - - -@patch("addons.images.count_metric.driver.get_rsu_list") -def test_populateRsuDict_success(mock_get_rsu_list): - # prepare - mock_get_rsu_list.return_value = [ - {"ipv4_address": "192.168.0.10", "primary_route": "I-80"} - ] - - # call - driver.populateRsuDict() - - # check that rsu_location_dict is correct - rsu_location_dict = driver.rsu_location_dict - expected_rsu_location_dict = {"192.168.0.10": "I-80"} - assert rsu_location_dict == expected_rsu_location_dict - - # check that rsu_count_dict is correct - rsu_count_dict = driver.rsu_count_dict - expected_rsu_count_dict = {"I-80": {"192.168.0.10": 0}, "Unknown": {}} - assert rsu_count_dict == expected_rsu_count_dict - - -@patch("addons.images.count_metric.driver.get_rsu_list") -def test_populateRsuDict_empty_object(mock_get_rsu_list): - # prepare - mock_get_rsu_list.return_value = [] - - driver.rsu_location_dict = {} - driver.rsu_count_dict = {} - - driver.populateRsuDict() - - assert driver.rsu_location_dict == {} - assert driver.rsu_count_dict == {"Unknown": {}} - - -@patch("addons.images.count_metric.driver.rsu_location_dict", {}) -@patch("addons.images.count_metric.driver.rsu_count_dict", {}) -@patch("addons.images.count_metric.driver.populateRsuDict", MagicMock()) -@patch("addons.images.count_metric.driver.KafkaMessageCounter") -def test_run_success(mock_KafkaMessageCounter): - # prepare - mock_KafkaMessageCounter.return_value = MagicMock() - mock_KafkaMessageCounter.return_value.run = MagicMock() - environ["MESSAGE_TYPES"] = "bsm" - - # call - driver.run() - - # check - driver.populateRsuDict.assert_called_once() - driver.KafkaMessageCounter.assert_called() - - -def test_run_message_types_not_set(): - # prepare - environ["MESSAGE_TYPES"] = "" - driver.rsu_location_dict = {} - driver.rsu_count_dict = {} - driver.logging = MagicMock() - driver.logging.error = MagicMock() - driver.exit = MagicMock() - driver.exit.side_effect = SystemExit - - # call - try: - driver.run() - except SystemExit: - pass - - # check - driver.logging.error.assert_called_once_with( - "MESSAGE_TYPES environment variable not set! Exiting." - ) - driver.exit.assert_called_once_with( - "MESSAGE_TYPES environment variable not set! Exiting." - ) diff --git a/services/addons/tests/count_metric/test_daily_emailer.py b/services/addons/tests/count_metric/test_daily_emailer.py new file mode 100644 index 00000000..5de38639 --- /dev/null +++ b/services/addons/tests/count_metric/test_daily_emailer.py @@ -0,0 +1,247 @@ +import os +from datetime import datetime, timedelta +from mock import MagicMock, patch +from addons.images.count_metric import daily_emailer + + +def test_query_mongo_in_counts(): + # prepare mocks and known variables + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.side_effect = mock_collection + mock_collection().aggregate.return_value = [ + {"_id": "10.0.0.1", "count": 5}, + {"_id": "10.0.0.2", "count": 25}, + ] + + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": {"BSM": {"in": 0, "out": 0}}, + } + } + + daily_emailer.message_types = ["BSM"] + + # run the command + daily_emailer.query_mongo_in_counts(rsu_dict, start_dt, end_dt, mock_db) + + # make assertions + mock_collection().aggregate.assert_called_once_with( + [ + { + "$match": { + "recordGeneratedAt": { + "$gte": start_dt, + "$lt": end_dt, + } + } + }, + { + "$group": { + "_id": "$metadata.originIp", + "count": {"$sum": 1}, + } + }, + ] + ) + assert rsu_dict["10.0.0.1"]["counts"]["BSM"]["in"] == 5 + assert rsu_dict["10.0.0.2"]["counts"]["BSM"]["in"] == 25 + assert rsu_dict["10.0.0.2"]["primary_route"] == "Unknown" + + daily_emailer.message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +def test_query_mongo_in_counts_no_id(): + # prepare mocks and known variables + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.side_effect = mock_collection + mock_collection().aggregate.return_value = [{"_id": None, "count": 5}] + + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": {"BSM": {"in": 0, "out": 0}}, + } + } + + daily_emailer.message_types = ["BSM"] + + # run the command + daily_emailer.query_mongo_in_counts(rsu_dict, start_dt, end_dt, mock_db) + + # make assertions + assert rsu_dict["10.0.0.1"]["counts"]["BSM"]["in"] == 0 + + daily_emailer.message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +def test_query_mongo_out_counts(): + # prepare mocks and known variables + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.side_effect = mock_collection + mock_collection().aggregate.return_value = [ + {"_id": "10.0.0.1", "count": 5}, + {"_id": "10.0.0.2", "count": 25}, + ] + + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": {"BSM": {"in": 0, "out": 0}}, + } + } + + daily_emailer.message_types = ["BSM"] + + # run the command + daily_emailer.query_mongo_out_counts(rsu_dict, start_dt, end_dt, mock_db) + + # make assertions + mock_collection().aggregate.assert_called_once_with( + [ + { + "$match": { + "recordGeneratedAt": { + "$gte": start_dt, + "$lt": end_dt, + } + } + }, + { + "$group": { + "_id": f"$metadata.originIp", + "count": {"$sum": 1}, + } + }, + ] + ) + assert rsu_dict["10.0.0.1"]["counts"]["BSM"]["out"] == 5 + assert rsu_dict["10.0.0.2"]["counts"]["BSM"]["out"] == 25 + assert rsu_dict["10.0.0.2"]["primary_route"] == "Unknown" + + daily_emailer.message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +def test_query_mongo_out_counts_no_id(): + # prepare mocks and known variables + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.side_effect = mock_collection + mock_collection().aggregate.return_value = [{"_id": None, "count": 5}] + + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": {"BSM": {"in": 0, "out": 0}}, + } + } + + daily_emailer.message_types = ["BSM"] + + # run the command + daily_emailer.query_mongo_out_counts(rsu_dict, start_dt, end_dt, mock_db) + + # make assertions + assert rsu_dict["10.0.0.1"]["counts"]["BSM"]["out"] == 0 + + daily_emailer.message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +@patch("addons.images.count_metric.daily_emailer.pgquery.query_db") +def test_prepare_rsu_dict(mock_query_db): + mock_query_db.return_value = [ + ({"ipv4_address": "10.0.0.1", "primary_route": "Route 1"},), + ] + daily_emailer.message_types = ["BSM"] + + # run + result = daily_emailer.prepare_rsu_dict() + + expected_result = { + "10.0.0.1": {"primary_route": "Route 1", "counts": {"BSM": {"in": 0, "out": 0}}} + } + mock_query_db.assert_called_once() + assert result == expected_result + + daily_emailer.message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +@patch.dict( + os.environ, + { + "DEPLOYMENT_TITLE": "Test", + "SMTP_SERVER_IP": "10.0.0.1", + "SMTP_USERNAME": "username", + "SMTP_PASSWORD": "password", + "SMTP_EMAIL": "test@gmail.com", + "SMTP_EMAIL_RECIPIENTS": "bob@gmail.com", + }, +) +@patch("addons.images.count_metric.daily_emailer.EmailSender") +def test_email_daily_counts(mock_emailsender): + emailsender_obj = mock_emailsender.return_value + + daily_emailer.email_daily_counts("test") + + emailsender_obj.send.assert_called_once_with( + sender="test@gmail.com", + recipient="bob@gmail.com", + subject="TEST Counts", + message="test", + replyEmail="", + username="username", + password="password", + pretty=True, + ) + + +@patch.dict( + os.environ, + { + "MONGO_DB_URI": "mongo-uri", + "MONGO_DB_NAME": "test_db", + }, +) +@patch("addons.images.count_metric.daily_emailer.MongoClient", MagicMock()) +@patch("addons.images.count_metric.daily_emailer.gen_email.generate_email_body") +@patch("addons.images.count_metric.daily_emailer.email_daily_counts") +@patch("addons.images.count_metric.daily_emailer.query_mongo_out_counts") +@patch("addons.images.count_metric.daily_emailer.query_mongo_in_counts") +@patch("addons.images.count_metric.daily_emailer.prepare_rsu_dict") +def test_run_daily_emailer( + mock_prepare_rsu_dict, + mock_query_mongo_in_counts, + mock_query_mongo_out_counts, + mock_email_daily_counts, + mock_gen_email, +): + daily_emailer.run_daily_emailer() + + mock_prepare_rsu_dict.assert_called_once() + mock_query_mongo_in_counts.assert_called_once() + mock_query_mongo_out_counts.assert_called_once() + mock_email_daily_counts.assert_called_once() + mock_gen_email.assert_called_once() diff --git a/services/addons/tests/count_metric/test_gen_email.py b/services/addons/tests/count_metric/test_gen_email.py new file mode 100644 index 00000000..dd0eb150 --- /dev/null +++ b/services/addons/tests/count_metric/test_gen_email.py @@ -0,0 +1,158 @@ +import os +from datetime import datetime, timedelta +from mock import MagicMock, patch +from addons.images.count_metric import gen_email + +message_types = ["BSM", "TIM", "Map", "SPaT", "SRM", "SSM"] + + +def test_diff_to_color(): + result = gen_email.diff_to_color(2) + assert result == "#a4ffa1" + + result = gen_email.diff_to_color(7) + assert result == "#ff7373" + + +def test_generate_table_header(): + result = gen_email.generate_table_header(message_types) + + expected = ( + "\n" + '\n' + 'RSU\n' + 'Road\n' + 'BSM In\n' + 'BSM Out\n' + 'TIM In\n' + 'TIM Out\n' + 'Map In\n' + 'Map Out\n' + 'SPaT In\n' + 'SPaT Out\n' + 'SRM In\n' + 'SRM Out\n' + 'SSM In\n' + 'SSM Out\n' + "\n\n" + ) + + assert result == expected + + +def test_generate_table_row(): + rsu_ip = "10.0.0.1" + data = { + "primary_route": "Route 1", + "counts": { + "BSM": {"in": 0, "out": 0, "diff_percent": 0}, + "TIM": {"in": 1, "out": 1, "diff_percent": 0}, + "Map": {"in": 2, "out": 2, "diff_percent": 0}, + "SPaT": {"in": 0, "out": 0, "diff_percent": 0}, + "SRM": {"in": 2, "out": 2, "diff_percent": 0}, + "SSM": {"in": 0, "out": 0, "diff_percent": 0}, + }, + } + row_style = "text-align: center;" + + result = gen_email.generate_table_row(rsu_ip, data, row_style, message_types) + + expected = ( + '\n' + "10.0.0.1\n" + "Route 1\n" + "0\n" + '0\n' + "1\n" + '1\n' + "2\n" + '2\n' + "0\n" + '0\n' + "2\n" + '2\n' + "0\n" + '0\n' + "\n" + ) + + assert result == expected + + +@patch("addons.images.count_metric.gen_email.generate_table_header") +@patch("addons.images.count_metric.gen_email.generate_table_row") +def test_generate_count_table(mock_gen_table_row, mock_gen_table_header): + mock_gen_table_header.return_value = "" + mock_gen_table_row.return_value = "" + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": { + "BSM": {"in": 0, "out": 0, "diff_percent": 0}, + "TIM": {"in": 1, "out": 1, "diff_percent": 0}, + "Map": {"in": 2, "out": 2, "diff_percent": 0}, + "SPaT": {"in": 0, "out": 0, "diff_percent": 0}, + "SRM": {"in": 2, "out": 2, "diff_percent": 0}, + "SSM": {"in": 0, "out": 0, "diff_percent": 0}, + }, + } + } + + result = gen_email.generate_count_table(rsu_dict, message_types) + + expected = '\n\n\n
' + + assert result == expected + + +def test_generate_count_table_empty(): + rsu_dict = {} + + result = gen_email.generate_count_table(rsu_dict, message_types) + + assert result == "" + + +@patch.dict( + os.environ, + {"DEPLOYMENT_TITLE": "Test"}, +) +@patch("addons.images.count_metric.gen_email.generate_count_table") +def test_generate_email_body(mock_generate_count_table): + mock_generate_count_table.return_value = "" + rsu_dict = { + "10.0.0.1": { + "primary_route": "Route 1", + "counts": { + "BSM": {"in": 0, "out": 0, "diff_percent": 0}, + "TIM": {"in": 1, "out": 1, "diff_percent": 0}, + "Map": {"in": 2, "out": 2, "diff_percent": 0}, + "SPaT": {"in": 0, "out": 0, "diff_percent": 0}, + "SRM": {"in": 2, "out": 2, "diff_percent": 0}, + "SSM": {"in": 0, "out": 0, "diff_percent": 0}, + }, + } + } + start_dt = (datetime.now() - timedelta(1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = (datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) + + result = gen_email.generate_email_body(rsu_dict, start_dt, end_dt, message_types) + + expected_start_string = datetime.strftime(start_dt, "%Y-%m-%d 00:00:00") + expected_end_string = datetime.strftime(end_dt, "%Y-%m-%d 00:00:00") + expected = ( + f"

TEST Count Report {expected_start_string} UTC - {expected_end_string} UTC

" + "

This is an automated email to report yesterday's ODE message counts for J2735 messages going in and out of the ODE. " + "In counts are the number of encoded messages received by the ODE from the load balancer. " + "Out counts are the number of decoded messages that have come out of the ODE in JSON form and " + "are available for querying in mongoDB. Ideally, these two counts should be identical. " + "Although, some deviation is expected due to count recording timings. Outbound counts exceeding " + "5% deviation with their corresponding inbound counts will be marked red. Outbound counts within the 5% deviation will be marked " + "green. Map and TIM Out counts are deduplicated so these are going to be lower at 1 per hour. The deviation is normalized with this in mind. " + 'Any RSUs with a road name of "Unknown" are not recorded in the PostgreSQL database and might need to be added.

' + "

RSU Message Counts

" + ) + + assert result == expected diff --git a/services/addons/tests/count_metric/test_kafka_counter.py b/services/addons/tests/count_metric/test_kafka_counter.py deleted file mode 100644 index 8db99b79..00000000 --- a/services/addons/tests/count_metric/test_kafka_counter.py +++ /dev/null @@ -1,464 +0,0 @@ -import os -import pytest -from mock import call, MagicMock, patch -from confluent_kafka import KafkaError, KafkaException -from addons.images.count_metric import kafka_counter - - -def createKafkaMessageCounter(type: int): - kafka_counter.bigquery.Client = MagicMock() - kafka_counter.bigquery.Client.return_value = MagicMock() - kafka_counter.bigquery.Client.return_value.query = MagicMock() - kafka_counter.bigquery.Client.return_value.query.return_value.result = MagicMock() - kafka_counter.bigquery.Client.return_value.query.return_value.result.return_value.total_rows = ( - 1 - ) - kafka_counter.pymongo.MongoClient = MagicMock() - kafka_counter.pymongo.MongoClient.return_value = MagicMock() - kafka_counter.bigquery.Client.__getitem__.return_value.__getitem__.return_value = ( - MagicMock() - ) - kafka_counter.bigquery.Client.__getitem__.return_value.__getitem__.return_value.insert_many.return_value = ( - MagicMock() - ) - thread_id = 0 - message_type = "bsm" - rsu_location_dict = {"noIP": "Unknown"} - rsu_count_dict = {"Unknown": {"noIP": 1}} - rsu_count_dict_zero = {"Unknown": {"noIP": 0}} - newKafkaMessageCounter = kafka_counter.KafkaMessageCounter( - thread_id, - message_type, - rsu_location_dict, - rsu_count_dict, - rsu_count_dict_zero, - type, - ) - - return newKafkaMessageCounter - - -def test_write_bq_with_type0_kmc_success(): - # prepare - os.environ["DESTINATION_DB"] = "BIGQUERY" - os.environ["KAFKA_BIGQUERY_TABLENAME"] = "test" - kafkaMessageCounterType0 = createKafkaMessageCounter(0) - kafka_counter.logging = MagicMock() - kafka_counter.logging.info = MagicMock() - - # call - query_values = "test" - kafkaMessageCounterType0.write_bigquery(query_values) - - # check - targetTable = os.getenv("KAFKA_BIGQUERY_TABLENAME") - expectedArgument = f"INSERT INTO `{targetTable}`(RSU, Road, Date, Type, Count) VALUES {query_values}" - kafkaMessageCounterType0.bq_client.query.assert_called_once_with(expectedArgument) - kafkaMessageCounterType0.bq_client.query.return_value.result.assert_called_once() - kafka_counter.logging.info.assert_called_once() - - -def test_write_bq_with_type1_kmc_success(): - # prepare - os.environ["DESTINATION_DB"] = "BIGQUERY" - os.environ["PUBSUB_BIGQUERY_TABLENAME"] = "test" - kafkaMessageCounterType1 = createKafkaMessageCounter(1) - kafka_counter.logging = MagicMock() - kafka_counter.logging.info = MagicMock() - - # call - query_values = "test" - kafkaMessageCounterType1.write_bigquery(query_values) - - # check - targetTable = os.getenv("PUBSUB_BIGQUERY_TABLENAME") - expectedArgument = f"INSERT INTO `{targetTable}`(RSU, Road, Date, Type, Count) VALUES {query_values}" - kafkaMessageCounterType1.bq_client.query.assert_called_once_with(expectedArgument) - kafkaMessageCounterType1.bq_client.query.return_value.result.assert_called_once() - kafka_counter.logging.info.assert_called_once() - - -def test_push_metrics_bq_success(): - os.environ["DESTINATION_DB"] = "BIGQUERY" - os.environ["PUBSUB_BIGQUERY_TABLENAME"] = "test" - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafkaMessageCounter.write_bigquery = MagicMock() - - # call - kafkaMessageCounter.push_metrics() - - # check - kafkaMessageCounter.write_bigquery.assert_called_once() - - -def test_push_metrics_bq_exception(): - os.environ["DESTINATION_DB"] = "BIGQUERY" - os.environ["PUBSUB_BIGQUERY_TABLENAME"] = "test" - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafkaMessageCounter.write_bigquery = MagicMock() - kafkaMessageCounter.write_bigquery.side_effect = Exception("test") - kafka_counter.logging = MagicMock() - kafka_counter.logging.error = MagicMock() - - # call - kafkaMessageCounter.push_metrics() - - # check - kafkaMessageCounter.write_bigquery.assert_called_once() - kafka_counter.logging.error.assert_called_once() - - -def test_write_mongo_with_type0_kmc_success(): - # prepare - os.environ["DESTINATION_DB"] = "MONGODB" - os.environ["MONGO_DB_URI"] = "URI" - os.environ["INPUT_COUNTS_MONGO_COLLECTION_NAME"] = "test_input" - kafkaMessageCounterType0 = createKafkaMessageCounter(0) - kafka_counter.logging = MagicMock() - kafka_counter.logging.info = MagicMock() - - # call - test_doc = {"test": "doc"} - kafkaMessageCounterType0.write_mongo(test_doc) - - # check - kafkaMessageCounterType0.mongo_client[os.getenv("MONGO_DB_NAME")][ - os.getenv("INPUT_COUNTS_MONGO_COLLECTION_NAME") - ].insert_many.assert_called_once_with(test_doc) - kafka_counter.logging.info.assert_called_once() - - -def test_write_mongo_with_type1_kmc_success(): - # prepare - os.environ["DESTINATION_DB"] = "MONGODB" - os.environ["MONGO_DB_URI"] = "URI" - os.environ["OUTPUT_COUNTS_MONGO_COLLECTION_NAME"] = "test_output" - kafkaMessageCounterType1 = createKafkaMessageCounter(1) - kafka_counter.logging = MagicMock() - kafka_counter.logging.info = MagicMock() - - # call - test_doc = {"test": "doc"} - kafkaMessageCounterType1.write_mongo(test_doc) - - # check - kafkaMessageCounterType1.mongo_client[os.getenv("MONGO_DB_NAME")][ - os.getenv("OUTPUT_COUNTS_MONGO_COLLECTION_NAME") - ].insert_many.assert_called_once_with(test_doc) - kafka_counter.logging.info.assert_called_once() - - -def test_push_metrics_mongo_success(): - os.environ["DESTINATION_DB"] = "MONGODB" - os.environ["MONGO_DB_URI"] = "URI" - os.environ["INPUT_COUNTS_MONGO_COLLECTION_NAME"] = "test_input" - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafkaMessageCounter.write_mongo = MagicMock() - - # call - kafkaMessageCounter.push_metrics() - - # check - kafkaMessageCounter.write_mongo.assert_called_once() - - -def test_push_metrics_mongo_exception(): - os.environ["DESTINATION_DB"] = "MONGODB" - os.environ["MONGO_DB_URI"] = "URI" - os.environ["INPUT_COUNTS_MONGO_COLLECTION_NAME"] = "test_input" - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafkaMessageCounter.write_mongo = MagicMock() - kafkaMessageCounter.write_mongo.side_effect = Exception("test") - kafka_counter.logging = MagicMock() - kafka_counter.logging.error = MagicMock() - - # call - kafkaMessageCounter.push_metrics() - - # check - kafkaMessageCounter.write_mongo.assert_called_once() - kafka_counter.logging.error.assert_called_once() - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.json") -def test_process_message_with_type0_kmc_origin_ip_present_success( - mock_json, mock_logging -): - kafkaMessageCounter = createKafkaMessageCounter(0) - originIp = "192.168.0.5" - mock_json.loads.return_value = { - "BsmMessageContent": [ - { - "metadata": { - "utctimestamp": "2020-10-01T00:00:00.000Z", - "originRsu": originIp, - }, - "payload": "00131A604A380583702005837800080008100000040583705043002580", - } - ] - } - value_return = MagicMock() - value_return.decode.return_value = "test" - msg = MagicMock() - msg.value.return_value = value_return - - # call - kafkaMessageCounter.process_message(msg) - - # check - assert kafkaMessageCounter.rsu_count_dict["Unknown"][originIp] == 1 - mock_logging.warning.assert_not_called() - mock_logging.error.assert_not_called() - mock_json.loads.assert_called_once_with("test") - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.json") -def test_process_message_with_type0_kmc_malformed_message(mock_json, mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafka_counter.json.loads.return_value = { - "BsmMessageContent": [ - { - "metadata": {"utctimestamp": "2020-10-01T00:00:00.000Z"}, - "payload": "00131A604A380583702005837800080008100000040583705043002580", - } - ] - } - value_return = MagicMock() - value_return.decode.return_value = "test" - msg = MagicMock() - msg.value.return_value = value_return - - # call - kafkaMessageCounter.process_message(msg) - - # check - assert kafkaMessageCounter.rsu_count_dict["Unknown"]["noIP"] == 2 - mock_logging.warning.assert_called_once() - mock_logging.error.assert_not_called() - mock_json.loads.assert_called_once_with("test") - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.json") -def test_process_message_with_type1_kmc_origin_ip_present_success( - mock_json, mock_logging -): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(1) - originIp = "192.168.0.5" - kafka_counter.json.loads.return_value = { - "metadata": {"utctimestamp": "2020-10-01T00:00:00.000Z", "originIp": originIp}, - "payload": "00131A604A380583702005837800080008100000040583705043002580", - } - value_return = MagicMock() - value_return.decode.return_value = "test" - msg = MagicMock() - msg.value.return_value = value_return - - # call - kafkaMessageCounter.process_message(msg) - - # check - assert kafkaMessageCounter.rsu_count_dict["Unknown"][originIp] == 1 - mock_logging.warning.assert_not_called() - mock_logging.error.assert_not_called() - mock_json.loads.assert_called_once_with("test") - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.json") -def test_process_message_with_type1_kmc_malformed_message(mock_json, mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(1) - kafka_counter.json.loads.return_value = { - "metadata": {"utctimestamp": "2020-10-01T00:00:00.000Z"}, - "payload": "00131A604A380583702005837800080008100000040583705043002580", - } - value_return = MagicMock() - value_return.decode.return_value = "test" - msg = MagicMock() - msg.value.return_value = value_return - - # call - kafkaMessageCounter.process_message(msg) - - # check - assert kafkaMessageCounter.rsu_count_dict["Unknown"]["noIP"] == 2 - mock_logging.warning.assert_called_once() - mock_logging.error.assert_not_called() - mock_json.loads.assert_called_once_with("test") - - -@patch("addons.images.count_metric.kafka_counter.logging") -def test_process_message_exception(mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - - # call - message = "" - kafkaMessageCounter.process_message(message) - - # check - assert kafkaMessageCounter.rsu_count_dict["Unknown"]["noIP"] == 1 - mock_logging.warning.assert_not_called() - mock_logging.error.assert_called_once() - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.Consumer") -def test_listen_for_message_and_process_success(mock_Consumer, mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - proc_msg = MagicMock() - proc_msg.side_effect = [True, False] - kafkaMessageCounter.should_run = proc_msg - kafkaMessageCounter.process_message = MagicMock() - - kafkaConsumer = MagicMock() - mock_Consumer.return_value = kafkaConsumer - msg = MagicMock() - kafkaConsumer.poll.return_value = msg - msg.error.return_value = None - - # call - topic = "test" - bootstrap_servers = "test" - kafkaMessageCounter.listen_for_message_and_process(topic, bootstrap_servers) - - # check - kafkaMessageCounter.process_message.assert_called_once() - mock_logging.warning.assert_called_once() - kafkaConsumer.poll.assert_called_once() - kafkaConsumer.close.assert_called_once() - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.Consumer") -def test_listen_for_message_and_process_eof(mock_Consumer, mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - proc_msg = MagicMock() - proc_msg.side_effect = [True, False] - kafkaMessageCounter.should_run = proc_msg - kafkaMessageCounter.process_message = MagicMock() - - kafkaConsumer = MagicMock() - mock_Consumer.return_value = kafkaConsumer - msg = MagicMock() - kafkaConsumer.poll.return_value = msg - msg_code = MagicMock() - msg_code.code.return_value = KafkaError._PARTITION_EOF - msg.error.return_value = msg_code - msg.topic.return_value = "test" - - # call - topic = "test" - bootstrap_servers = "test" - kafkaMessageCounter.listen_for_message_and_process(topic, bootstrap_servers) - - # check - expected_calls = [ - call("Topic test [1] reached end at offset 1\n"), - call("0: Disconnected from Kafka topic, reconnecting..."), - ] - kafkaMessageCounter.process_message.assert_not_called() - mock_logging.warning.assert_has_calls(expected_calls) - kafkaConsumer.close.assert_called_once() - - -@patch("addons.images.count_metric.kafka_counter.logging") -@patch("addons.images.count_metric.kafka_counter.Consumer") -def test_listen_for_message_and_process_error(mock_Consumer, mock_logging): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - proc_msg = MagicMock() - proc_msg.side_effect = [True, False] - kafkaMessageCounter.should_run = proc_msg - kafkaMessageCounter.process_message = MagicMock() - - kafkaConsumer = MagicMock() - mock_Consumer.return_value = kafkaConsumer - msg = MagicMock() - kafkaConsumer.poll.return_value = msg - msg_code = MagicMock() - msg_code.code.return_value = None - msg.error.return_value = msg_code - - # call and verify it raises the exception - with pytest.raises(KafkaException): - topic = "test" - bootstrap_servers = "test" - kafkaMessageCounter.listen_for_message_and_process(topic, bootstrap_servers) - - kafkaMessageCounter.process_message.assert_not_called() - mock_logging.warning.assert_called_with( - "0: Disconnected from Kafka topic, reconnecting..." - ) - kafkaConsumer.close.assert_called_once() - - -def test_get_topic_from_type_success(): - # prepare - kafkaMessageCounterType0 = createKafkaMessageCounter(0) - kafkaMessageCounterType1 = createKafkaMessageCounter(1) - - # call - topicType0 = kafkaMessageCounterType0.get_topic_from_type() - topicType1 = kafkaMessageCounterType1.get_topic_from_type() - - # check - messageType = "bsm" - expectedTopicType0 = f"topic.OdeRawEncoded{messageType.upper()}Json" - expectedTopicType1 = f"topic.Ode{messageType.capitalize()}Json" - assert topicType0 == expectedTopicType0 - assert topicType1 == expectedTopicType1 - - -# # PROBLEM TEST -def test_read_topic_success(): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafkaMessageCounter.get_topic_from_type = MagicMock() - kafkaMessageCounter.get_topic_from_type.return_value = "test" - os.environ["ODE_KAFKA_BROKERS"] = "test" - kafkaMessageCounter.listen_for_message_and_process = MagicMock() - kafkaMessageCounter.should_run = MagicMock() - kafkaMessageCounter.should_run.side_effect = [True, False] - - # call - kafkaMessageCounter.read_topic() - - # check - kafkaMessageCounter.get_topic_from_type.assert_called_once() - kafkaMessageCounter.listen_for_message_and_process.assert_called_once_with( - "test", "test" - ) - - -def test_start_counter_success(): - # prepare - kafkaMessageCounter = createKafkaMessageCounter(0) - kafka_counter.logging = MagicMock() - kafka_counter.logging.info = MagicMock() - kafka_counter.BackgroundScheduler = MagicMock() - kafka_counter.BackgroundScheduler.return_value = MagicMock() - kafka_counter.BackgroundScheduler.return_value.add_job = MagicMock() - kafka_counter.BackgroundScheduler.return_value.start = MagicMock() - kafkaMessageCounter.read_topic = MagicMock() - - # call - kafkaMessageCounter.start_counter() - - # check - kafka_counter.BackgroundScheduler.assert_called_once() - kafka_counter.BackgroundScheduler.return_value.add_job.assert_called_once() - kafka_counter.BackgroundScheduler.return_value.start.assert_called_once() - kafka_counter.logging.info.assert_called_once() - kafkaMessageCounter.read_topic.assert_called_once() diff --git a/services/addons/tests/count_metric/test_mongo_counter.py b/services/addons/tests/count_metric/test_mongo_counter.py new file mode 100644 index 00000000..acacacb7 --- /dev/null +++ b/services/addons/tests/count_metric/test_mongo_counter.py @@ -0,0 +1,76 @@ +import os +from datetime import datetime, timedelta +from mock import MagicMock, patch +from addons.images.count_metric import mongo_counter + + +def test_write_counts(): + mock_collection = MagicMock() + mock_mongo_db = {"CVCounts": mock_collection} + + mongo_counter.write_counts(mock_mongo_db, ["test"]) + + mock_collection.insert_many.assert_called_with(["test"]) + + +@patch.dict(os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name"}) +def test_count_query_bsm(): + mock_collection = MagicMock() + mock_collection.aggregate.return_value = [ + { + "_id": "10.0.0.1", + "count": 5, + } + ] + mock_mongo_db = {"OdeBsmJson": mock_collection} + + start_dt = datetime.now().replace( + year=2024, month=1, day=1, minute=0, second=0, microsecond=0 + ) + end_dt = datetime.now().replace( + year=2024, month=1, day=2, minute=0, second=0, microsecond=0 + ) + + result = mongo_counter.count_query(mock_mongo_db, "bsm", start_dt, end_dt) + + expected_result = [ + { + "messageType": "bsm", + "rsuIp": "10.0.0.1", + "timestamp": start_dt, + "count": 5, + } + ] + assert result == expected_result + + +@patch.dict(os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name"}) +@patch("addons.images.count_metric.mongo_counter.write_counts") +@patch("addons.images.count_metric.mongo_counter.count_query") +def test_run_mongo_counter(mock_count_query, mock_write_counts): + start_dt = datetime.now().replace( + year=2024, month=1, day=1, minute=0, second=0, microsecond=0 + ) + mock_count_query.return_value = [ + { + "messageType": "test", + "rsuIp": "10.0.0.1", + "timestamp": start_dt, + "count": 5, + } + ] + mock_mongo_db = MagicMock() + mongo_counter.message_types = ["test"] + + mongo_counter.run_mongo_counter(mock_mongo_db) + + expected = [ + { + "messageType": "test", + "rsuIp": "10.0.0.1", + "timestamp": start_dt, + "count": 5, + } + ] + mock_count_query.assert_called_once() + mock_write_counts.assert_called_with(mock_mongo_db, expected) diff --git a/services/addons/tests/firmware_manager/test_commsignia_upgrader.py b/services/addons/tests/firmware_manager/test_commsignia_upgrader.py index c3810b52..b738a05d 100644 --- a/services/addons/tests/firmware_manager/test_commsignia_upgrader.py +++ b/services/addons/tests/firmware_manager/test_commsignia_upgrader.py @@ -33,7 +33,7 @@ def test_commsignia_upgrader_init(): @patch("addons.images.firmware_manager.commsignia_upgrader.SCPClient") @patch("addons.images.firmware_manager.commsignia_upgrader.SSHClient") -def test_commsignia_upgrader_upgrade_success(mock_sshclient, mock_scpclient): +def test_commsignia_upgrader_upgrade_success_no_post_update(mock_sshclient, mock_scpclient): # Mock SSH Client and successful firmware upgrade return value sshclient_obj = mock_sshclient.return_value _stdout = MagicMock() @@ -44,7 +44,8 @@ def test_commsignia_upgrader_upgrade_success(mock_sshclient, mock_scpclient): scpclient_obj = mock_scpclient.return_value test_commsignia_upgrader = CommsigniaUpgrader(test_upgrade_info) - test_commsignia_upgrader.download_blob = MagicMock() + test_commsignia_upgrader.check_online = MagicMock(return_value=True) + test_commsignia_upgrader.download_blob = MagicMock(return_value=False) test_commsignia_upgrader.cleanup = MagicMock() notify = MagicMock() test_commsignia_upgrader.notify_firmware_manager = notify @@ -77,6 +78,123 @@ def test_commsignia_upgrader_upgrade_success(mock_sshclient, mock_scpclient): # Assert notified success value notify.assert_called_with(success=True) +@patch("addons.images.firmware_manager.commsignia_upgrader.SCPClient") +@patch("addons.images.firmware_manager.commsignia_upgrader.SSHClient") +@patch("addons.images.firmware_manager.commsignia_upgrader.time") +def test_commsignia_upgrader_upgrade_success_post_update(mock_time, mock_sshclient, mock_scpclient): + # Mock SSH Client and successful firmware upgrade return value + sshclient_obj = mock_sshclient.return_value + _stdout = MagicMock() + sshclient_obj.exec_command.return_value = MagicMock(), _stdout, MagicMock() + _stdout.read.return_value.decode.return_value = "ALL OK" + + # Mock SCP Client + scpclient_obj = mock_scpclient.return_value + + test_commsignia_upgrader = CommsigniaUpgrader(test_upgrade_info) + test_commsignia_upgrader.check_online = MagicMock(return_value=True) + test_commsignia_upgrader.download_blob = MagicMock(return_value=True) + test_commsignia_upgrader.cleanup = MagicMock() + notify = MagicMock() + test_commsignia_upgrader.notify_firmware_manager = notify + test_commsignia_upgrader.wait_until_online = MagicMock(return_value=0) + + # Mock time.sleep to avoid waiting during test + mock_time.sleep = MagicMock(return_value=None) + + test_commsignia_upgrader.upgrade() + + # Assert initial SSH connection + sshclient_obj.set_missing_host_key_policy.assert_called_with(WarningPolicy) + sshclient_obj.connect.assert_called_with( + "8.8.8.8", + username="test-user", + password="test-psw", + look_for_keys=False, + allow_agent=False, + ) + + # Assert SCP file transfer + mock_scpclient.assert_called_with(sshclient_obj.get_transport()) + scpclient_obj.put.assert_called_with( + "/home/8.8.8.8/post_upgrade.sh", remote_path="/tmp/" + ) + scpclient_obj.close.assert_called_with() + + # Assert SSH firmware upgrade run + sshclient_obj.exec_command.assert_has_calls( + [ + call("signedUpgrade.sh /tmp/firmware_package.tar"), + call("reboot"), + call("chmod +x /tmp/post_upgrade.sh"), + call("/tmp/post_upgrade.sh"), + ] + ) + sshclient_obj.close.assert_called_with() + + # Assert notified success value + notify.assert_called_with(success=True) + +@patch("addons.images.firmware_manager.commsignia_upgrader.SCPClient") +@patch("addons.images.firmware_manager.commsignia_upgrader.SSHClient") +@patch("addons.images.firmware_manager.commsignia_upgrader.time") +@patch("addons.images.firmware_manager.commsignia_upgrader.logging") +def test_commsignia_upgrader_upgrade_post_update_fail(mock_logging, mock_time, mock_sshclient, mock_scpclient): + # Mock SSH Client and successful firmware upgrade return value + sshclient_obj = mock_sshclient.return_value + _stdout = MagicMock() + sshclient_obj.exec_command.return_value = MagicMock(), _stdout, MagicMock() + _stdout.read.return_value.decode = MagicMock(side_effect=["ALL OK", "NOT OK TEST"]) + + # Mock SCP Client + scpclient_obj = mock_scpclient.return_value + + test_commsignia_upgrader = CommsigniaUpgrader(test_upgrade_info) + test_commsignia_upgrader.check_online = MagicMock(return_value=True) + test_commsignia_upgrader.download_blob = MagicMock(return_value=True) + test_commsignia_upgrader.cleanup = MagicMock() + notify = MagicMock() + test_commsignia_upgrader.notify_firmware_manager = notify + test_commsignia_upgrader.wait_until_online = MagicMock(return_value=0) + + # Mock time.sleep to avoid waiting during test + mock_time.sleep = MagicMock(return_value=None) + + # Mock logging.error to check for expected error message + mock_logging.error = MagicMock() + + test_commsignia_upgrader.upgrade() + + # Assert initial SSH connection + sshclient_obj.set_missing_host_key_policy.assert_called_with(WarningPolicy) + sshclient_obj.connect.assert_called_with( + "8.8.8.8", + username="test-user", + password="test-psw", + look_for_keys=False, + allow_agent=False, + ) + + # Assert SCP file transfer + mock_scpclient.assert_called_with(sshclient_obj.get_transport()) + scpclient_obj.put.assert_called_with( + '/home/8.8.8.8/post_upgrade.sh', remote_path='/tmp/' + ) + scpclient_obj.close.assert_called_with() + + # Assert SSH firmware upgrade run + sshclient_obj.exec_command.assert_has_calls( + [ + call("signedUpgrade.sh /tmp/firmware_package.tar"), + call("reboot"), + call("chmod +x /tmp/post_upgrade.sh"), + call("/tmp/post_upgrade.sh"), + ] + ) + sshclient_obj.close.assert_called_with() + mock_logging.error.assert_called_with("Failed to execute post upgrade script for rsu 8.8.8.8: NOT OK TEST") + # Assert notified success value + notify.assert_called_with(success=True) @patch("addons.images.firmware_manager.commsignia_upgrader.SCPClient") @patch("addons.images.firmware_manager.commsignia_upgrader.SSHClient") @@ -91,6 +209,7 @@ def test_commsignia_upgrader_upgrade_fail(mock_sshclient, mock_scpclient): scpclient_obj = mock_scpclient.return_value test_commsignia_upgrader = CommsigniaUpgrader(test_upgrade_info) + test_commsignia_upgrader.check_online = MagicMock(return_value=True) test_commsignia_upgrader.download_blob = MagicMock() test_commsignia_upgrader.cleanup = MagicMock() notify = MagicMock() @@ -136,6 +255,7 @@ def test_commsignia_upgrader_upgrade_exception( sshclient_obj.connect.side_effect = Exception("Exception occurred during upgrade") test_commsignia_upgrader = CommsigniaUpgrader(test_upgrade_info) + test_commsignia_upgrader.check_online = MagicMock(return_value=True) test_commsignia_upgrader.download_blob = MagicMock() cleanup = MagicMock() notify = MagicMock() diff --git a/services/addons/tests/firmware_manager/test_download_blob.py b/services/addons/tests/firmware_manager/test_download_blob.py index cc44ced9..970ec8f9 100644 --- a/services/addons/tests/firmware_manager/test_download_blob.py +++ b/services/addons/tests/firmware_manager/test_download_blob.py @@ -1,7 +1,9 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch import os +import pytest from addons.images.firmware_manager import download_blob +from addons.images.firmware_manager.download_blob import UnsupportedFileTypeException @patch.dict( @@ -17,14 +19,69 @@ def test_download_gcp_blob(mock_storage_client, mock_logging): # run download_blob.download_gcp_blob( - blob_name="test.blob", destination_file_name="/home/test/" + blob_name="test.tar", destination_file_name="/home/test/" ) # validate mock_storage_client.assert_called_with("test-project") mock_client.get_bucket.assert_called_with("test-bucket") - mock_bucket.blob.assert_called_with("test.blob") + mock_bucket.blob.assert_called_with("test.tar") mock_blob.download_to_filename.assert_called_with("/home/test/") mock_logging.info.assert_called_with( - "Downloaded storage object test.blob from bucket test-bucket to local file /home/test/." + "Downloaded storage object test.tar from bucket test-bucket to local file /home/test/." ) + + +@patch.dict( + os.environ, {"GCP_PROJECT": "test-project", "BLOB_STORAGE_BUCKET": "test-bucket"} +) +@patch("addons.images.firmware_manager.download_blob.logging") +def test_download_gcp_blob_unsupported_file_type(mock_logging): + # prepare + blob_name = "test.blob" + destination_file_name = "/home/test/" + + # run + result = download_blob.download_gcp_blob(blob_name, destination_file_name) + + # validate + mock_logging.error.assert_called_with( + f"Unsupported file type for storage object {blob_name}. Only .tar files are supported." + ) + assert result == False + + +@patch("addons.images.firmware_manager.download_blob.logging") +def test_download_docker_blob(mock_logging): + # prepare + os.system = MagicMock() + blob_name = "test.tar" + destination_file_name = "/home/test/" + + # run + download_blob.download_docker_blob(blob_name, destination_file_name) + + # validate + os.system.assert_called_with( + f"cp /mnt/blob_storage/{blob_name} {destination_file_name}" + ) + mock_logging.info.assert_called_with( + f"Copied storage object {blob_name} from directory /mnt/blob_storage to local file {destination_file_name}." + ) + + +@patch("addons.images.firmware_manager.download_blob.logging") +def test_download_docker_blob_unsupported_file_type(mock_logging): + # prepare + os.system = MagicMock() + blob_name = "test.blob" + destination_file_name = "/home/test/" + + # run + result = download_blob.download_docker_blob(blob_name, destination_file_name) + + # validate + mock_logging.error.assert_called_with( + f"Unsupported file type for storage object {blob_name}. Only .tar files are supported." + ) + assert result == False diff --git a/services/addons/tests/firmware_manager/test_firmware_manager.py b/services/addons/tests/firmware_manager/test_firmware_manager.py index 3b50e744..11b19b5f 100644 --- a/services/addons/tests/firmware_manager/test_firmware_manager.py +++ b/services/addons/tests/firmware_manager/test_firmware_manager.py @@ -1,5 +1,6 @@ from unittest.mock import patch, MagicMock from subprocess import DEVNULL +from collections import deque import test_firmware_manager_values as fmv from addons.images.firmware_manager import firmware_manager @@ -31,6 +32,90 @@ def test_get_rsu_upgrade_data_one(mock_querydb): assert result == expected_result +# start_tasks_from_queue tests + + +@patch("addons.images.firmware_manager.firmware_manager.active_upgrades", {}) +@patch( + "addons.images.firmware_manager.firmware_manager.upgrade_queue", deque(["8.8.8.8"]) +) +@patch( + "addons.images.firmware_manager.firmware_manager.upgrade_queue_info", + { + "8.8.8.8": { + "ipv4_address": "8.8.8.8", + "manufacturer": "Commsignia", + "model": "ITS-RS4-M", + "ssh_username": "user", + "ssh_password": "psw", + "target_firmware_id": 2, + "target_firmware_version": "y20.39.0", + "install_package": "install_package.tar", + } + }, +) +@patch("addons.images.firmware_manager.firmware_manager.logging") +@patch( + "addons.images.firmware_manager.firmware_manager.Popen", + side_effect=Exception("Process failed to start"), +) +def test_start_tasks_from_queue_popen_fail(mock_popen, mock_logging): + firmware_manager.start_tasks_from_queue() + + # Assert firmware upgrade process was started with expected arguments + expected_json_str = ( + '\'{"ipv4_address": "8.8.8.8", "manufacturer": "Commsignia", "model": "ITS-RS4-M", ' + '"ssh_username": "user", "ssh_password": "psw", "target_firmware_id": 2, "target_firmware_version": "y20.39.0", ' + '"install_package": "install_package.tar"}\'' + ) + mock_popen.assert_called_with( + ["python3", f"/home/commsignia_upgrader.py", expected_json_str], + stdout=DEVNULL, + ) + mock_logging.error.assert_called_with( + f"Encountered error of type {Exception} while starting automatic upgrade process for 8.8.8.8: Process failed to start" + ) + + +@patch("addons.images.firmware_manager.firmware_manager.active_upgrades", {}) +@patch( + "addons.images.firmware_manager.firmware_manager.upgrade_queue", deque(["8.8.8.8"]) +) +@patch( + "addons.images.firmware_manager.firmware_manager.upgrade_queue_info", + { + "8.8.8.8": { + "ipv4_address": "8.8.8.8", + "manufacturer": "Commsignia", + "model": "ITS-RS4-M", + "ssh_username": "user", + "ssh_password": "psw", + "target_firmware_id": 2, + "target_firmware_version": "y20.39.0", + "install_package": "install_package.tar", + } + }, +) +@patch("addons.images.firmware_manager.firmware_manager.Popen") +def test_start_tasks_from_queue_popen_success(mock_popen): + mock_popen_obj = mock_popen.return_value + + firmware_manager.start_tasks_from_queue() + + # Assert firmware upgrade process was started with expected arguments + expected_json_str = ( + '\'{"ipv4_address": "8.8.8.8", "manufacturer": "Commsignia", "model": "ITS-RS4-M", ' + '"ssh_username": "user", "ssh_password": "psw", "target_firmware_id": 2, "target_firmware_version": "y20.39.0", ' + '"install_package": "install_package.tar"}\'' + ) + mock_popen.assert_called_with( + ["python3", f"/home/commsignia_upgrader.py", expected_json_str], + stdout=DEVNULL, + ) + # Assert the process reference is successfully tracked in the active_upgrades dictionary + assert firmware_manager.active_upgrades["8.8.8.8"]["process"] == mock_popen_obj + + # init_firmware_upgrade tests @@ -72,7 +157,7 @@ def test_init_firmware_upgrade_already_running(): mock_flask_jsonify.assert_called_with( { - "error": f"Firmware upgrade failed to start for '8.8.8.8': an upgrade is already underway for the target device" + "error": f"Firmware upgrade failed to start for '8.8.8.8': an upgrade is already underway or queued for the target device" } ) assert code == 500 @@ -109,55 +194,8 @@ def test_init_firmware_upgrade_no_eligible_upgrade(): "addons.images.firmware_manager.firmware_manager.get_rsu_upgrade_data", MagicMock(return_value=[fmv.rsu_info]), ) -@patch("addons.images.firmware_manager.firmware_manager.logging") -@patch( - "addons.images.firmware_manager.firmware_manager.Popen", - side_effect=Exception("Process failed to start"), -) -def test_init_firmware_upgrade_popen_fail(mock_popen, mock_logging): - mock_flask_request = MagicMock() - mock_flask_request.get_json.return_value = {"rsu_ip": "8.8.8.8"} - mock_flask_jsonify = MagicMock() - with patch( - "addons.images.firmware_manager.firmware_manager.request", mock_flask_request - ): - with patch( - "addons.images.firmware_manager.firmware_manager.jsonify", - mock_flask_jsonify, - ): - message, code = firmware_manager.init_firmware_upgrade() - - # Assert firmware upgrade process was started with expected arguments - expected_json_str = ( - '\'{"ipv4_address": "8.8.8.8", "manufacturer": "Commsignia", "model": "ITS-RS4-M", ' - '"ssh_username": "user", "ssh_password": "psw", "target_firmware_id": 2, "target_firmware_version": "y20.39.0", ' - '"install_package": "install_package.tar"}\'' - ) - mock_popen.assert_called_with( - ["python3", f"/home/commsignia_upgrader.py", expected_json_str], - stdout=DEVNULL, - ) - mock_logging.error.assert_called_with( - f"Encountered error of type {Exception} while starting automatic upgrade process for 8.8.8.8: Process failed to start" - ) - - # Assert REST response is as expected from a successful run - mock_flask_jsonify.assert_called_with( - { - "error": f"Firmware upgrade failed to start for '8.8.8.8': upgrade process failed to run" - } - ) - assert code == 500 - - -@patch("addons.images.firmware_manager.firmware_manager.active_upgrades", {}) -@patch( - "addons.images.firmware_manager.firmware_manager.get_rsu_upgrade_data", - MagicMock(return_value=[fmv.rsu_info]), -) -@patch("addons.images.firmware_manager.firmware_manager.Popen") -def test_init_firmware_upgrade_popen_success(mock_popen): - mock_popen_obj = mock_popen.return_value +@patch("addons.images.firmware_manager.firmware_manager.start_tasks_from_queue") +def test_init_firmware_upgrade_success(mock_stfq): mock_flask_request = MagicMock() mock_flask_request.get_json.return_value = {"rsu_ip": "8.8.8.8"} mock_flask_jsonify = MagicMock() @@ -170,21 +208,11 @@ def test_init_firmware_upgrade_popen_success(mock_popen): ): message, code = firmware_manager.init_firmware_upgrade() - # Assert firmware upgrade process was started with expected arguments - expected_json_str = ( - '\'{"ipv4_address": "8.8.8.8", "manufacturer": "Commsignia", "model": "ITS-RS4-M", ' - '"ssh_username": "user", "ssh_password": "psw", "target_firmware_id": 2, "target_firmware_version": "y20.39.0", ' - '"install_package": "install_package.tar"}\'' - ) - mock_popen.assert_called_with( - ["python3", f"/home/commsignia_upgrader.py", expected_json_str], - stdout=DEVNULL, - ) + # Assert start_tasks_from_queue is called + mock_stfq.assert_called_with() - # Assert the process reference is successfully tracked in the active_upgrades dictionary - assert ( - firmware_manager.active_upgrades["8.8.8.8"]["process"] == mock_popen_obj - ) + # Assert the process reference is successfully tracked in the upgrade_queue + assert firmware_manager.upgrade_queue[0] == "8.8.8.8" # Assert REST response is as expected from a successful run mock_flask_jsonify.assert_called_with( @@ -192,6 +220,8 @@ def test_init_firmware_upgrade_popen_success(mock_popen): ) assert code == 201 + firmware_manager.upgrade_queue = deque([]) + # firmware_upgrade_completed tests @@ -412,7 +442,7 @@ def test_list_active_upgrades(): } } mock_flask_jsonify.assert_called_with( - {"active_upgrades": expected_active_upgrades} + {"active_upgrades": expected_active_upgrades, "upgrade_queue": []} ) assert code == 200 @@ -462,24 +492,15 @@ def test_check_for_upgrades_exception(mock_popen, mock_logging): MagicMock(return_value=fmv.multi_rsu_info), ) @patch("addons.images.firmware_manager.firmware_manager.logging") -@patch("addons.images.firmware_manager.firmware_manager.Popen") -def test_check_for_upgrades(mock_popen, mock_logging): - mock_popen_obj = mock_popen.return_value - +@patch("addons.images.firmware_manager.firmware_manager.start_tasks_from_queue") +def test_check_for_upgrades(mock_stfq, mock_logging): firmware_manager.check_for_upgrades() # Assert firmware upgrade process was started with expected arguments - expected_json_str = ( - '\'{"ipv4_address": "9.9.9.9", "manufacturer": "Commsignia", "model": "ITS-RS4-M", ' - '"ssh_username": "user", "ssh_password": "psw", "target_firmware_id": 2, "target_firmware_version": "y20.39.0", ' - '"install_package": "install_package.tar"}\'' - ) - mock_popen.assert_called_once_with( - ["python3", f"/home/commsignia_upgrader.py", expected_json_str], stdout=DEVNULL - ) + mock_stfq.assert_called_once_with() # Assert the process reference is successfully tracked in the active_upgrades dictionary - assert firmware_manager.active_upgrades["9.9.9.9"]["process"] == mock_popen_obj + assert firmware_manager.upgrade_queue[0] == "9.9.9.9" mock_logging.info.assert_called_with( "Firmware upgrade successfully started for '9.9.9.9'" ) diff --git a/services/addons/tests/firmware_manager/test_upgrader.py b/services/addons/tests/firmware_manager/test_upgrader.py index 640dcc72..d273b4e8 100644 --- a/services/addons/tests/firmware_manager/test_upgrader.py +++ b/services/addons/tests/firmware_manager/test_upgrader.py @@ -1,7 +1,9 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch import os +import pytest from addons.images.firmware_manager import upgrader +from addons.images.firmware_manager.upgrader import StorageProviderNotSupportedException # Test class for testing the abstract class @@ -85,6 +87,21 @@ def test_download_blob_gcp(mock_Path, mock_download_gcp_blob): "/home/8.8.8.8/firmware_package.tar", ) +@patch.dict(os.environ, {"BLOB_STORAGE_PROVIDER": "DOCKER"}) +@patch("addons.images.firmware_manager.upgrader.download_blob.download_docker_blob") +@patch("addons.images.firmware_manager.upgrader.Path") +def test_download_blob_docker(mock_Path, mock_download_docker_blob): + mock_path_obj = mock_Path.return_value + test_upgrader = TestUpgrader(test_upgrade_info) + + test_upgrader.download_blob() + + mock_path_obj.mkdir.assert_called_with(exist_ok=True) + mock_download_docker_blob.assert_called_with( + "test-manufacturer/test-model/1.0.0/firmware_package.tar", + "/home/8.8.8.8/firmware_package.tar", + ) + @patch.dict(os.environ, {"BLOB_STORAGE_PROVIDER": "Test"}) @patch("addons.images.firmware_manager.upgrader.logging") @@ -94,11 +111,12 @@ def test_download_blob_not_supported(mock_Path, mock_download_gcp_blob, mock_log mock_path_obj = mock_Path.return_value test_upgrader = TestUpgrader(test_upgrade_info) - test_upgrader.download_blob() + with pytest.raises(StorageProviderNotSupportedException): + test_upgrader.download_blob() - mock_path_obj.mkdir.assert_called_with(exist_ok=True) - mock_download_gcp_blob.assert_not_called() - mock_logging.error.assert_called_with("Unsupported blob storage provider") + mock_path_obj.mkdir.assert_called_with(exist_ok=True) + mock_download_gcp_blob.assert_not_called() + mock_logging.error.assert_called_with("Unsupported blob storage provider") @patch("addons.images.firmware_manager.upgrader.logging") @@ -142,3 +160,30 @@ def test_notify_firmware_manager_exception(mock_requests, mock_logging): mock_logging.error.assert_called_with( "Failed to connect to the Firmware Manager API for '8.8.8.8': Exception occurred during upgrade" ) + +@patch("addons.images.firmware_manager.upgrader.time") +@patch("addons.images.firmware_manager.upgrader.subprocess") +def test_upgrader_wait_until_online_success(mock_subprocess, mock_time): + run_response_obj = MagicMock() + run_response_obj.returncode = 0 + mock_subprocess.run.return_value = run_response_obj + + test_upgrader = TestUpgrader(test_upgrade_info) + code = test_upgrader.wait_until_online() + + assert code == 0 + assert mock_time.sleep.call_count == 1 + + +@patch("addons.images.firmware_manager.upgrader.time") +@patch("addons.images.firmware_manager.upgrader.subprocess") +def test_upgrader_wait_until_online_timeout(mock_subprocess, mock_time): + run_response_obj = MagicMock() + run_response_obj.returncode = 1 + mock_subprocess.run.return_value = run_response_obj + + test_upgrader = TestUpgrader(test_upgrade_info) + code = test_upgrader.wait_until_online() + + assert code == -1 + assert mock_time.sleep.call_count == 180 \ No newline at end of file diff --git a/services/addons/tests/firmware_manager/test_yunex_upgrader.py b/services/addons/tests/firmware_manager/test_yunex_upgrader.py index 1b1a7f50..0108779c 100644 --- a/services/addons/tests/firmware_manager/test_yunex_upgrader.py +++ b/services/addons/tests/firmware_manager/test_yunex_upgrader.py @@ -60,7 +60,7 @@ def test_yunex_upgrader_run_xfer_upgrade_fail_code(mock_subprocess): test_yunex_upgrader = YunexUpgrader(test_upgrade_info) code = test_yunex_upgrader.run_xfer_upgrade("core-file-name") - + assert code == -1 @@ -80,34 +80,6 @@ def test_yunex_upgrader_run_xfer_upgrade_fail_output(mock_subprocess): assert code == -1 -@patch("addons.images.firmware_manager.yunex_upgrader.time") -@patch("addons.images.firmware_manager.yunex_upgrader.subprocess") -def test_yunex_upgrader_wait_until_online_success(mock_subprocess, mock_time): - run_response_obj = MagicMock() - run_response_obj.returncode = 0 - mock_subprocess.run.return_value = run_response_obj - - test_yunex_upgrader = YunexUpgrader(test_upgrade_info) - code = test_yunex_upgrader.wait_until_online() - - assert code == 0 - assert mock_time.sleep.call_count == 1 - - -@patch("addons.images.firmware_manager.yunex_upgrader.time") -@patch("addons.images.firmware_manager.yunex_upgrader.subprocess") -def test_yunex_upgrader_wait_until_online_timeout(mock_subprocess, mock_time): - run_response_obj = MagicMock() - run_response_obj.returncode = 1 - mock_subprocess.run.return_value = run_response_obj - - test_yunex_upgrader = YunexUpgrader(test_upgrade_info) - code = test_yunex_upgrader.wait_until_online() - - assert code == -1 - assert mock_time.sleep.call_count == 180 - - @patch("addons.images.firmware_manager.yunex_upgrader.time") @patch("addons.images.firmware_manager.yunex_upgrader.json") @patch("builtins.open", new_callable=mock_open, read_data="data") @@ -122,6 +94,7 @@ def test_yunex_upgrader_upgrade_success( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(return_value=0) test_yunex_upgrader.wait_until_online = MagicMock(return_value=0) @@ -166,6 +139,7 @@ def test_yunex_upgrader_core_upgrade_fail( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(return_value=-1) test_yunex_upgrader.wait_until_online = MagicMock(return_value=0) @@ -206,6 +180,7 @@ def test_yunex_upgrader_core_ping_fail( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(return_value=0) test_yunex_upgrader.wait_until_online = MagicMock(return_value=-1) @@ -246,6 +221,7 @@ def test_yunex_upgrader_sdk_upgrade_fail( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(side_effect=[0, -1]) test_yunex_upgrader.wait_until_online = MagicMock(return_value=0) @@ -286,6 +262,7 @@ def test_yunex_upgrader_sdk_ping_fail( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(return_value=0) test_yunex_upgrader.wait_until_online = MagicMock(side_effect=[0, -1]) @@ -326,6 +303,7 @@ def test_yunex_upgrader_provision_upgrade_fail( mock_json.load.return_value = test_upgrade_info_json test_yunex_upgrader = YunexUpgrader(test_upgrade_info) + test_yunex_upgrader.check_online = MagicMock(return_value=True) test_yunex_upgrader.download_blob = MagicMock() test_yunex_upgrader.run_xfer_upgrade = MagicMock(side_effect=[0, 0, -1]) test_yunex_upgrader.wait_until_online = MagicMock(return_value=0) diff --git a/services/addons/tests/geo_msg_query/test_geo_query.py b/services/addons/tests/geo_msg_query/test_geo_query.py new file mode 100644 index 00000000..021fe5d4 --- /dev/null +++ b/services/addons/tests/geo_msg_query/test_geo_query.py @@ -0,0 +1,356 @@ +import os +from pymongo import MongoClient, DESCENDING, GEOSPHERE +from datetime import datetime +from unittest.mock import MagicMock, patch +import logging +import pytest +from addons.images.geo_msg_query import geo_msg_query +from addons.images.geo_msg_query.geo_msg_query import ( + create_message, + process_message, + set_indexes, +) + + +# create_message unit tests +def test_create_message_bsm_nanoseconds(): + original_message = { + "payload": { + "data": {"coreData": {"position": {"longitude": 123.456, "latitude": 78.9}}} + }, + "metadata": { + "odeReceivedAt": "2022-01-01T12:34:56.789000Z", + "originIp": "127.0.0.1", + }, + } + msg_type = "Bsm" + + expected_message = { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [123.456, 78.9]}, + "properties": { + "id": "127.0.0.1", + "timestamp": datetime.strptime( + "2022-01-01T12:34:56.789Z", "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "msg_type": "Bsm", + }, + } + + assert create_message(original_message, msg_type) == expected_message + + +def test_create_message_bsm_milliseconds(): + original_message = { + "payload": { + "data": {"coreData": {"position": {"longitude": 123.456, "latitude": 78.9}}} + }, + "metadata": { + "odeReceivedAt": "2022-01-01T12:34:56.789Z", + "originIp": "127.0.0.1", + }, + } + msg_type = "Bsm" + + expected_message = { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [123.456, 78.9]}, + "properties": { + "id": "127.0.0.1", + "timestamp": datetime.strptime( + "2022-01-01T12:34:56.789Z", "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "msg_type": "Bsm", + }, + } + + assert create_message(original_message, msg_type) == expected_message + + +def test_create_message_psm(): + original_message = { + "payload": {"data": {"position": {"longitude": 12.34, "latitude": 56.78}}}, + "metadata": { + "odeReceivedAt": "2022-01-01T12:34:56.789000Z", + "originIp": "127.0.0.1", + }, + } + msg_type = "Psm" + + expected_message = { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [12.34, 56.78]}, + "properties": { + "id": "127.0.0.1", + "timestamp": datetime.strptime( + "2022-01-01T12:34:56.789Z", "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "msg_type": "Psm", + }, + } + + assert create_message(original_message, msg_type) == expected_message + + +def test_create_message_invalid_type(): + original_message = { + "payload": { + "data": {"coreData": {"position": {"longitude": 123.456, "latitude": 78.9}}} + }, + "metadata": { + "odeReceivedAt": "2022-01-01T12:34:56.789012Z", + "originIp": "127.0.0.1", + }, + } + msg_type = "InvalidType" + + expected_message = None + + with patch.object(logging, "warn") as mock_warn: + assert create_message(original_message, msg_type) == expected_message + mock_warn.assert_called_once_with( + "create_message: Could not create a message for type: InvalidType" + ) + + +# process_message unit tests +@patch("addons.images.geo_msg_query.geo_msg_query.create_message") +def test_process_message_inserts_new_message_when_created_successfully( + mock_process_message, +): + message = "Test message" + db = MagicMock() + collection = "test_collection" + msg_type = "test_type" + + mock_process_message.return_value = "New message" + process_message(message, db, collection, msg_type) + + db[collection].insert_one.assert_called_once() + + +@patch("addons.images.geo_msg_query.geo_msg_query.create_message") +@patch("logging.error") +def test_process_message_logs_error_when_message_creation_fails( + mock_logging, mock_process_message +): + message = "Invalid message" + db = MagicMock() + collection = "test_collection" + msg_type = "test_type" + + mock_process_message.return_value = None + process_message(message, db, collection, msg_type) + + mock_logging.assert_called_once_with( + f"process_message: Could not create a message from the input {msg_type} message: {message}" + ) + + +@patch("logging.info") +def test_set_indexes_empty(mock_logging): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.return_value = mock_collection + mock_index_info = {} + mock_collection.index_information.return_value = mock_index_info + + set_indexes(mock_db, "output_collection", "7") + + mock_collection.create_index.assert_any_call( + [ + ("properties.timestamp", DESCENDING), + ("properties.msg_type", DESCENDING), + ("geometry", GEOSPHERE), + ], + name="timestamp_geosphere_index", + ) + + mock_collection.create_index.assert_any_call( + [("properties.timestamp", DESCENDING)], + name="ttl_index", + expireAfterSeconds=604800, + ) + mock_logging.assert_any_call("Creating timestamp_geosphere_index") + mock_logging.assert_any_call("Creating ttl_index") + assert mock_logging.call_count == 3 + + +@patch("logging.info") +def test_set_indexes_ttl_recreate(mock_logging): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.return_value = mock_collection + mock_index_info = { + "ttl_index": {"expireAfterSeconds": 86400}, + "timestamp_geosphere_index": "timestamp_geosphere_index", + } + mock_collection.index_information.return_value = mock_index_info + + set_indexes(mock_db, "output_collection", "7") + + mock_collection.create_index.assert_any_call( + [("properties.timestamp", DESCENDING)], + name="ttl_index", + expireAfterSeconds=604800, + ) + mock_logging.assert_any_call("timestamp_geosphere_index already exists") + mock_logging.assert_any_call( + "ttl_index exists but with different TTL value. Recreating..." + ) + assert mock_logging.call_count == 3 + + +@patch("logging.info") +def test_set_indexes_exists(mock_logging): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_db.__getitem__.return_value = mock_collection + mock_index_info = { + "ttl_index": {"expireAfterSeconds": 604800}, + "timestamp_geosphere_index": "timestamp_geosphere_index", + } + mock_collection.index_information.return_value = mock_index_info + + set_indexes(mock_db, "output_collection", "7") + + mock_logging.assert_any_call("timestamp_geosphere_index already exists") + mock_logging.assert_any_call("ttl_index already exists with the correct TTL value") + assert mock_logging.call_count == 3 + + +# watch_collection method unit tests +@patch("logging.debug") +@patch("addons.images.geo_msg_query.geo_msg_query.process_message") +def test_watch_collection_success(mock_process_message, mock_logging): + geo_msg_query.process_message = mock_process_message + + mock_db = MagicMock() + mock_input_collection = "OdeBsmJson" + mock_output_collection = "GeoMsg" + mock_change = { + "fullDocument": {"message": "Test message"}, + "operationType": "insert", + } + mock_stream = MagicMock() + mock_stream.__iter__.return_value = [mock_change] + mock_db.__getitem__.return_value.watch.return_value.__enter__.return_value = ( + mock_stream + ) + + geo_msg_query.watch_collection( + mock_db, mock_input_collection, mock_output_collection + ) + + mock_process_message.assert_called_once_with( + mock_change["fullDocument"], mock_db, mock_output_collection, "Bsm" + ) + mock_logging.assert_any_call("Bsm Count: 1") + + +@patch("logging.debug") +@patch("addons.images.geo_msg_query.geo_msg_query.process_message") +def test_watch_collection_ttl_ignore(mock_process_message, mock_logging): + geo_msg_query.process_message = mock_process_message + + mock_db = MagicMock() + mock_input_collection = "OdeBsmJson" + mock_output_collection = "GeoMsg" + mock_change = { + "operationType": "delete", + } + mock_stream = MagicMock() + mock_stream.__iter__.return_value = [mock_change] + mock_db.__getitem__.return_value.watch.return_value.__enter__.return_value = ( + mock_stream + ) + + geo_msg_query.watch_collection( + mock_db, mock_input_collection, mock_output_collection + ) + + mock_logging.assert_any_call(f"Ignoring change with operationType: delete") + + +@patch("logging.error") +def test_watch_collection_exception(mock_logging): + mock_db = MagicMock() + mock_input_collection = "OdeBsmJson" + mock_output_collection = "GeoMsg" + mock_error = Exception("Test error") + mock_db.__getitem__.side_effect = mock_error + + geo_msg_query.watch_collection( + mock_db, mock_input_collection, mock_output_collection + ) + + mock_logging.assert_any_call( + "An error occurred while watching collection: OdeBsmJson" + ) + mock_logging.assert_any_call(str(mock_error)) + + +# run method unit tests +@patch.dict( + os.environ, + { + "MONGO_DB_URI": "mongodb://localhost:27017", + "MONGO_DB_NAME": "test_db", + "MONGO_INPUT_COLLECTIONS": "OdeBsmJson,OdePsmJson", + "MONGO_GEO_OUTPUT_COLLECTION": "GeoMsg", + "MONGO_TTL": "7", + }, +) +@patch("addons.images.geo_msg_query.geo_msg_query.watch_collection") +@patch("addons.images.geo_msg_query.geo_msg_query.set_indexes") +@patch("addons.images.geo_msg_query.geo_msg_query.set_mongo_client") +@patch("addons.images.geo_msg_query.geo_msg_query.ThreadPoolExecutor") +def test_run( + mock_thread_pool_executor, + mock_set_mongo_client, + mock_set_indexes, + mock_watch_collection, +): + + mock_db = MagicMock() + + geo_msg_query.set_mongo_client = mock_set_mongo_client + mock_set_mongo_client.return_value = mock_db + geo_msg_query.set_indexes = mock_set_indexes + geo_msg_query.watch_collection = mock_watch_collection + + geo_msg_query.run() + + mock_set_mongo_client.assert_called_with("mongodb://localhost:27017", "test_db") + mock_set_indexes.assert_called_with(mock_db, "GeoMsg", "7") + + mock_thread_pool_executor.assert_called_once_with(max_workers=5) + mock_executer = mock_thread_pool_executor.return_value.__enter__.return_value + mock_executer.submit.assert_any_call( + mock_watch_collection, mock_db, "OdePsmJson", "GeoMsg" + ) + mock_executer.submit.assert_any_call( + mock_watch_collection, mock_db, "OdeBsmJson", "GeoMsg" + ) + + +@patch("addons.images.geo_msg_query.geo_msg_query.set_indexes") +@patch("addons.images.geo_msg_query.geo_msg_query.set_mongo_client") +@patch("addons.images.geo_msg_query.geo_msg_query.ThreadPoolExecutor") +@patch("logging.error") +def test_run_exit( + mock_logging, mock_thread_pool_executor, mock_set_mongo_client, mock_set_indexes +): + with pytest.raises(SystemExit) as e: + geo_msg_query.run() + + assert str(e.value) == "Environment variables are not set! Exiting." + mock_logging.assert_any_call("Environment variables are not set! Exiting.") + mock_thread_pool_executor.assert_not_called() + mock_set_mongo_client.assert_not_called() + mock_set_indexes.assert_not_called() + mock_thread_pool_executor.assert_not_called() + + +if __name__ == "__main__": + pytest.main() diff --git a/services/addons/tests/iss_health_check/test_iss_health_checker.py b/services/addons/tests/iss_health_check/test_iss_health_checker.py index ea4a4b38..0e2069d3 100644 --- a/services/addons/tests/iss_health_check/test_iss_health_checker.py +++ b/services/addons/tests/iss_health_check/test_iss_health_checker.py @@ -2,6 +2,7 @@ import os from addons.images.iss_health_check import iss_health_checker +from addons.images.iss_health_check.iss_health_checker import RsuDataWrapper @patch("addons.images.iss_health_check.iss_health_checker.pgquery.query_db") @@ -10,7 +11,8 @@ def test_get_rsu_data_no_data(mock_query_db): result = iss_health_checker.get_rsu_data() # check - assert result == {} + expected = RsuDataWrapper({}) + assert result == expected mock_query_db.assert_called_once() mock_query_db.assert_called_with( "SELECT jsonb_build_object('rsu_id', rsu_id, 'iss_scms_id', iss_scms_id) FROM public.rsus WHERE iss_scms_id IS NOT NULL ORDER BY rsu_id" @@ -27,7 +29,7 @@ def test_get_rsu_data_with_data(mock_query_db): ] result = iss_health_checker.get_rsu_data() - expected_result = {"ABC": {"rsu_id": 1}, "DEF": {"rsu_id": 2}, "GHI": {"rsu_id": 3}} + expected_result = RsuDataWrapper({"ABC": {"rsu_id": 1}, "DEF": {"rsu_id": 2}, "GHI": {"rsu_id": 3}}) # check assert result == expected_result @@ -52,7 +54,7 @@ def test_get_rsu_data_with_data(mock_query_db): def test_get_scms_status_data( mock_get_rsu_data, mock_get_token, mock_requests, mock_response ): - mock_get_rsu_data.return_value = {"ABC": {"rsu_id": 1}, "DEF": {"rsu_id": 2}} + mock_get_rsu_data.return_value = RsuDataWrapper({"ABC": {"rsu_id": 1}, "DEF": {"rsu_id": 2}}) mock_get_token.get_token.return_value = "test-token" mock_requests.get.return_value = mock_response mock_response.json.side_effect = [ @@ -141,3 +143,66 @@ def test_insert_scms_data(mock_write_db, mock_datetime): "('2022-11-03T00:00:00.000Z', '0', NULL, 2)" ) mock_write_db.assert_called_with(expectedQuery) + + +@patch("addons.images.iss_health_check.iss_health_checker.datetime") +@patch("addons.images.iss_health_check.iss_health_checker.pgquery.write_db") +def test_insert_scms_data_no_rsu_id(mock_write_db, mock_datetime): + mock_datetime.strftime.return_value = "2022-11-03T00:00:00.000Z" + test_data = { + "ABC": { + "deviceHealth": "Healthy", + "expiration": "2022-11-02T00:00:00.000Z", + }, + "DEF": {"rsu_id": 2, "deviceHealth": "Unhealthy", "expiration": None}, + } + # call + iss_health_checker.insert_scms_data(test_data) + + expectedQuery = ( + 'INSERT INTO public.scms_health("timestamp", health, expiration, rsu_id) VALUES ' + "('2022-11-03T00:00:00.000Z', '0', NULL, 2)" + ) + mock_write_db.assert_called_with(expectedQuery) + + +@patch("addons.images.iss_health_check.iss_health_checker.datetime") +@patch("addons.images.iss_health_check.iss_health_checker.pgquery.write_db") +def test_insert_scms_data_no_deviceHealth(mock_write_db, mock_datetime): + mock_datetime.strftime.return_value = "2022-11-03T00:00:00.000Z" + test_data = { + "ABC": { + "rsu_id": 1, + "expiration": "2022-11-02T00:00:00.000Z", + }, + "DEF": {"rsu_id": 2, "deviceHealth": "Unhealthy", "expiration": None}, + } + # call + iss_health_checker.insert_scms_data(test_data) + + expectedQuery = ( + 'INSERT INTO public.scms_health("timestamp", health, expiration, rsu_id) VALUES ' + "('2022-11-03T00:00:00.000Z', '0', NULL, 2)" + ) + mock_write_db.assert_called_with(expectedQuery) + + +@patch("addons.images.iss_health_check.iss_health_checker.datetime") +@patch("addons.images.iss_health_check.iss_health_checker.pgquery.write_db") +def test_insert_scms_data_no_expiration(mock_write_db, mock_datetime): + mock_datetime.strftime.return_value = "2022-11-03T00:00:00.000Z" + test_data = { + "ABC": { + "rsu_id": 1, + "deviceHealth": "Healthy", + }, + "DEF": {"rsu_id": 2, "deviceHealth": "Unhealthy", "expiration": "test"}, + } + # call + iss_health_checker.insert_scms_data(test_data) + + expectedQuery = ( + 'INSERT INTO public.scms_health("timestamp", health, expiration, rsu_id) VALUES ' + "('2022-11-03T00:00:00.000Z', '0', 'test', 2)" + ) + mock_write_db.assert_called_with(expectedQuery) \ No newline at end of file diff --git a/services/addons/tests/iss_health_check/test_iss_token.py b/services/addons/tests/iss_health_check/test_iss_token.py index 392b041e..37c73eaa 100644 --- a/services/addons/tests/iss_health_check/test_iss_token.py +++ b/services/addons/tests/iss_health_check/test_iss_token.py @@ -1,230 +1,478 @@ -from unittest.mock import patch, MagicMock -import os -import json - -from addons.images.iss_health_check import iss_token - - -@patch( - "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" -) -def test_create_secret(mock_sm_client): - iss_token.create_secret(mock_sm_client, "test-secret_id", "test-parent") - expected_request = { - "parent": "test-parent", - "secret_id": "test-secret_id", - "secret": {"replication": {"automatic": {}}}, - } - mock_sm_client.create_secret.assert_called_with(request=expected_request) - - -@patch( - "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" -) -@patch("addons.images.iss_health_check.iss_token.secretmanager") -def test_check_if_secret_exists_true(mock_secretmanager, mock_sm_client): - mock_secretmanager.ListSecretsRequest.return_value = "list-request" - - item_match = MagicMock() - item_match.name = "proj/test-proj/secret/test-secret_id" - mock_list_values = [item_match] - mock_sm_client.list_secrets.return_value = mock_list_values - - actual_value = iss_token.check_if_secret_exists( - mock_sm_client, "test-secret_id", "test-parent" - ) - mock_secretmanager.ListSecretsRequest.assert_called_with(parent="test-parent") - mock_sm_client.list_secrets.assert_called_with(request="list-request") - assert actual_value == True - - -@patch( - "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" -) -@patch("addons.images.iss_health_check.iss_token.secretmanager") -def test_check_if_secret_exists_false(mock_secretmanager, mock_sm_client): - mock_secretmanager.ListSecretsRequest.return_value = "list-request" - - item_not_match = MagicMock() - item_not_match.name = "proj/test-proj/secret/test-secret" - mock_list_values = [item_not_match] - mock_sm_client.list_secrets.return_value = mock_list_values - - actual_value = iss_token.check_if_secret_exists( - mock_sm_client, "test-secret_id", "test-parent" - ) - mock_secretmanager.ListSecretsRequest.assert_called_with(parent="test-parent") - mock_sm_client.list_secrets.assert_called_with(request="list-request") - assert actual_value == False - - -@patch( - "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" -) -def test_get_latest_secret_version(mock_sm_client): - mock_response = MagicMock() - mock_response.payload.data = str.encode('{"message": "Secret payload data"}') - mock_sm_client.access_secret_version.return_value = mock_response - - actual_value = iss_token.get_latest_secret_version( - mock_sm_client, "test-secret_id", "test-parent" - ) - mock_sm_client.access_secret_version.assert_called_with( - request={"name": "test-parent/secrets/test-secret_id/versions/latest"} - ) - assert actual_value == {"message": "Secret payload data"} - - -@patch( - "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" -) -def test_add_secret_version(mock_sm_client): - secret_id = "test-secret_id" - parent = "test-parent" - data = {"message": "Secret payload data"} - iss_token.add_secret_version(mock_sm_client, secret_id, parent, data) - - expected_request = { - "parent": f"{parent}/secrets/{secret_id}", - "payload": {"data": str.encode(json.dumps(data))}, - } - mock_sm_client.add_secret_version.assert_called_with(request=expected_request) - - -@patch.dict( - os.environ, - { - "PROJECT_ID": "test-proj", - "ISS_API_KEY": "test-api-key", - "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", - "ISS_API_KEY_NAME": "test-api-key-name", - }, -) -@patch("addons.images.iss_health_check.iss_token.requests.Response") -@patch("addons.images.iss_health_check.iss_token.requests") -@patch("addons.images.iss_health_check.iss_token.uuid") -@patch("addons.images.iss_health_check.iss_token.add_secret_version") -@patch("addons.images.iss_health_check.iss_token.create_secret") -@patch("addons.images.iss_health_check.iss_token.check_if_secret_exists") -@patch("addons.images.iss_health_check.iss_token.secretmanager") -def test_get_token_create_secret( - mock_secretmanager, - mock_check_if_secret_exists, - mock_create_secret, - mock_add_secret_version, - mock_uuid, - mock_requests, - mock_response, -): - # Mock every major dependency - mock_sm_client = MagicMock() - mock_secretmanager.SecretManagerServiceClient.return_value = mock_sm_client - mock_check_if_secret_exists.return_value = False - mock_uuid.uuid4.return_value = 12345 - mock_requests.post.return_value = mock_response - mock_response.json.return_value = {"Item": "new-iss-token"} - - # Call function - expected_value = "new-iss-token" - actual_value = iss_token.get_token() - - # Check if iss_token function calls were made correctly - mock_check_if_secret_exists.assert_called_with( - mock_sm_client, "iss-token-secret", "projects/test-proj" - ) - mock_create_secret.assert_called_with( - mock_sm_client, "iss-token-secret", "projects/test-proj" - ) - mock_add_secret_version.assert_called_with( - mock_sm_client, - "iss-token-secret", - "projects/test-proj", - {"name": "test-api-key-name_12345", "token": expected_value}, - ) - - # Check if HTTP requests were made correctly - expected_headers = {"x-api-key": "test-api-key"} - expected_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} - mock_requests.post.assert_called_with( - "https://api.dm.iss-scms.com/api/test-token", - json=expected_body, - headers=expected_headers, - ) - - # Assert final value - assert actual_value == expected_value - - -@patch.dict( - os.environ, - { - "PROJECT_ID": "test-proj", - "ISS_API_KEY": "test-api-key", - "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", - "ISS_API_KEY_NAME": "test-api-key-name", - }, -) -@patch("addons.images.iss_health_check.iss_token.requests.Response") -@patch("addons.images.iss_health_check.iss_token.requests") -@patch("addons.images.iss_health_check.iss_token.uuid") -@patch("addons.images.iss_health_check.iss_token.add_secret_version") -@patch("addons.images.iss_health_check.iss_token.get_latest_secret_version") -@patch("addons.images.iss_health_check.iss_token.check_if_secret_exists") -@patch("addons.images.iss_health_check.iss_token.secretmanager") -def test_get_token_secret_exists( - mock_secretmanager, - mock_check_if_secret_exists, - mock_get_latest_secret_version, - mock_add_secret_version, - mock_uuid, - mock_requests, - mock_response, -): - # Mock every major dependency - mock_sm_client = MagicMock() - mock_secretmanager.SecretManagerServiceClient.return_value = mock_sm_client - mock_check_if_secret_exists.return_value = True - mock_get_latest_secret_version.return_value = { - "name": "test-api-key-name_01234", - "token": "old-token", - } - mock_uuid.uuid4.return_value = 12345 - mock_requests.post.return_value = mock_response - mock_response.json.return_value = {"Item": "new-iss-token"} - - # Call function - expected_value = "new-iss-token" - actual_value = iss_token.get_token() - - # Check if iss_token function calls were made correctly - mock_check_if_secret_exists.assert_called_with( - mock_sm_client, "iss-token-secret", "projects/test-proj" - ) - mock_get_latest_secret_version.assert_called_with( - mock_sm_client, "iss-token-secret", "projects/test-proj" - ) - mock_add_secret_version.assert_called_with( - mock_sm_client, - "iss-token-secret", - "projects/test-proj", - {"name": "test-api-key-name_12345", "token": expected_value}, - ) - - # Check if HTTP requests were made correctly - expected_headers = {"x-api-key": "old-token"} - expected_post_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} - mock_requests.post.assert_called_with( - "https://api.dm.iss-scms.com/api/test-token", - json=expected_post_body, - headers=expected_headers, - ) - - expected_delete_body = {"friendlyName": "test-api-key-name_01234"} - mock_requests.delete.assert_called_with( - "https://api.dm.iss-scms.com/api/test-token", - json=expected_delete_body, - headers=expected_headers, - ) - - # Assert final value - assert actual_value == expected_value +from unittest.mock import patch, MagicMock +import os +import json + +import pytest + +from addons.images.iss_health_check import iss_token + +# --------------------- Storage Type tests --------------------- + +@patch.dict( + os.environ, + { + "STORAGE_TYPE": "gcp", + }, +) +def test_get_storage_type_gcp(): + actual_value = iss_token.get_storage_type() + assert actual_value == "gcp" + + +@patch.dict( + os.environ, + { + "STORAGE_TYPE": "postgres", + }, +) +def test_get_storage_type_postgres(): + actual_value = iss_token.get_storage_type() + assert actual_value == "postgres" + + +@patch.dict( + os.environ, + { + "STORAGE_TYPE": "GCP", + }, +) +def test_get_storage_type_gcp_case_insensitive(): + actual_value = iss_token.get_storage_type() + assert actual_value == "gcp" + + +@patch.dict( + os.environ, + { + "STORAGE_TYPE": "POSTGRES", + }, +) +def test_get_storage_type_postgres_case_insensitive(): + actual_value = iss_token.get_storage_type() + assert actual_value == "postgres" + + +@patch.dict( + os.environ, + { + "STORAGE_TYPE": "test", + }, +) +def test_get_storage_type_invalid(): + with pytest.raises(SystemExit): + iss_token.get_storage_type() + + +@patch.dict( + os.environ, + { + + }, +) +def test_get_storage_type_unset(): + with pytest.raises(SystemExit): + iss_token.get_storage_type() + +# --------------------- end of Storage Type tests --------------------- + + +# --------------------- GCP tests --------------------- +@patch( + "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" +) +def test_create_secret(mock_sm_client): + iss_token.create_secret(mock_sm_client, "test-secret_id", "test-parent") + expected_request = { + "parent": "test-parent", + "secret_id": "test-secret_id", + "secret": {"replication": {"automatic": {}}}, + } + mock_sm_client.create_secret.assert_called_with(request=expected_request) + + +@patch( + "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" +) +@patch("addons.images.iss_health_check.iss_token.secretmanager") +def test_check_if_secret_exists_true(mock_secretmanager, mock_sm_client): + mock_secretmanager.ListSecretsRequest.return_value = "list-request" + + item_match = MagicMock() + item_match.name = "proj/test-proj/secret/test-secret_id" + mock_list_values = [item_match] + mock_sm_client.list_secrets.return_value = mock_list_values + + actual_value = iss_token.check_if_secret_exists( + mock_sm_client, "test-secret_id", "test-parent" + ) + mock_secretmanager.ListSecretsRequest.assert_called_with(parent="test-parent") + mock_sm_client.list_secrets.assert_called_with(request="list-request") + assert actual_value == True + + +@patch( + "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" +) +@patch("addons.images.iss_health_check.iss_token.secretmanager") +def test_check_if_secret_exists_false(mock_secretmanager, mock_sm_client): + mock_secretmanager.ListSecretsRequest.return_value = "list-request" + + item_not_match = MagicMock() + item_not_match.name = "proj/test-proj/secret/test-secret" + mock_list_values = [item_not_match] + mock_sm_client.list_secrets.return_value = mock_list_values + + actual_value = iss_token.check_if_secret_exists( + mock_sm_client, "test-secret_id", "test-parent" + ) + mock_secretmanager.ListSecretsRequest.assert_called_with(parent="test-parent") + mock_sm_client.list_secrets.assert_called_with(request="list-request") + assert actual_value == False + + +@patch( + "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" +) +def test_get_latest_secret_version(mock_sm_client): + mock_response = MagicMock() + mock_response.payload.data = str.encode('{"message": "Secret payload data"}') + mock_sm_client.access_secret_version.return_value = mock_response + + actual_value = iss_token.get_latest_secret_version( + mock_sm_client, "test-secret_id", "test-parent" + ) + mock_sm_client.access_secret_version.assert_called_with( + request={"name": "test-parent/secrets/test-secret_id/versions/latest"} + ) + assert actual_value == {"message": "Secret payload data"} + + +@patch( + "addons.images.iss_health_check.iss_token.secretmanager.SecretManagerServiceClient" +) +def test_add_secret_version(mock_sm_client): + secret_id = "test-secret_id" + parent = "test-parent" + data = {"message": "Secret payload data"} + iss_token.add_secret_version(mock_sm_client, secret_id, parent, data) + + expected_request = { + "parent": f"{parent}/secrets/{secret_id}", + "payload": {"data": str.encode(json.dumps(data))}, + } + mock_sm_client.add_secret_version.assert_called_with(request=expected_request) + + +@patch.dict( + os.environ, + { + "PROJECT_ID": "test-proj", + "ISS_API_KEY": "test-api-key", + "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", + "ISS_API_KEY_NAME": "test-api-key-name", + "STORAGE_TYPE": "gcp", + }, +) +@patch("addons.images.iss_health_check.iss_token.requests.Response") +@patch("addons.images.iss_health_check.iss_token.requests") +@patch("addons.images.iss_health_check.iss_token.uuid") +@patch("addons.images.iss_health_check.iss_token.add_secret_version") +@patch("addons.images.iss_health_check.iss_token.create_secret") +@patch("addons.images.iss_health_check.iss_token.check_if_secret_exists") +@patch("addons.images.iss_health_check.iss_token.secretmanager") +def test_get_token_create_secret( + mock_secretmanager, + mock_check_if_secret_exists, + mock_create_secret, + mock_add_secret_version, + mock_uuid, + mock_requests, + mock_response, +): + # Mock every major dependency + mock_sm_client = MagicMock() + mock_secretmanager.SecretManagerServiceClient.return_value = mock_sm_client + mock_check_if_secret_exists.return_value = False + mock_uuid.uuid4.return_value = 12345 + mock_requests.post.return_value = mock_response + mock_response.json.return_value = {"Item": "new-iss-token"} + + # Call function + expected_value = "new-iss-token" + actual_value = iss_token.get_token() + + # Check if iss_token function calls were made correctly + mock_check_if_secret_exists.assert_called_with( + mock_sm_client, "iss-token-secret", "projects/test-proj" + ) + mock_create_secret.assert_called_with( + mock_sm_client, "iss-token-secret", "projects/test-proj" + ) + mock_add_secret_version.assert_called_with( + mock_sm_client, + "iss-token-secret", + "projects/test-proj", + {"name": "test-api-key-name_12345", "token": expected_value}, + ) + + # Check if HTTP requests were made correctly + expected_headers = {"x-api-key": "test-api-key"} + expected_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} + mock_requests.post.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_body, + headers=expected_headers, + ) + + # Assert final value + assert actual_value == expected_value + + +@patch.dict( + os.environ, + { + "PROJECT_ID": "test-proj", + "ISS_API_KEY": "test-api-key", + "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", + "ISS_API_KEY_NAME": "test-api-key-name", + "STORAGE_TYPE": "gcp", + }, +) +@patch("addons.images.iss_health_check.iss_token.requests.Response") +@patch("addons.images.iss_health_check.iss_token.requests") +@patch("addons.images.iss_health_check.iss_token.uuid") +@patch("addons.images.iss_health_check.iss_token.add_secret_version") +@patch("addons.images.iss_health_check.iss_token.get_latest_secret_version") +@patch("addons.images.iss_health_check.iss_token.check_if_secret_exists") +@patch("addons.images.iss_health_check.iss_token.secretmanager") +def test_get_token_secret_exists( + mock_secretmanager, + mock_check_if_secret_exists, + mock_get_latest_secret_version, + mock_add_secret_version, + mock_uuid, + mock_requests, + mock_response, +): + # Mock every major dependency + mock_sm_client = MagicMock() + mock_secretmanager.SecretManagerServiceClient.return_value = mock_sm_client + mock_check_if_secret_exists.return_value = True + mock_get_latest_secret_version.return_value = { + "name": "test-api-key-name_01234", + "token": "old-token", + } + mock_uuid.uuid4.return_value = 12345 + mock_requests.post.return_value = mock_response + mock_response.json.return_value = {"Item": "new-iss-token"} + + # Call function + expected_value = "new-iss-token" + actual_value = iss_token.get_token() + + # Check if iss_token function calls were made correctly + mock_check_if_secret_exists.assert_called_with( + mock_sm_client, "iss-token-secret", "projects/test-proj" + ) + mock_get_latest_secret_version.assert_called_with( + mock_sm_client, "iss-token-secret", "projects/test-proj" + ) + mock_add_secret_version.assert_called_with( + mock_sm_client, + "iss-token-secret", + "projects/test-proj", + {"name": "test-api-key-name_12345", "token": expected_value}, + ) + + # Check if HTTP requests were made correctly + expected_headers = {"x-api-key": "old-token"} + expected_post_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} + mock_requests.post.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_post_body, + headers=expected_headers, + ) + + expected_delete_body = {"friendlyName": "test-api-key-name_01234"} + mock_requests.delete.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_delete_body, + headers=expected_headers, + ) + + # Assert final value + assert actual_value == expected_value + +# --------------------- end of GCP tests --------------------- + + +# --------------------- Postgres tests --------------------- + +@patch( + "addons.images.iss_health_check.iss_token.pgquery", +) +def test_check_if_data_exists_true(mock_pgquery): + mock_pgquery.query_db.return_value = [(1,)] + actual_value = iss_token.check_if_data_exists("test-table-name") + expected_query = ( + "SELECT * FROM test-table-name" + ) + mock_pgquery.query_db.assert_called_with(expected_query) + assert actual_value == True + + +@patch( + "addons.images.iss_health_check.iss_token.pgquery", +) +def test_check_if_data_exists_false(mock_pgquery): + mock_pgquery.query_db.return_value = [] + actual_value = iss_token.check_if_data_exists("test-table-name") + expected_query = ( + "SELECT * FROM test-table-name" + ) + mock_pgquery.query_db.assert_called_with(expected_query) + assert actual_value == False + + +@patch( + "addons.images.iss_health_check.iss_token.pgquery", +) +def test_add_data(mock_pgquery): + iss_token.add_data("test-table-name", "test-common-name", "test-token") + expected_query = ( + "INSERT INTO test-table-name (common_name, token) " + "VALUES ('test-common-name', 'test-token')" + ) + mock_pgquery.write_db.assert_called_with(expected_query) + + +@patch( + "addons.images.iss_health_check.iss_token.pgquery", +) +def test_get_latest_data(mock_pgquery): + mock_pgquery.query_db.return_value = [(1, "test-common-name", "test-token")] + actual_value = iss_token.get_latest_data("test-table-name") + expected_query = ( + "SELECT * FROM test-table-name ORDER BY iss_key_id DESC LIMIT 1" + ) + mock_pgquery.query_db.assert_called_with(expected_query) + assert actual_value == {"id": 1, "name": "test-common-name", "token": "test-token"} + + +@patch.dict( + os.environ, + { + "PROJECT_ID": "test-proj", + "ISS_API_KEY": "test-api-key", + "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", + "ISS_API_KEY_NAME": "test-api-key-name", + "STORAGE_TYPE": "postgres", + "ISS_KEY_TABLE_NAME": "test-table-name", + }, +) +@patch("addons.images.iss_health_check.iss_token.requests.Response") +@patch("addons.images.iss_health_check.iss_token.requests") +@patch("addons.images.iss_health_check.iss_token.uuid") +@patch("addons.images.iss_health_check.iss_token.add_data") +@patch("addons.images.iss_health_check.iss_token.check_if_data_exists") +def test_get_token_data_does_not_exist( + mock_check_if_data_exists, + mock_add_data, + mock_uuid, + mock_requests, + mock_response, +): + # Mock every major dependency + mock_check_if_data_exists.return_value = False + mock_uuid.uuid4.return_value = 12345 + mock_requests.post.return_value = mock_response + mock_response.json.return_value = {"Item": "new-iss-token"} + + # Call function + result = iss_token.get_token() + + # Check if iss_token function calls were made correctly + mock_check_if_data_exists.assert_called_with("test-table-name") + mock_add_data.assert_called_with( + "test-table-name", "test-api-key-name_12345", "new-iss-token" + ) + + # Check if HTTP requests were made correctly + expected_headers = {"x-api-key": "test-api-key"} + expected_post_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} + mock_requests.post.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_post_body, + headers=expected_headers, + ) + + # Assert final value + assert result == "new-iss-token" + + +@patch.dict( + os.environ, + { + "PROJECT_ID": "test-proj", + "ISS_API_KEY": "test-api-key", + "ISS_SCMS_TOKEN_REST_ENDPOINT": "https://api.dm.iss-scms.com/api/test-token", + "ISS_API_KEY_NAME": "test-api-key-name", + "STORAGE_TYPE": "postgres", + "ISS_KEY_TABLE_NAME": "test-table-name", + }, +) +@patch("addons.images.iss_health_check.iss_token.requests.Response") +@patch("addons.images.iss_health_check.iss_token.requests") +@patch("addons.images.iss_health_check.iss_token.uuid") +@patch("addons.images.iss_health_check.iss_token.add_data") +@patch("addons.images.iss_health_check.iss_token.check_if_data_exists") +@patch("addons.images.iss_health_check.iss_token.get_latest_data") +def test_get_token_data_exists( + mock_get_latest_data, + mock_check_if_data_exists, + mock_add_data, + mock_uuid, + mock_requests, + mock_response, +): + # Mock every major dependency + mock_check_if_data_exists.return_value = True + mock_get_latest_data.return_value = { + "id": 1, + "name": "test-api-key-name_01234", + "token": "old-token", + } + mock_uuid.uuid4.return_value = 12345 + mock_requests.post.return_value = mock_response + mock_response.json.return_value = {"Item": "new-iss-token"} + + # Call function + result = iss_token.get_token() + + # Check if iss_token function calls were made correctly + mock_check_if_data_exists.assert_called_with("test-table-name") + mock_get_latest_data.assert_called_with("test-table-name") + mock_add_data.assert_called_with( + "test-table-name", "test-api-key-name_12345", "new-iss-token" + ) + + # Check if HTTP requests were made correctly + expected_headers = {"x-api-key": "old-token"} + expected_post_body = {"friendlyName": "test-api-key-name_12345", "expireDays": 1} + mock_requests.post.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_post_body, + headers=expected_headers, + ) + + expected_delete_body = {"friendlyName": "test-api-key-name_01234"} + mock_requests.delete.assert_called_with( + "https://api.dm.iss-scms.com/api/test-token", + json=expected_delete_body, + headers=expected_headers, + ) + + # Assert final value + assert result == "new-iss-token" + +# --------------------- end of Postgres tests --------------------- \ No newline at end of file diff --git a/services/addons/tests/rsu_ping/test_purger.py b/services/addons/tests/rsu_ping/test_purger.py deleted file mode 100644 index 722bddc3..00000000 --- a/services/addons/tests/rsu_ping/test_purger.py +++ /dev/null @@ -1,62 +0,0 @@ -from mock import MagicMock, call, patch -from datetime import datetime, timedelta -from addons.images.rsu_ping import purger -from freezegun import freeze_time - - -@freeze_time("2023-07-06") -@patch("addons.images.rsu_ping.purger.pgquery.query_db") -def test_get_last_online_rsu_records(mock_query_db): - # mock - mock_query_db.return_value = [(1, 1, datetime.now())] - - # call - result = purger.get_last_online_rsu_records() - - # check - assert len(result) == 1 - assert result[0][0] == 1 - assert result[0][1] == 1 - assert result[0][2].strftime("%Y/%m/%d") == "2023/07/06" - - -@freeze_time("2023-07-06") -@patch("addons.images.rsu_ping.purger.pgquery.write_db") -def test_purge_ping_data(mock_write_db): - now_dt = datetime.now() - purger.get_last_online_rsu_records = MagicMock( - return_value=[ - [0, 0, now_dt - timedelta(hours=10)], - [1, 1, now_dt - timedelta(days=3)], - ] - ) - purger.logging.info = MagicMock() - purger.logging.debug = MagicMock() - - purger.purge_ping_data(24) - - purger.get_last_online_rsu_records.assert_called_once() - mock_write_db.assert_has_calls( - [ - call( - "DELETE FROM public.ping WHERE rsu_id = 0 AND timestamp < '2023/07/05T00:00:00'::timestamp" - ), - call("DELETE FROM public.ping WHERE rsu_id = 1 AND ping_id != 1"), - ] - ) - purger.logging.info.assert_called_once() - - -@freeze_time("2023-07-06") -@patch("addons.images.rsu_ping.purger.pgquery.write_db") -def test_purge_ping_data_none(mock_write_db): - now_dt = datetime.now() - purger.get_last_online_rsu_records = MagicMock(return_value=[]) - purger.logging.info = MagicMock() - purger.logging.debug = MagicMock() - - purger.purge_ping_data(24) - - purger.get_last_online_rsu_records.assert_called_once() - mock_write_db.assert_not_called() - purger.logging.info.assert_called_once() diff --git a/services/addons/tests/rsu_status_check/data/rsu_snmp_fetch_data.py b/services/addons/tests/rsu_status_check/data/rsu_snmp_fetch_data.py new file mode 100644 index 00000000..dac3dbe4 --- /dev/null +++ b/services/addons/tests/rsu_status_check/data/rsu_snmp_fetch_data.py @@ -0,0 +1,81 @@ +rsu_list = [ + { + "rsu_id": 1, + "ipv4_address": "5.5.5.5", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 2, + "ipv4_address": "6.6.6.6", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 3, + "ipv4_address": "7.7.7.7", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 4, + "ipv4_address": "8.8.8.8", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, +] + +# test_get_rsu_list + +get_rsu_list_query_string = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rd.rsu_id, rd.ipv4_address, snmp.username AS snmp_username, snmp.password AS snmp_password, sver.version_code AS snmp_version " + "FROM public.rsus AS rd " + "LEFT JOIN public.snmp_credentials AS snmp ON snmp.snmp_credential_id = rd.snmp_credential_id " + "LEFT JOIN public.snmp_versions AS sver ON sver.snmp_version_id = rd.snmp_version_id" + ") as row" +) + +query_rsu_list = [ + ( + { + "rsu_id": 1, + "ipv4_address": "5.5.5.5", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + ), + ( + { + "rsu_id": 2, + "ipv4_address": "6.6.6.6", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + ), + ( + { + "rsu_id": 3, + "ipv4_address": "7.7.7.7", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, + ), + ( + { + "rsu_id": 4, + "ipv4_address": "8.8.8.8", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, + ), +] diff --git a/services/addons/tests/rsu_status_check/test_purger.py b/services/addons/tests/rsu_status_check/test_purger.py new file mode 100644 index 00000000..21c3f40f --- /dev/null +++ b/services/addons/tests/rsu_status_check/test_purger.py @@ -0,0 +1,79 @@ +from mock import MagicMock, call, patch +from datetime import datetime, timedelta +from addons.images.rsu_status_check import purger +from freezegun import freeze_time + + +@patch("addons.images.rsu_status_check.purger.pgquery.query_db") +def test_get_all_rsus(mock_query_db): + # mock + mock_query_db.return_value = [ + ({"rsu_id": 1},), + ({"rsu_id": 2},), + ({"rsu_id": 3},), + ({"rsu_id": 4},), + ] + + # call + result = purger.get_all_rsus() + + # check + assert 1 in result and result[1] is None + assert 2 in result and result[2] is None + assert 3 in result and result[3] is None + assert 4 in result and result[4] is None + assert 5 not in result + + +@patch("addons.images.rsu_status_check.purger.pgquery.query_db") +def test_get_last_online_rsu_records(mock_query_db): + # mock + mock_query_db.return_value = [ + ({"ping_id": 1, "rsu_id": 1, "timestamp": "2023-07-06T00:00:00"},), + ] + rsu_dict = {1: None} + + # call + result = purger.get_last_online_rsu_records(rsu_dict) + + # check + assert len(result) == 1 + assert result[1]["ping_id"] == 1 + assert result[1]["timestamp"].strftime("%Y/%m/%d") == "2023/07/06" + + +@freeze_time("2023-07-06") +@patch("addons.images.rsu_status_check.purger.get_all_rsus", MagicMock()) +@patch("addons.images.rsu_status_check.purger.get_last_online_rsu_records") +@patch("addons.images.rsu_status_check.purger.pgquery.write_db") +def test_purge_ping_data(mock_write_db, mock_glorr): + now_dt = datetime.now() + mock_glorr.return_value = { + 1: {"ping_id": 1, "timestamp": now_dt - timedelta(hours=10)}, + 2: {"ping_id": 2, "timestamp": now_dt - timedelta(days=3)}, + } + + purger.purge_ping_data(24) + + purger.get_last_online_rsu_records.assert_called_once() + mock_write_db.assert_has_calls( + [ + call( + "DELETE FROM public.ping WHERE rsu_id = 1 AND timestamp < '2023-07-05T00:00:00'::timestamp" + ), + call("DELETE FROM public.ping WHERE rsu_id = 2 AND ping_id != 2"), + ] + ) + + +@freeze_time("2023-07-06") +@patch("addons.images.rsu_status_check.purger.get_all_rsus", MagicMock()) +@patch("addons.images.rsu_status_check.purger.get_last_online_rsu_records") +@patch("addons.images.rsu_status_check.purger.pgquery.write_db") +def test_purge_ping_data_none(mock_write_db, mock_glorr): + mock_glorr.return_value = {} + + purger.purge_ping_data(24) + + purger.get_last_online_rsu_records.assert_called_once() + mock_write_db.assert_not_called() diff --git a/services/addons/tests/rsu_ping/test_rsu_ping_fetch.py b/services/addons/tests/rsu_status_check/test_rsu_ping_fetch.py similarity index 98% rename from services/addons/tests/rsu_ping/test_rsu_ping_fetch.py rename to services/addons/tests/rsu_status_check/test_rsu_ping_fetch.py index 8d3b3317..7c0b0880 100644 --- a/services/addons/tests/rsu_ping/test_rsu_ping_fetch.py +++ b/services/addons/tests/rsu_status_check/test_rsu_ping_fetch.py @@ -1,8 +1,8 @@ from mock import call, MagicMock, patch -from addons.images.rsu_ping import rsu_ping_fetch +from addons.images.rsu_status_check import rsu_ping_fetch -@patch("addons.images.rsu_ping.purger.pgquery.query_db") +@patch("addons.images.rsu_status_check.purger.pgquery.query_db") def test_get_rsu_data(mock_query_db): # mock mock_query_db.return_value = [(1, "ipaddr")] @@ -15,7 +15,7 @@ def test_get_rsu_data(mock_query_db): mock_query_db.assert_called_once() -@patch("addons.images.rsu_ping.purger.pgquery.write_db") +@patch("addons.images.rsu_status_check.purger.pgquery.write_db") def test_insert_rsu_ping(mock_write_db): # call testJson = { diff --git a/services/addons/tests/rsu_ping/test_rsu_pinger.py b/services/addons/tests/rsu_status_check/test_rsu_pinger.py similarity index 78% rename from services/addons/tests/rsu_ping/test_rsu_pinger.py rename to services/addons/tests/rsu_status_check/test_rsu_pinger.py index fd79ff6c..1066fb02 100644 --- a/services/addons/tests/rsu_ping/test_rsu_pinger.py +++ b/services/addons/tests/rsu_status_check/test_rsu_pinger.py @@ -1,10 +1,9 @@ from mock import MagicMock, call, patch -from datetime import datetime, timedelta from subprocess import DEVNULL -from addons.images.rsu_ping import rsu_pinger +from addons.images.rsu_status_check import rsu_pinger -@patch("addons.images.rsu_ping.rsu_pinger.pgquery.write_db") +@patch("addons.images.rsu_status_check.rsu_pinger.pgquery.write_db") def test_insert_ping_data(mock_write_db): ping_data = {1: "0", 2: "1", 3: "1"} time_str = "2023-11-01 00:00:00" @@ -22,7 +21,7 @@ def test_insert_ping_data(mock_write_db): mock_write_db.assert_called_with(expected_query) -@patch("addons.images.rsu_ping.rsu_pinger.Popen") +@patch("addons.images.rsu_status_check.rsu_pinger.Popen") def test_ping_rsu_ips_online(mock_Popen): mock_p = MagicMock() mock_p.poll.return_value = 1 @@ -45,7 +44,7 @@ def test_ping_rsu_ips_online(mock_Popen): assert result == expected_result -@patch("addons.images.rsu_ping.rsu_pinger.Popen") +@patch("addons.images.rsu_status_check.rsu_pinger.Popen") def test_ping_rsu_ips_offline(mock_Popen): mock_p = MagicMock() mock_p.poll.return_value = 1 @@ -68,7 +67,7 @@ def test_ping_rsu_ips_offline(mock_Popen): assert result == expected_result -@patch("addons.images.rsu_ping.rsu_pinger.pgquery.query_db") +@patch("addons.images.rsu_status_check.rsu_pinger.pgquery.query_db") def test_get_rsu_ips(mock_query_db): mock_query_db.return_value = [ ({"rsu_id": 1, "ipv4_address": "1.1.1.1"},), @@ -83,9 +82,9 @@ def test_get_rsu_ips(mock_query_db): assert result == expected_result -@patch("addons.images.rsu_ping.rsu_pinger.get_rsu_ips") -@patch("addons.images.rsu_ping.rsu_pinger.ping_rsu_ips") -@patch("addons.images.rsu_ping.rsu_pinger.insert_ping_data") +@patch("addons.images.rsu_status_check.rsu_pinger.get_rsu_ips") +@patch("addons.images.rsu_status_check.rsu_pinger.ping_rsu_ips") +@patch("addons.images.rsu_status_check.rsu_pinger.insert_ping_data") def test_run_rsu_pinger(mock_insert_ping_data, mock_ping_rsu_ips, mock_get_rsu_ips): mock_ping_rsu_ips.return_value = {1: "1", 2: "0", 3: "1"} @@ -98,9 +97,9 @@ def test_run_rsu_pinger(mock_insert_ping_data, mock_ping_rsu_ips, mock_get_rsu_i mock_insert_ping_data.assert_called_once() -@patch("addons.images.rsu_ping.rsu_pinger.get_rsu_ips") -@patch("addons.images.rsu_ping.rsu_pinger.ping_rsu_ips") -@patch("addons.images.rsu_ping.rsu_pinger.insert_ping_data") +@patch("addons.images.rsu_status_check.rsu_pinger.get_rsu_ips") +@patch("addons.images.rsu_status_check.rsu_pinger.ping_rsu_ips") +@patch("addons.images.rsu_status_check.rsu_pinger.insert_ping_data") def test_run_rsu_pinger_err(mock_insert_ping_data, mock_ping_rsu_ips, mock_get_rsu_ips): mock_ping_rsu_ips.return_value = {} diff --git a/services/addons/tests/rsu_status_check/test_rsu_snmp_fetch.py b/services/addons/tests/rsu_status_check/test_rsu_snmp_fetch.py new file mode 100644 index 00000000..2ae17559 --- /dev/null +++ b/services/addons/tests/rsu_status_check/test_rsu_snmp_fetch.py @@ -0,0 +1,15 @@ +from mock import patch +from addons.images.rsu_status_check import rsu_snmp_fetch +from addons.tests.rsu_status_check.data import rsu_snmp_fetch_data + + +@patch("addons.images.rsu_status_check.rsu_snmp_fetch.pgquery.query_db") +def test_get_rsu_list(mock_query_db): + mock_query_db.return_value = rsu_snmp_fetch_data.query_rsu_list + + # call + result = rsu_snmp_fetch.get_rsu_list() + + # verify + mock_query_db.assert_called_with(rsu_snmp_fetch_data.get_rsu_list_query_string) + assert result == rsu_snmp_fetch_data.rsu_list diff --git a/services/api/README.md b/services/api/README.md index 7dc191c1..ba1ad8c6 100644 --- a/services/api/README.md +++ b/services/api/README.md @@ -50,9 +50,9 @@ Returns the message counts for a single, selected RSU from a BigQuery table. It Returns the list of all ipv4 addresses with MAP message data in the PostgreSQL database when argument ip_list is true. Returns the MAP message geoJSON data for the RSU specified in the ip_address argument as a single JSON object when ip_list is false. -### /rsu-bsm-data (POST) +### /rsu-geo-msg-data (POST) -Returns geoJSON data for BSM messages from a BigQuery table given start time, end time, and geofence coordinates. It performs a select query on a table specified by the BSM_DB_NAME environment variable. Returns an array of JSON objects. +Returns geoJSON data for BSM messages from a MongoDB collection given start time, end time, and geofence coordinates. It performs a find query on a collection specified by the GEO_DB_NAME environment variable. Returns an array of JSON objects. In the event that the number of records exceeds the threshold specified by the MAX_GEO_QUERY_RECORDS environment variable filtering will occur so that each nth record is returned. 1. Verifies the command and calls the corresponding function. 2. Provided RSU data is plugged into the appropriate data structure depending upon the RSU REST endpoint. @@ -297,10 +297,8 @@ HTTP URL Arguments: - PG_DB_PORT: The database port. - PG_PG_DB_USER: The database user that will be used to authenticate the cloud function when it queries the database. - PG_PG_DB_PASS: The database user's password that will be used to authenticate the cloud function. -- COUNTS_DB_TYPE: Set to either "MongoDB" or "BigQuery" depending on where the message counts are stored. - COUNTS_MSG_TYPES: Set to a list of message types to include in counts query. Sample format is described in the sample.env. -- COUNTS_DB_NAME: The BigQuery table or MongoDB collection name where the RSU message counts are located. -- BSM_DB_NAME: The database name for BSM visualization data. +- GEO_DB_NAME: The database name for geospatial message visualization data. This is currently only supported for BSM and PSM message types. - SSM_DB_NAME: The database name for SSM visualization data. - SRM_DB_NAME: The database name for SRM visualization data. - MONGO_DB_URI: URI for the MongoDB connection. diff --git a/services/api/azure-pipelines.yml b/services/api/azure-pipelines.yml deleted file mode 100644 index 6474bd2b..00000000 --- a/services/api/azure-pipelines.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Pipeline for creating and pushing Cloud Run artifacts - -trigger: - branches: - include: - - main - paths: - include: - - "api/*" - -pool: - vmImage: ubuntu-latest - -steps: - - task: CopyFiles@2 - inputs: - SourceFolder: 'api' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/jpo_cvmanager_api' - - # Publish the artifacts directory for consumption in publish pipeline - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: "$(Build.ArtifactStagingDirectory)" - ArtifactName: "docker_run" - publishLocation: "Container" diff --git a/services/api/sample.env b/services/api/sample.env index 83153b61..c4211495 100644 --- a/services/api/sample.env +++ b/services/api/sample.env @@ -25,9 +25,6 @@ KEYCLOAK_API_CLIENT_SECRET_KEY= # Firmware Manager connectivity in the format 'http://endpoint:port' FIRMWARE_MANAGER_ENDPOINT=http://:8089 -# Set to either "BIGQUERY" or "MONGODB" depending on message count location. -COUNTS_DB_TYPE= - # If "BIGQUERY", set the location of the GCP service account key GOOGLE_APPLICATION_CREDENTIALS='./resources/google/sample_gcp_service_account.json' @@ -36,14 +33,15 @@ MONGO_DB_URI= MONGO_DB_NAME="ODE" # Set these variables if using either "MONGODB" or "BIGQUERY" -# COUNTS_DB_NAME: Used for V2X message counts # COUNTS_MSG_TYPES: Comma seperated list of message types -COUNTS_DB_NAME= COUNTS_MSG_TYPES='BSM,SSM,SPAT,SRM,MAP' -BSM_DB_NAME= +GEO_DB_NAME= SSM_DB_NAME= SRM_DB_NAME= +# Specifies the maximum number of V2x messages returned from the geo_query_geo_data_mongo method before filtering occurs +MAX_GEO_QUERY_RECORDS= + # WZDX Variables WZDX_API_KEY = WZDX_ENDPOINT = diff --git a/services/api/src/contact_support.py b/services/api/src/contact_support.py index ca564238..2db2d4fc 100644 --- a/services/api/src/contact_support.py +++ b/services/api/src/contact_support.py @@ -5,7 +5,7 @@ from marshmallow import Schema from marshmallow import fields -from emailSender import EmailSender +from common.emailSender import EmailSender class ContactSupportSchema(Schema): diff --git a/services/api/src/emailSender.py b/services/api/src/emailSender.py deleted file mode 100644 index 81ce14c5..00000000 --- a/services/api/src/emailSender.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging -import smtplib, ssl - - -class EmailSender: - def __init__(self, smtp_server, port): - self.smtp_server = smtp_server - self.port = port - self.context = ssl._create_unverified_context() - self.server = smtplib.SMTP(self.smtp_server, self.port) - - def send(self, sender, recipient, subject, message, replyEmail, username, password): - try: - self.server.ehlo() # say hello to server - self.server.starttls(context=self.context) # start TLS encryption - self.server.ehlo() # say hello again - self.server.login(username, password) - - # prepare email - toSend = self.prepareEmailToSend( - sender, recipient, subject, message, replyEmail - ) - - # send email - self.server.sendmail(sender, recipient, toSend) - logging.debug(f"Email sent to {recipient}") - except Exception as e: - print(e) - finally: - self.server.quit() - - def prepareEmailToSend(self, sender, recipient, subject, message, replyEmail): - emailHeaders = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n" % ( - sender, - recipient, - subject, - ) - toSend = emailHeaders + message + "\r\n\r\nReply-To: " + replyEmail - return toSend diff --git a/services/api/src/iss_scms_status.py b/services/api/src/iss_scms_status.py index e9e596b7..baa41aa2 100644 --- a/services/api/src/iss_scms_status.py +++ b/services/api/src/iss_scms_status.py @@ -1,6 +1,6 @@ import logging import common.pgquery as pgquery -import util +import common.util as util import os diff --git a/services/api/src/main.py b/services/api/src/main.py index e56dca36..ad3b6e6e 100644 --- a/services/api/src/main.py +++ b/services/api/src/main.py @@ -9,12 +9,13 @@ from healthcheck import HealthCheck from rsuinfo import RsuInfo from rsu_querycounts import RsuQueryCounts +from rsu_querymsgfwd import RsuQueryMsgFwd from rsu_online_status import RsuOnlineStatus from rsu_commands import RsuCommandRequest from rsu_map_info import RsuMapInfo from rsu_geo_query import RsuGeoQuery from wzdx_feed import WzdxFeed -from rsu_bsmdata import RsuBsmData +from rsu_geo_msg_query import RsuGeoData from iss_scms_status import IssScmsStatus from rsu_ssm_srm import RsuSsmSrmData from admin_new_rsu import AdminNewRsu @@ -42,11 +43,12 @@ api.add_resource(RsuInfo, "/rsuinfo") api.add_resource(RsuOnlineStatus, "/rsu-online-status") api.add_resource(RsuQueryCounts, "/rsucounts") +api.add_resource(RsuQueryMsgFwd, "/rsu-msgfwd-query") api.add_resource(RsuCommandRequest, "/rsu-command") api.add_resource(RsuMapInfo, "/rsu-map-info") api.add_resource(RsuGeoQuery, "/rsu-geo-query") api.add_resource(WzdxFeed, "/wzdx-feed") -api.add_resource(RsuBsmData, "/rsu-bsm-data") +api.add_resource(RsuGeoData, "/rsu-geo-msg-data") api.add_resource(IssScmsStatus, "/iss-scms-status") api.add_resource(RsuSsmSrmData, "/rsu-ssm-srm-data") api.add_resource(AdminNewRsu, "/admin-new-rsu") diff --git a/services/api/src/middleware.py b/services/api/src/middleware.py index c691cad1..9c8d0b0b 100644 --- a/services/api/src/middleware.py +++ b/services/api/src/middleware.py @@ -45,11 +45,12 @@ def get_user_role(token): "/rsuinfo": True, "/rsu-online-status": True, "/rsucounts": True, + "/rsu-msgfwd-query": True, "/rsu-command": True, "/rsu-map-info": True, "/iss-scms-status": True, "/wzdx-feed": False, - "/rsu-bsm-data": False, + "/rsu-geo-msg-data": False, "/rsu-ssm-srm-data": False, "/admin-new-rsu": False, "/admin-rsu": False, diff --git a/services/api/src/rsu_bsmdata.py b/services/api/src/rsu_bsmdata.py deleted file mode 100644 index f0df0e51..00000000 --- a/services/api/src/rsu_bsmdata.py +++ /dev/null @@ -1,196 +0,0 @@ -from google.cloud import bigquery -import util -import os -import logging -from datetime import datetime -from pymongo import MongoClient - -coord_resolution = 0.0001 # lats more than this are considered different -time_resolution = 10 # time deltas bigger than this are considered different - - -def bsm_hash(ip, timestamp, long, lat): - return ( - ip - + "_" - + str(int(timestamp / time_resolution)) - + "_" - + str(int(long / coord_resolution)) - + "_" - + str(int(lat / coord_resolution)) - ) - - -def query_bsm_data_mongo(pointList, start, end): - start_date = util.format_date_utc(start, "DATETIME") - end_date = util.format_date_utc(end, "DATETIME") - - try: - client = MongoClient(os.getenv("MONGO_DB_URI"), serverSelectionTimeoutMS=5000) - db = client[os.getenv("MONGO_DB_NAME")] - collection = db[os.getenv("BSM_DB_NAME")] - except Exception as e: - logging.error( - f"Failed to connect to Mongo counts collection with error message: {e}" - ) - return [], 503 - - filter = { - "properties.timestamp": {"$gte": start_date, "$lte": end_date}, - "geometry": { - "$geoWithin": {"$geometry": {"type": "Polygon", "coordinates": [pointList]}} - }, - } - hashmap = {} - count = 0 - total_count = 0 - - try: - logging.debug( - f"Running filter: {filter} on mongo collection {os.getenv('BSM_DB_NAME')}" - ) - for doc in collection.find(filter=filter): - message_hash = bsm_hash( - doc["properties"]["id"], - int(datetime.timestamp(doc["properties"]["timestamp"])), - doc["geometry"]["coordinates"][0], - doc["geometry"]["coordinates"][1], - ) - - if message_hash not in hashmap: - doc["properties"]["time"] = doc["properties"]["timestamp"].strftime( - "%Y-%m-%dT%H:%M:%Sz" - ) - doc.pop("_id") - doc["properties"].pop("timestamp") - hashmap[message_hash] = doc - count += 1 - total_count += 1 - else: - total_count += 1 - - logging.info( - f"Filter successful. Records returned: {count}, Total records: {total_count}" - ) - return list(hashmap.values()), 200 - except Exception as e: - logging.error(f"Filter failed: {e}") - return [], 500 - - -def query_bsm_data_bq(pointList, start, end): - start_date = util.format_date_utc(start) - end_date = util.format_date_utc(end) - client = bigquery.Client() - tablename = os.environ["BSM_DB_NAME"] - geogString = "POLYGON((" - for elem in pointList: - long = str(elem.pop(0)) - lat = str(elem.pop(0)) - geogString += long + " " + lat + "," - - geogString = geogString[:-1] + "))" - - query = ( - "SELECT DISTINCT bsm.metadata.originIp as Ip, " - f"bsm.payload.data.coreData.position.longitude as long, " - f"bsm.payload.data.coreData.position.latitude as lat, " - f"bsm.metadata.odeReceivedAt as time " - f"FROM `{tablename}` " - f'WHERE TIMESTAMP(bsm.metadata.odeReceivedAt) >= TIMESTAMP("{start_date}") ' - f'AND TIMESTAMP(bsm.metadata.odeReceivedAt) <= TIMESTAMP("{end_date}") ' - f"AND ST_CONTAINS(ST_GEOGFROM('{geogString}'), " - f"ST_GEOGPOINT(bsm.payload.data.coreData.position.longitude, bsm.payload.data.coreData.position.latitude))" - ) - - logging.info(f"Running query on table {tablename}") - - query_job = client.query(query) - hashmap = {} - count = 0 - total_count = 0 - - for row in query_job: - message_hash = bsm_hash( - row["Ip"], - int(datetime.timestamp(util.format_date_utc(row["time"], "DATETIME"))), - row["long"], - row["lat"], - ) - - if message_hash not in hashmap: - doc = { - "type": "Feature", - "geometry": {"type": "Point", "coordinates": [row["long"], row["lat"]]}, - "properties": { - "id": row["Ip"], - "time": util.format_date_utc(row["time"]) + "z", - }, - } - hashmap[message_hash] = doc - count += 1 - total_count += 1 - else: - total_count += 1 - - logging.info(f"Query successful. Record returned: {count}") - return list(hashmap.values()), 200 - - -# REST endpoint resource class and schema -from flask import request -from flask_restful import Resource -from marshmallow import Schema, fields - - -class RsuBsmDataSchema(Schema): - geometry = fields.String(required=False) - start = fields.DateTime(required=False) - end = fields.DateTime(required=False) - - -class RsuBsmData(Resource): - options_headers = { - "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], - "Access-Control-Allow-Headers": "Content-Type,Authorization", - "Access-Control-Allow-Methods": "POST", - "Access-Control-Max-Age": "3600", - } - - headers = { - "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], - "Content-Type": "application/json", - } - - def options(self): - # CORS support - return ("", 204, self.options_headers) - - def post(self): - logging.debug("RsuBsmData POST requested") - - # Get arguments from request - try: - data = request.json - pointList = data["geometry"] - start = data["start"] - end = data["end"] - except: - return ( - 'Body format: {"start": string, "end": string, "geometry": coordinate list}', - 400, - self.headers, - ) - db_type = os.getenv("COUNTS_DB_TYPE", "BIGQUERY").upper() - data = [] - code = None - - if db_type == "MONGODB": - logging.debug("RsuBsmData Mongodb query") - data, code = query_bsm_data_mongo(pointList, start, end) - # If the db_type is set to anything other than MONGODB then default to bigquery - else: - logging.debug("RsuBsmData BigQuery query") - data, code = query_bsm_data_bq(pointList, start, end) - - return (data, code, self.headers) diff --git a/services/api/src/rsu_commands.py b/services/api/src/rsu_commands.py index 9593ae93..489194fe 100644 --- a/services/api/src/rsu_commands.py +++ b/services/api/src/rsu_commands.py @@ -1,8 +1,9 @@ from marshmallow import Schema, fields import common.pgquery as pgquery import logging -import rsufwdsnmpwalk -import rsufwdsnmpset +import common.rsufwdsnmpwalk as rsufwdsnmpwalk +import common.rsufwdsnmpset as rsufwdsnmpset +import common.update_rsu_snmp_pg as update_rsu_snmp_pg import rsu_upgrade import ssh_commands import os @@ -74,6 +75,7 @@ def execute_command(command, rsu_ip, args, rsu_info): request_data["snmp_creds"] = { "username": rsu_info["snmp_username"], "password": rsu_info["snmp_password"], + "encrypt_pw": rsu_info["snmp_encrypt_pw"], } request_data["snmp_version"] = rsu_info["snmp_version"] @@ -87,7 +89,7 @@ def fetch_rsu_info(rsu_ip, organization): query = ( "SELECT to_jsonb(row) " "FROM (" - "SELECT man.name AS manufacturer_name, rcred.username AS ssh_username, rcred.password AS ssh_password, snmp.username AS snmp_username, snmp.password AS snmp_password, sver.version_code AS snmp_version " + "SELECT rd.rsu_id AS rsu_id, man.name AS manufacturer_name, rcred.username AS ssh_username, rcred.password AS ssh_password, snmp.username AS snmp_username, snmp.password AS snmp_password, snmp.encrypt_password as snmp_encrypt_pw, sver.version_code AS snmp_version " "FROM public.rsus AS rd " "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " "JOIN public.rsu_models AS rm ON rm.rsu_model_id = rd.model " @@ -105,11 +107,13 @@ def fetch_rsu_info(rsu_ip, organization): # Grab the first result, it should be the only result row = dict(data[0][0]) rsu_info = { + "rsu_id": row["rsu_id"], "manufacturer": row["manufacturer_name"], "ssh_username": row["ssh_username"], "ssh_password": row["ssh_password"], "snmp_username": row["snmp_username"], "snmp_password": row["snmp_password"], + "snmp_encrypt_pw": row["snmp_encrypt_pw"], "snmp_version": row["snmp_version"], } return rsu_info @@ -162,8 +166,20 @@ def execute_rsufwdsnmpset(command, organization, rsu_list, args): dest_ip = args["dest_ip"] del args["dest_ip"] + rsu_info_list = [] for rsu in rsu_list: rsu_info = fetch_rsu_info(rsu, organization) + rsu_info_list.append( + { + "rsu_id": rsu_info["rsu_id"], + "ipv4_address": rsu, + "snmp_username": rsu_info["snmp_username"], + "snmp_password": rsu_info["snmp_password"], + "snmp_encrypt_pw": rsu_info["snmp_encrypt_pw"], + "snmp_version": rsu_info["snmp_version"], + } + ) + if rsu_info is None: return_dict[rsu] = { "code": 400, @@ -185,6 +201,11 @@ def execute_rsufwdsnmpset(command, organization, rsu_list, args): "code": 400, "data": f"Invalid index for RSU: {rsu}", } + + # Regardless of what occurred, update PostgreSQL with latest SNMP configs + configs = update_rsu_snmp_pg.get_snmp_configs(rsu_info_list) + update_rsu_snmp_pg.update_postgresql(configs, subset=True) + return return_dict diff --git a/services/api/src/rsu_geo_msg_query.py b/services/api/src/rsu_geo_msg_query.py new file mode 100644 index 00000000..77376072 --- /dev/null +++ b/services/api/src/rsu_geo_msg_query.py @@ -0,0 +1,142 @@ +import common.util as util +import os +import logging +from datetime import datetime +from pymongo import MongoClient +import math + +coord_resolution = 0.0001 # lats more than this are considered different +time_resolution = 10 # time deltas bigger than this are considered different + + +def geo_hash(ip, timestamp, long, lat): + return ( + ip + + "_" + + str(int(timestamp / time_resolution)) + + "_" + + str(int(long / coord_resolution)) + + "_" + + str(int(lat / coord_resolution)) + ) + + +def query_geo_data_mongo(pointList, start, end, msg_type): + start_date = util.format_date_utc(start, "DATETIME") + end_date = util.format_date_utc(end, "DATETIME") + mongo_uri = os.getenv("MONGO_DB_URI") + db_name = os.getenv("MONGO_DB_NAME") + coll_name = os.getenv("GEO_DB_NAME") + + try: + logging.debug( + f"Connecting to Mongo {coll_name} collection with URI: {mongo_uri} with db: {db_name}" + ) + client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000) + db = client[db_name] + collection = db[coll_name] + except Exception as e: + logging.error( + f"Failed to connect to Mongo {coll_name} collection with error message: {e}" + ) + return [], 503 + + filter = { + "properties.msg_type": msg_type, + "properties.timestamp": {"$gte": start_date, "$lte": end_date}, + "geometry": { + "$geoWithin": {"$geometry": {"type": "Polygon", "coordinates": [pointList]}} + }, + } + hashmap = {} + count = 0 + total_count = 0 + + try: + logging.debug(f"Running filter: {filter} on mongo collection {coll_name}") + num_docs = collection.count_documents(filter) + max_records = int(os.getenv("MAX_GEO_QUERY_RECORDS", 10000)) + filter_record = math.ceil(num_docs / max_records) + for doc in collection.find(filter=filter): + message_hash = geo_hash( + doc["properties"]["id"], + int(datetime.timestamp(doc["properties"]["timestamp"])), + doc["geometry"]["coordinates"][0], + doc["geometry"]["coordinates"][1], + ) + + if message_hash not in hashmap: + # Add first, last, and every nth record + if count == 0 or num_docs == (total_count + 1) or total_count % filter_record == 0: + doc["properties"]["time"] = doc["properties"]["timestamp"].strftime( + "%Y-%m-%dT%H:%M:%Sz" + ) + doc.pop("_id") + doc["properties"].pop("timestamp") + hashmap[message_hash] = doc + count += 1 + total_count += 1 + else: + total_count += 1 + else: + total_count += 1 + + logging.info( + f"Filter successful. Records returned: {count}, Total records: {total_count}" + ) + return list(hashmap.values()), 200 + except Exception as e: + logging.error(f"Filter failed: {e}") + return [], 500 + + +# REST endpoint resource class and schema +from flask import request +from flask_restful import Resource +from marshmallow import Schema, fields + + +class RsuGeoDataSchema(Schema): + geometry = fields.String(required=False) + start = fields.DateTime(required=False) + end = fields.DateTime(required=False) + msg_type = fields.String(required=False) + + +class RsuGeoData(Resource): + options_headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Access-Control-Allow-Headers": "Content-Type,Authorization", + "Access-Control-Allow-Methods": "POST", + "Access-Control-Max-Age": "3600", + } + + headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Content-Type": "application/json", + } + + def options(self): + # CORS support + return ("", 204, self.options_headers) + + def post(self): + logging.debug("RsuGeoData POST requested") + + # Get arguments from request + try: + data = request.json + msg_type = data["msg_type"] + pointList = data["geometry"] + start = data["start"] + end = data["end"] + except: + return ( + 'Body format: {"start": string, "end": string, "geometry": coordinate list}', + 400, + self.headers, + ) + + data, code = query_geo_data_mongo(pointList, start, end, msg_type.capitalize()) + + return (data, code, self.headers) diff --git a/services/api/src/rsu_geo_query.py b/services/api/src/rsu_geo_query.py index 555efbed..0aa09ad0 100644 --- a/services/api/src/rsu_geo_query.py +++ b/services/api/src/rsu_geo_query.py @@ -24,7 +24,7 @@ def query_org_rsus(orgName): return result -def query_rsu_devices(ipList, pointList): +def query_rsu_devices(ipList, pointList, vendor=None): geogString = "POLYGON((" for elem in pointList: long = str(elem.pop(0)) @@ -41,9 +41,16 @@ def query_rsu_devices(ipList, pointList): f"ST_Y(geography::geometry) AS lat " f"FROM rsus " f"WHERE ipv4_address = ANY('{{{ipList}}}'::inet[]) " - f"AND ST_Contains(ST_SetSRID(ST_GeomFromText('{geogString}'), 4326), rsus.geography::geometry)" - ") as row" ) + if vendor is not None: + query += ( + f" AND ipv4_address IN (SELECT rd.ipv4_address " + "FROM public.rsus as rd " + "JOIN public.rsu_models as rm ON rm.rsu_model_id = rd.model " + "JOIN public.manufacturers as man on man.manufacturer_id = rm.manufacturer " + f"WHERE man.name = '{vendor}') " + ) + query += f"AND ST_Contains(ST_SetSRID(ST_GeomFromText('{geogString}'), 4326), rsus.geography::geometry)) as row" logging.debug(query) logging.info(f"Running query_rsu_devices") @@ -75,6 +82,7 @@ class RsuGeoQuerySchema(Schema): required=False, validate=validate.Length(min=1), ) + vendor = fields.String(required=False) class RsuGeoQuery(Resource): @@ -109,11 +117,12 @@ def post(self): logging.debug(data) organization = request.environ["organization"] pointList = data["geometry"] + vendor = data["vendor"] if data["vendor"] != "Select Vendor" else None except: logging.debug("failed to parse request") return ('Body format: {"geometry": coordinate list}', 400, self.headers) ipList = query_org_rsus(organization) if ipList: - data, code = query_rsu_devices(ipList, pointList) + data, code = query_rsu_devices(ipList, pointList, vendor) return (data, code, self.headers) diff --git a/services/api/src/rsu_map_info.py b/services/api/src/rsu_map_info.py index ab0e1c65..c23f243a 100644 --- a/services/api/src/rsu_map_info.py +++ b/services/api/src/rsu_map_info.py @@ -1,5 +1,5 @@ import logging -from util import format_date_denver +from common.util import format_date_denver import common.pgquery as pgquery import os diff --git a/services/api/src/rsu_online_status.py b/services/api/src/rsu_online_status.py index 734e1437..f0e80573 100644 --- a/services/api/src/rsu_online_status.py +++ b/services/api/src/rsu_online_status.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta import logging -import util +import common.util as util import common.pgquery as pgquery import os @@ -70,11 +70,11 @@ def get_last_online_data(ip, organization): return { "ip": ip, - "last_online": util.format_date_denver( - result[0].strftime("%m/%d/%Y %I:%M:%S %p") - ) - if len(result) != 0 - else "No Data", + "last_online": ( + util.format_date_denver(result[0].strftime("%m/%d/%Y %I:%M:%S %p")) + if len(result) != 0 + else "No Data" + ), } diff --git a/services/api/src/rsu_querycounts.py b/services/api/src/rsu_querycounts.py index 99c224c4..84cb421e 100644 --- a/services/api/src/rsu_querycounts.py +++ b/services/api/src/rsu_querycounts.py @@ -1,82 +1,63 @@ -from google.cloud import bigquery from datetime import datetime, timedelta import common.pgquery as pgquery -import util +import common.util as util import os import logging -import json from pymongo import MongoClient +message_types = { + "bsm": "BSM", + "map": "Map", + "spat": "SPaT", + "srm": "SRM", + "ssm": "SSM", + "tim": "TIM", + "psm": "PSM", +} -def query_rsu_counts_mongo(allowed_ips, message_type, start, end): - start_date = util.format_date_utc(start, "DATETIME") - end_date = util.format_date_utc(end, "DATETIME") + +def query_rsu_counts_mongo(allowed_ips_dict, message_type, start, end): + start_dt = util.format_date_utc(start, "DATETIME").replace( + hour=0, minute=0, second=0, microsecond=0 + ) + end_dt = util.format_date_utc(end, "DATETIME").replace( + hour=0, minute=0, second=0, microsecond=0 + ) try: client = MongoClient(os.getenv("MONGO_DB_URI"), serverSelectionTimeoutMS=5000) - db = client[os.getenv("MONGO_DB_NAME")] - collection = db[os.getenv("COUNTS_DB_NAME")] + mongo_db = client[os.getenv("MONGO_DB_NAME")] + collection = mongo_db[f"CVCounts"] except Exception as e: logging.error( f"Failed to connect to Mongo counts collection with error message: {e}" ) return {}, 503 - filter = { - "timestamp": {"$gte": start_date, "$lt": end_date}, - "message_type": message_type.upper(), - } - result = {} - count = 0 - try: - logging.debug(f"Running filter: {filter}, on collection: {collection.name}") - for doc in collection.find(filter=filter): - if doc["ip"] in allowed_ips: - count += 1 - item = {"road": doc["road"], "count": doc["count"]} - result[doc["ip"]] = item - - logging.info(f"Filter successful. Length of data: {count}") - return result, 200 - except Exception as e: - logging.error(f"Filter failed: {e}") - return {}, 500 - - -def query_rsu_counts_bq(allowed_ips, message_type, start, end): - start_date = util.format_date_utc(start) - end_date = util.format_date_utc(end) - try: - client = bigquery.Client() - tablename = os.environ["COUNTS_DB_NAME"] - - query = ( - "SELECT RSU, Road, SUM(Count) as Count " - f"FROM `{tablename}` " - f'WHERE Date >= DATETIME("{start_date}") ' - f'AND Date < DATETIME("{end_date}") ' - f'AND Type = "{message_type.upper()}" ' - f"GROUP BY RSU, Road " - ) - - logging.info(f"Running query on table {tablename}") - - query_job = client.query(query) - - result = {} - count = 0 - for row in query_job: - if row["RSU"] in allowed_ips: - count += 1 - item = {"road": row["Road"], "count": row["Count"]} - result[row["RSU"]] = item - - logging.info(f"Query successful. Length of data: {count}") - return result, 200 - except Exception as e: - logging.error(f"Query failed: {e}") - return {}, 500 + for rsu_ip in allowed_ips_dict: + query = { + "messageType": message_types[message_type.lower()], + "rsuIp": rsu_ip, + "timestamp": { + "$gte": start_dt, + "$lt": end_dt, + }, + } + + try: + logging.debug(f"Running query: {query}, on collection: {collection.name}") + response = collection.find_one(query) + if not response: + item = {"road": allowed_ips_dict[rsu_ip], "count": 0} + else: + item = {"road": allowed_ips_dict[rsu_ip], "count": response["count"]} + result[rsu_ip] = item + except Exception as e: + logging.error(f"Filter failed: {e}") + return {}, 500 + + return result, 200 def get_organization_rsus(organization): @@ -84,18 +65,24 @@ def get_organization_rsus(organization): # Execute the query and fetch all results query = ( - "SELECT jsonb_build_object('ip', rd.ipv4_address) " - "FROM public.rsus AS rd " + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rd.ipv4_address, rd.primary_route " + "FROM public.rsus rd " "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " f"WHERE ron_v.name = '{organization}' " - "ORDER BY rd.ipv4_address" + "ORDER BY primary_route ASC, milepost ASC" + ") as row" ) logging.debug(f'Executing query: "{query};"') data = pgquery.query_db(query) - logging.debug(str(data)) - ips = [rsu[0]["ip"] for rsu in data] - return ips + + rsu_dict = {} + for row in data: + row = dict(row[0]) + rsu_dict[row["ipv4_address"]] = row["primary_route"] + return rsu_dict # REST endpoint resource class and schema @@ -147,22 +134,17 @@ def get(self): # Validate request with supported message types logging.debug(f"COUNTS_MSG_TYPES: {os.getenv('COUNTS_MSG_TYPES','NOT_SET')}") msgList = os.getenv("COUNTS_MSG_TYPES", "BSM,SSM,SPAT,SRM,MAP") - msgList = [msgtype.strip() for msgtype in msgList.split(",")] - if message.upper() not in msgList: + msgList = [msgtype.strip().title() for msgtype in msgList.split(",")] + if message.title() not in msgList: return ( "Invalid Message Type.\nValid message types: " + ", ".join(msgList), 400, self.headers, ) - db_type = os.getenv("COUNTS_DB_TYPE", "BIGQUERY").upper() data = 0 code = 204 - rsus = get_organization_rsus(request.environ["organization"]) - if db_type == "MONGODB": - data, code = query_rsu_counts_mongo(rsus, message.upper(), start, end) - # If the db_type is set to anything other than MONGODB then default to bigquery - else: - data, code = query_rsu_counts_bq(rsus, message.upper(), start, end) + rsu_dict = get_organization_rsus(request.environ["organization"]) + data, code = query_rsu_counts_mongo(rsu_dict, message, start, end) return (data, code, self.headers) diff --git a/services/api/src/rsu_querymsgfwd.py b/services/api/src/rsu_querymsgfwd.py new file mode 100644 index 00000000..531b7769 --- /dev/null +++ b/services/api/src/rsu_querymsgfwd.py @@ -0,0 +1,107 @@ +import common.pgquery as pgquery +import common.snmpwalk_helpers as snmpwalk_helpers +import common.util as util +import os +import logging + + +def query_snmp_msgfwd(rsu_ip, organization): + logging.info(f"Preparing to query for all RSU IPs for {organization}...") + + # Execute the query and fetch all results + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT smt.name msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active " + "FROM public.snmp_msgfwd_config smc " + "JOIN public.snmp_msgfwd_type smt ON smc.msgfwd_type = smt.snmp_msgfwd_type_id " + "JOIN (" + "SELECT rd.rsu_id, rd.ipv4_address " + "FROM public.rsus rd " + "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " + f"WHERE ron_v.name = '{organization}'" + ") rdo ON smc.rsu_id = rdo.rsu_id " + f"WHERE rdo.ipv4_address = '{rsu_ip}' " + "ORDER BY smt.name, snmp_index ASC" + ") as row" + ) + + logging.debug(f'Executing query: "{query};"') + data = pgquery.query_db(query) + + msgfwd_configs_dict = {} + for row in data: + row = dict(row[0]) + + config_row = { + "Message Type": row["message_type"].upper(), + "IP": row["dest_ipv4"], + "Port": row["dest_port"], + "Start DateTime": util.format_date_denver_iso(row["start_datetime"]), + "End DateTime": util.format_date_denver_iso(row["end_datetime"]), + "Config Active": snmpwalk_helpers.active(row["active"]), + } + + # Based on the value of msgfwd_type, store the configuration data to match the response object of rsufwdsnmpwalk + if row["msgfwd_type"] == "rsuDsrcFwd": + msgfwd_configs_dict[row["snmp_index"]] = config_row + elif row["msgfwd_type"] == "rsuReceivedMsg": + if "rsuReceivedMsgTable" not in msgfwd_configs_dict: + msgfwd_configs_dict["rsuReceivedMsgTable"] = {} + msgfwd_configs_dict["rsuReceivedMsgTable"][row["snmp_index"]] = config_row + elif row["msgfwd_type"] == "rsuXmitMsgFwding": + if "rsuXmitMsgFwdingTable" not in msgfwd_configs_dict: + msgfwd_configs_dict["rsuXmitMsgFwdingTable"] = {} + msgfwd_configs_dict["rsuXmitMsgFwdingTable"][row["snmp_index"]] = config_row + else: + logging.warn(f"Encountered unknown message forwarding configuration type '{row["msgfwd_type"]}' for RSU '{rsu_ip}'") + + # Make sure both RX and TX objects are available if the RSU ends up having NTCIP 1218 configurations + if "rsuReceivedMsgTable" in msgfwd_configs_dict and "rsuXmitMsgFwdingTable" not in msgfwd_configs_dict: + msgfwd_configs_dict["rsuXmitMsgFwdingTable"] = {} + elif "rsuXmitMsgFwdingTable" in msgfwd_configs_dict and "rsuReceivedMsgTable" not in msgfwd_configs_dict: + msgfwd_configs_dict["rsuReceivedMsgTable"] = {} + + return {"RsuFwdSnmpwalk": msgfwd_configs_dict}, 200 + + +# REST endpoint resource class and schema +from flask import request, abort +from flask_restful import Resource +from marshmallow import Schema, fields + + +class RsuQueryMsgFwdSchema(Schema): + rsu_ip = fields.IPv4(required=True) + + +class RsuQueryMsgFwd(Resource): + options_headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Access-Control-Allow-Headers": "Content-Type,Authorization,Organization", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Max-Age": "3600", + } + + headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Content-Type": "application/json", + } + + def options(self): + # CORS support + return ("", 204, self.options_headers) + + def get(self): + logging.debug("RsuQueryMsgFwd GET requested") + # Schema check for arguments + schema = RsuQueryMsgFwdSchema() + errors = schema.validate(request.args) + if errors: + abort(400, str(errors)) + # Get arguments from request and set defaults if not provided + rsu_ip = request.args.get("rsu_ip") + + data, code = query_snmp_msgfwd(rsu_ip, request.environ["organization"]) + + return (data, code, self.headers) diff --git a/services/api/src/rsu_ssm_srm.py b/services/api/src/rsu_ssm_srm.py index f4f530a8..ad4b57d4 100644 --- a/services/api/src/rsu_ssm_srm.py +++ b/services/api/src/rsu_ssm_srm.py @@ -1,87 +1,121 @@ -from google.cloud import bigquery -import util +import common.util as util import os import logging from datetime import datetime, timedelta +from pymongo import MongoClient -def query_ssm_data(result): +def query_ssm_data_mongo(result): end_date = datetime.now() end_utc = util.format_date_utc(end_date.isoformat()) start_date = end_date - timedelta(days=1) start_utc = util.format_date_utc(start_date.isoformat()) - client = bigquery.Client() - tablename = os.environ["SSM_DB_NAME"] - - query = ( - "SELECT rtdh_timestamp as time, ssm.metadata.originIp as ip, " - f"ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].requester.request, " - f"ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].requester.typeData.role, " - f"ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].status, " - f"ssm.metadata.recordType as type " - f'FROM `{tablename}` WHERE TIMESTAMP(rtdh_timestamp) >= "{start_utc}" ' - f'AND TIMESTAMP(rtdh_timestamp) <= "{end_utc}" ' - f"ORDER BY rtdh_timestamp ASC" - ) - - logging.info(f"Running query on table {tablename}") - - query_job = client.query(query) - - for row in query_job: - result.append( - { - "time": util.format_date_denver(row["time"].isoformat()), - "ip": row["ip"], - "requestId": row["request"], - "role": row["role"], - "status": row["status"], - "type": row["type"], - } - ) - - return 200, result + try: + client = MongoClient(os.getenv("MONGO_DB_URI"), serverSelectionTimeoutMS=5000) + db = client[os.getenv("MONGO_DB_NAME")] + collection = db[os.getenv("SSM_DB_NAME")] + except Exception as e: + logging.error( + f"Failed to connect to Mongo counts collection with error message: {e}" + ) + return [], 503 + + filter = {"recordGeneratedAt": {"$gte": start_utc, "$lte": end_utc}} + project = { + "recordGeneratedAt": 1, + "metadata.originIp": 1, + "metadata.recordType": 1, + "payload.data.status.signalStatus.sigStatus.signalStatusPackage.requester.request": 1, + "payload.data.status.signalStatus.sigStatus.signalStatusPackage.requester.role": 1, + "payload.data.status.signalStatus.sigStatus.signalStatusPackage.status": 1, + "_id": 0, + } -def query_srm_data(result): + logging.debug(f"Running filter on SSM mongoDB collection") + + # The data schema for the mongoDB collection is the same for the OdeSsmJson schema + # This can be viewed here: https://github.com/usdot-jpo-ode/jpo-ode/blob/develop/jpo-ode-core/src/main/resources/schemas/schema-ssm.json + try: + for doc in collection.find(filter, project): + result.append( + { + "time": util.format_date_denver(doc["recordGeneratedAt"]), + "ip": doc["metadata"]["originIp"], + "requestId": doc["payload"]["data"]["status"]["signalStatus"][0][ + "sigStatus" + ]["signalStatusPackage"][0]["requester"]["request"], + "role": doc["payload"]["data"]["status"]["signalStatus"][0][ + "sigStatus" + ]["signalStatusPackage"][0]["requester"]["role"], + "status": doc["payload"]["data"]["status"]["signalStatus"][0][ + "sigStatus" + ]["signalStatusPackage"][0]["status"], + "type": doc["metadata"]["recordType"], + } + ) + return 200, result + except Exception as e: + logging.error(f"SSM filter failed: {e}") + return 500, result + + +def query_srm_data_mongo(result): end_date = datetime.now() end_utc = util.format_date_utc(end_date.isoformat()) start_date = end_date - timedelta(days=1) start_utc = util.format_date_utc(start_date.isoformat()) - client = bigquery.Client() - tablename = os.environ["SRM_DB_NAME"] - - query = ( - "SELECT rtdh_timestamp as time, srm.metadata.originIp as ip, " - f"srm.payload.data.requests.signalRequestPackage[ordinal(1)].request.requestID as request, " - f"srm.payload.data.requestor.type.role, " - f"srm.payload.data.requestor.position.position.latitude as lat, " - f"srm.payload.data.requestor.position.position.longitude as long, " - f"srm.metadata.recordType as type " - f'FROM `{tablename}` WHERE TIMESTAMP(rtdh_timestamp) >= "{start_utc}" ' - f'AND TIMESTAMP(rtdh_timestamp) <= "{end_utc}" ' - f"ORDER BY rtdh_timestamp ASC" - ) - - logging.info(f"Running query on table {tablename}") - - query_job = client.query(query) - - for row in query_job: - result.append( - { - "time": util.format_date_denver(row["time"].isoformat()), - "ip": row["ip"], - "requestId": row["request"], - "role": row["role"], - "lat": row["lat"], - "long": row["long"], - "type": row["type"], - "status": "N/A", - } + + try: + client = MongoClient(os.getenv("MONGO_DB_URI"), serverSelectionTimeoutMS=5000) + db = client[os.getenv("MONGO_DB_NAME")] + collection = db[os.getenv("SRM_DB_NAME")] + except Exception as e: + logging.error( + f"Failed to connect to Mongo counts collection with error message: {e}" ) + return [], 503 + + filter = {"recordGeneratedAt": {"$gte": start_utc, "$lte": end_utc}} + project = { + "recordGeneratedAt": 1, + "metadata.originIp": 1, + "metadata.recordType": 1, + "payload.data.requests.signalRequestPackage.request.requestID": 1, + "payload.data.requestor.type.role": 1, + "payload.data.requestor.position.position.latitude": 1, + "payload.data.requestor.position.position.longitude": 1, + "_id": 0, + } - return 200, result + logging.debug(f"Running filter on SRM mongoDB collection") + + # The data schema for the mongoDB collection is the same for the OdeSrmJson schema + # This can be viewed here: https://github.com/usdot-jpo-ode/jpo-ode/blob/develop/jpo-ode-core/src/main/resources/schemas/schema-srm.json + try: + for doc in collection.find(filter, project): + result.append( + { + "time": util.format_date_denver(doc["recordGeneratedAt"]), + "ip": doc["metadata"]["originIp"], + "requestId": doc["payload"]["data"]["requests"][ + "signalRequestPackage" + ][0]["request"]["requestID"], + "role": doc["payload"]["data"]["requestor"]["type"]["role"], + "lat": doc["payload"]["data"]["requestor"]["position"]["position"][ + "latitude" + ], + "long": doc["payload"]["data"]["requestor"]["position"]["position"][ + "longitude" + ], + "type": doc["metadata"]["recordType"], + "status": "N/A", + } + ) + return 200, result + except Exception as e: + logging.error(f"SRM filter failed: {e}") + return 500, result # REST endpoint resource class and schema @@ -109,7 +143,7 @@ def options(self): def get(self): logging.debug("RsuSsmSrmData GET requested") data = [] - code, ssmRes = query_ssm_data(data) - code, finalRes = query_srm_data(ssmRes) + code, ssmRes = query_ssm_data_mongo(data) + code, finalRes = query_srm_data_mongo(ssmRes) finalRes.sort(key=lambda x: x["time"]) return (finalRes, code, self.headers) diff --git a/services/api/src/snmpcredential.py b/services/api/src/snmpcredential.py deleted file mode 100644 index 1723b1a1..00000000 --- a/services/api/src/snmpcredential.py +++ /dev/null @@ -1,5 +0,0 @@ -def get_authstring(snmp_creds): - snmp_authstring = "-u {user} -a SHA -A {pw} -x AES -X {pw} -l authpriv".format( - user=snmp_creds["username"], pw=snmp_creds["password"] - ) - return snmp_authstring diff --git a/services/api/tests/data/rsu_bsmdata_data.py b/services/api/tests/data/rsu_geo_msg_query_data.py similarity index 83% rename from services/api/tests/data/rsu_bsmdata_data.py rename to services/api/tests/data/rsu_geo_msg_query_data.py index b0567b2f..9cfece42 100644 --- a/services/api/tests/data/rsu_bsmdata_data.py +++ b/services/api/tests/data/rsu_geo_msg_query_data.py @@ -22,11 +22,11 @@ ] ) -##################################### query_bsm_data ########################################### +##################################### query_geo_data ########################################### point_list = [10.000, -10.000] -mongo_bsm_data_response = [ +mongo_geo_data_response = [ { "_id": "bson_id", "type": "Feature", @@ -35,7 +35,7 @@ } ] -processed_bsm_message_data = [ +processed_geo_message_data = [ { "type": "Feature", "properties": { @@ -46,7 +46,7 @@ } ] -bq_bsm_data_response = [ +bq_geo_data_response = [ { "Ip": "8.8.8.8", "long": point_list[0], @@ -55,13 +55,13 @@ }, ] -rsu_bsm_query = ( - "SELECT DISTINCT bsm.metadata.originIp as Ip, bsm.payload.data.coreData.position.longitude as long, " - "bsm.payload.data.coreData.position.latitude as lat, bsm.metadata.odeReceivedAt as time " - "FROM `Fake_table` WHERE TIMESTAMP(bsm.metadata.odeReceivedAt) " - '>= TIMESTAMP("2022-05-23T18:00:00") AND TIMESTAMP(bsm.metadata.odeReceivedAt) <= TIMESTAMP("2022-05-24T18:00:00") ' +rsu_geo_query = ( + "SELECT DISTINCT geo.metadata.originIp as Ip, geo.payload.data.coreData.position.longitude as long, " + "geo.payload.data.coreData.position.latitude as lat, geo.metadata.odeReceivedAt as time " + "FROM `Fake_table` WHERE TIMESTAMP(geo.metadata.odeReceivedAt) " + '>= TIMESTAMP("2022-05-23T18:00:00") AND TIMESTAMP(geo.metadata.odeReceivedAt) <= TIMESTAMP("2022-05-24T18:00:00") ' "AND ST_CONTAINS(ST_GEOGFROM('POLYGON((-105.63907347720362 39.785390458673525,-105.64302767662384 39.73371501339022))" - "'), ST_GEOGPOINT(bsm.payload.data.coreData.position.longitude, bsm.payload.data.coreData.position.latitude))" + "'), ST_GEOGPOINT(geo.payload.data.coreData.position.longitude, geo.payload.data.coreData.position.latitude))" ) record_one = multidict.MultiDict( @@ -89,7 +89,7 @@ ] ) -bsm_data_expected_single = [ +geo_data_expected_single = [ { "type": "Feature", "geometry": {"type": "Point", "coordinates": [-105, 39]}, @@ -97,7 +97,7 @@ } ] -bsm_data_expected_multiple = [ +geo_data_expected_multiple = [ { "type": "Feature", "geometry": {"type": "Point", "coordinates": [-105, 39]}, diff --git a/services/api/tests/data/rsu_geo_query_data.py b/services/api/tests/data/rsu_geo_query_data.py index 9faac5c5..f1a9341e 100644 --- a/services/api/tests/data/rsu_geo_query_data.py +++ b/services/api/tests/data/rsu_geo_query_data.py @@ -44,5 +44,14 @@ [-105.34460908203145, 39.724583197251334], ] +point_list_vendor = [ + [-105.34460908203145, 39.724583197251334], + [-105.34666901855489, 39.670180083300174], + [-105.25122529296911, 39.679162192647944], + [-105.2539718750002, 39.72088725644132], + [-105.34460908203145, 39.724583197251334], +] rsu_devices_query = "SELECT to_jsonb(row) FROM (SELECT ipv4_address as ip, ST_X(geography::geometry) AS long, ST_Y(geography::geometry) AS lat FROM rsus WHERE ipv4_address = ANY('{10.11.81.12}'::inet[]) AND ST_Contains(ST_SetSRID(ST_GeomFromText('POLYGON((-105.34460908203145 39.724583197251334,-105.34666901855489 39.670180083300174,-105.25122529296911 39.679162192647944,-105.2539718750002 39.72088725644132,-105.34460908203145 39.724583197251334))'), 4326), rsus.geography::geometry)) as row" + +rsu_devices_query_vendor = "SELECT to_jsonb(row) FROM (SELECT ipv4_address as ip, ST_X(geography::geometry) AS long, ST_Y(geography::geometry) AS lat FROM rsus WHERE ipv4_address = ANY('{10.11.81.12}'::inet[]) AND ipv4_address IN (SELECT rd.ipv4_address FROM public.rsus as rd JOIN public.rsu_models as rm ON rm.rsu_model_id = rd.model JOIN public.manufacturers as man on man.manufacturer_id = rm.manufacturer WHERE man.name = 'Test') AND ST_Contains(ST_SetSRID(ST_GeomFromText('POLYGON((-105.34460908203145 39.724583197251334,-105.34666901855489 39.670180083300174,-105.25122529296911 39.679162192647944,-105.2539718750002 39.72088725644132,-105.34460908203145 39.724583197251334))'), 4326), rsus.geography::geometry)) as row" \ No newline at end of file diff --git a/services/api/tests/data/rsu_querymsgfwd_data.py b/services/api/tests/data/rsu_querymsgfwd_data.py new file mode 100644 index 00000000..5e32f271 --- /dev/null +++ b/services/api/tests/data/rsu_querymsgfwd_data.py @@ -0,0 +1,144 @@ +import multidict +from datetime import datetime +import pytz + +##################################### request_data ########################################### + +request_environ = multidict.MultiDict( + [ + ("user_info", {"organizations": [{"name": "Test", "role": "user"}]}), + ("organization", "Test"), + ] +) + +request_args_good = multidict.MultiDict( + [ + ("rsu_ip", "10.0.0.80"), + ] +) + +request_args_bad_message = multidict.MultiDict( + [ + ("rsu_ip", "bad rsu ip"), + ] +) + +##################################### query_msgfwd_configs ########################################### + +rsu_msgfwd_query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT smt.name msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active " + "FROM public.snmp_msgfwd_config smc " + "JOIN public.snmp_msgfwd_type smt ON smc.msgfwd_type = smt.snmp_msgfwd_type_id " + "JOIN (" + "SELECT rd.rsu_id, rd.ipv4_address " + "FROM public.rsus rd " + "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " + f"WHERE ron_v.name = 'Test'" + ") rdo ON smc.rsu_id = rdo.rsu_id " + f"WHERE rdo.ipv4_address = '10.0.0.80' " + "ORDER BY smt.name, snmp_index ASC" + ") as row" +) + +return_value_rsuDsrcFwd = [ + ( + { + "msgfwd_type": "rsuDsrcFwd", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "10.0.0.80", + "dest_port": 46800, + "start_datetime": "2024/04/01T00:00:00", + "end_datetime": "2034/04/01T00:00:00", + "active": "1", + }, + ), + ( + { + "msgfwd_type": "rsuDsrcFwd", + "snmp_index": 2, + "message_type": "BSM", + "dest_ipv4": "10.0.0.81", + "dest_port": 46800, + "start_datetime": "2024/04/01T00:00:00", + "end_datetime": "2034/04/01T00:00:00", + "active": "1", + }, + ), +] + +result_rsuDsrcFwd = { + "RsuFwdSnmpwalk": { + 1: { + "Message Type": "BSM", + "IP": "10.0.0.80", + "Port": 46800, + "Start DateTime": "2024-04-01T00:00:00-06:00", + "End DateTime": "2034-04-01T00:00:00-06:00", + "Config Active": "Enabled", + }, + 2: { + "Message Type": "BSM", + "IP": "10.0.0.81", + "Port": 46800, + "Start DateTime": "2024-04-01T00:00:00-06:00", + "End DateTime": "2034-04-01T00:00:00-06:00", + "Config Active": "Enabled", + }, + } +} + + +return_value_rxtxfwd = [ + ( + { + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "10.0.0.80", + "dest_port": 46800, + "start_datetime": "2024/04/01T00:00:00", + "end_datetime": "2034/04/01T00:00:00", + "active": "1", + }, + ), + ( + { + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "10.0.0.80", + "dest_port": 44920, + "start_datetime": "2024/04/01T00:00:00", + "end_datetime": "2034/04/01T00:00:00", + "active": "1", + }, + ), +] + +result_rxtxfwd = { + "RsuFwdSnmpwalk": { + "rsuReceivedMsgTable": { + 1: { + "Message Type": "BSM", + "IP": "10.0.0.80", + "Port": 46800, + "Start DateTime": "2024-04-01T00:00:00-06:00", + "End DateTime": "2034-04-01T00:00:00-06:00", + "Config Active": "Enabled", + } + }, + "rsuXmitMsgFwdingTable": { + 1: { + "Message Type": "MAP", + "IP": "10.0.0.80", + "Port": 44920, + "Start DateTime": "2024-04-01T00:00:00-06:00", + "End DateTime": "2034-04-01T00:00:00-06:00", + "Config Active": "Enabled", + } + }, + } +} diff --git a/services/api/tests/data/rsu_ssm_srm_data.py b/services/api/tests/data/rsu_ssm_srm_data.py index b6d90b25..e0d4af46 100644 --- a/services/api/tests/data/rsu_ssm_srm_data.py +++ b/services/api/tests/data/rsu_ssm_srm_data.py @@ -16,90 +16,124 @@ "%Y-%m-%dT%H:%M:%S", ) -ssm_expected_query = ( - f"SELECT rtdh_timestamp as time, " - f"ssm.metadata.originIp as ip, ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].requester.request, " - f"ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].requester.typeData.role, " - f"ssm.payload.data.status.signalStatus[ordinal(1)].sigStatus.signalStatusPackage[ordinal(1)].status, " - f'ssm.metadata.recordType as type FROM `Fake_table` WHERE TIMESTAMP(rtdh_timestamp) >= "{start_date}" ' - f'AND TIMESTAMP(rtdh_timestamp) <= "{end_date}" ORDER BY rtdh_timestamp ASC' -) +ssm_record_one = { + "recordGeneratedAt": "2022-12-13T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "ssmTx"}, + "payload": { + "data": { + "status": { + "signalStatus": [ + { + "sigStatus": { + "signalStatusPackage": [ + { + "requester": { + "request": 13, + "role": "publicTrasport", + }, + "status": "granted", + } + ] + } + } + ] + } + } + }, +} -srm_expected_query = ( - f"SELECT rtdh_timestamp as time, srm.metadata.originIp as ip, " - f"srm.payload.data.requests.signalRequestPackage[ordinal(1)].request.requestID as request, " - f"srm.payload.data.requestor.type.role, srm.payload.data.requestor.position.position.latitude as lat, " - f"srm.payload.data.requestor.position.position.longitude as long, srm.metadata.recordType as type " - f'FROM `Fake_table` WHERE TIMESTAMP(rtdh_timestamp) >= "{start_date}" AND ' - f'TIMESTAMP(rtdh_timestamp) <= "{end_date}" ORDER BY rtdh_timestamp ASC' -) +ssm_record_two = { + "recordGeneratedAt": "2022-12-14T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "ssmTx"}, + "payload": { + "data": { + "status": { + "signalStatus": [ + { + "sigStatus": { + "signalStatusPackage": [ + { + "requester": { + "request": 10, + "role": "publicTrasport", + }, + "status": "granted", + } + ] + } + } + ] + } + } + }, +} -ssm_record_one = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/13 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 13), - ("role", "publicTrasport"), - ("status", "granted"), - ("type", "ssmTx"), - ] -) -ssm_record_two = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/14 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 10), - ("role", "publicTrasport"), - ("status", "granted"), - ("type", "ssmTx"), - ] -) -ssm_record_three = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/12 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 17), - ("role", "publicTrasport"), - ("status", "granted"), - ("type", "ssmTx"), - ] -) -srm_record_one = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/13 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 9), - ("role", "publicTrasport"), - ("lat", "100.00"), - ("long", "50.00"), - ("status", "N/A"), - ("type", "srmTx"), - ] -) -srm_record_two = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/12 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 13), - ("role", "publicTrasport"), - ("lat", "101.00"), - ("long", "49.00"), - ("status", "N/A"), - ("type", "srmTx"), - ] -) -srm_record_three = multidict.MultiDict( - [ - ("time", datetime.strptime("2022/12/14 00:00:00", "%Y/%m/%d %H:%M:%S")), - ("ip", "127.0.0.1"), - ("request", 17), - ("role", "publicTrasport"), - ("lat", "102.00"), - ("long", "53.00"), - ("status", "N/A"), - ("type", "srmTx"), - ] -) +ssm_record_three = { + "recordGeneratedAt": "2022-12-12T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "ssmTx"}, + "payload": { + "data": { + "status": { + "signalStatus": [ + { + "sigStatus": { + "signalStatusPackage": [ + { + "requester": { + "request": 17, + "role": "publicTrasport", + }, + "status": "granted", + } + ] + } + } + ] + } + } + }, +} + + +srm_record_one = { + "recordGeneratedAt": "2022-12-13T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "srmTx"}, + "payload": { + "data": { + "requests": {"signalRequestPackage": [{"request": {"requestID": 9}}]}, + "requestor": { + "type": {"role": "publicTrasport"}, + "position": {"position": {"latitude": "100.00", "longitude": "50.00"}}, + }, + } + }, +} +srm_record_two = { + "recordGeneratedAt": "2022-12-12T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "srmTx"}, + "payload": { + "data": { + "requests": {"signalRequestPackage": [{"request": {"requestID": 13}}]}, + "requestor": { + "type": {"role": "publicTrasport"}, + "position": {"position": {"latitude": "101.00", "longitude": "49.00"}}, + }, + } + }, +} +srm_record_three = { + "recordGeneratedAt": "2022-12-14T07:00:00.000+00:00", + "metadata": {"originIp": "127.0.0.1", "recordType": "srmTx"}, + "payload": { + "data": { + "requests": {"signalRequestPackage": [{"request": {"requestID": 17}}]}, + "requestor": { + "type": {"role": "publicTrasport"}, + "position": {"position": {"latitude": "102.00", "longitude": "53.00"}}, + }, + } + }, +} ssm_single_result_expected = [ { @@ -224,3 +258,52 @@ "status": "N/A", }, ] + + +srm_processed_one = { + "time": datetime.strftime( + datetime.strptime("12/13/2022 12:00:00 AM", "%m/%d/%Y %I:%M:%S %p").astimezone( + timezone("America/Denver") + ), + "%m/%d/%Y %I:%M:%S %p", + ), + "ip": "127.0.0.1", + "requestId": 9, + "role": "publicTrasport", + "lat": "100.00", + "long": "50.00", + "type": "srmTx", + "status": "N/A", +} + +srm_processed_two = { + "time": datetime.strftime( + datetime.strptime("12/12/2022 12:00:00 AM", "%m/%d/%Y %I:%M:%S %p").astimezone( + timezone("America/Denver") + ), + "%m/%d/%Y %I:%M:%S %p", + ), + "ip": "127.0.0.1", + "requestId": 13, + "role": "publicTrasport", + "lat": "101.00", + "long": "49.00", + "type": "srmTx", + "status": "N/A", +} + +srm_processed_three = { + "time": datetime.strftime( + datetime.strptime("12/14/2022 12:00:00 AM", "%m/%d/%Y %I:%M:%S %p").astimezone( + timezone("America/Denver") + ), + "%m/%d/%Y %I:%M:%S %p", + ), + "ip": "127.0.0.1", + "requestId": 17, + "role": "publicTrasport", + "lat": "102.00", + "long": "53.00", + "type": "srmTx", + "status": "N/A", +} diff --git a/services/api/tests/src/test_rsu_bsmdata.py b/services/api/tests/src/test_rsu_bsmdata.py deleted file mode 100644 index 153a2666..00000000 --- a/services/api/tests/src/test_rsu_bsmdata.py +++ /dev/null @@ -1,98 +0,0 @@ -from unittest.mock import patch, MagicMock -import os -from api.src.rsu_bsmdata import query_bsm_data_mongo, bsm_hash, query_bsm_data_bq -import api.tests.data.rsu_bsmdata_data as rsu_bsmdata_data - - -def test_bsm_hash(): - result = bsm_hash("192.168.1.1", 1616636734, 123.4567, 234.5678) - assert result is not None - - -@patch.dict( - os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "BSM_DB_NAME": "col"} -) -@patch("api.src.rsu_bsmdata.MongoClient") -def test_query_bsm_data_mongo(mock_mongo): - mock_db = MagicMock() - mock_collection = MagicMock() - mock_mongo.return_value.__getitem__.return_value = mock_db - mock_db.__getitem__.return_value = mock_collection - mock_db.validate_collection.return_value = "valid" - - mock_collection.find.return_value = rsu_bsmdata_data.mongo_bsm_data_response - - start = "2023-07-01T00:00:00Z" - end = "2023-07-02T00:00:00Z" - response, code = query_bsm_data_mongo(rsu_bsmdata_data.point_list, start, end) - expected_response = rsu_bsmdata_data.processed_bsm_message_data - - mock_mongo.assert_called() - mock_collection.find.assert_called() - assert code == 200 - assert response == expected_response - - -@patch.dict( - os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "BSM_DB_NAME": "col"} -) -@patch("api.src.rsu_bsmdata.MongoClient") -def test_query_bsm_data_mongo_filter_failed(mock_mongo): - mock_db = MagicMock() - mock_collection = MagicMock() - mock_mongo.return_value.__getitem__.return_value = mock_db - mock_db.__getitem__.return_value = mock_collection - mock_db.validate_collection.return_value = "valid" - - mock_collection.find.side_effect = Exception("Failed to find") - - start = "2023-07-01T00:00:00Z" - end = "2023-07-02T00:00:00Z" - response, code = query_bsm_data_mongo(rsu_bsmdata_data.point_list, start, end) - expected_response = [] - - mock_mongo.assert_called() - mock_collection.find.assert_called() - assert code == 500 - assert response == expected_response - - -@patch.dict( - os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "BSM_DB_NAME": "col"} -) -@patch("api.src.rsu_bsmdata.MongoClient") -def test_query_bsm_data_mongo_failed_to_connect(mock_mongo): - mock_mongo.side_effect = Exception("Failed to connect") - - start = "2023-07-01T00:00:00Z" - end = "2023-07-02T00:00:00Z" - response, code = query_bsm_data_mongo(rsu_bsmdata_data.point_list, start, end) - expected_response = [] - - mock_mongo.assert_called() - assert code == 503 - assert response == expected_response - - -@patch.dict(os.environ, {"BSM_DB_NAME": "col"}) -@patch("api.src.rsu_bsmdata.bigquery") -def test_query_bsm_data_bq(mock_bq): - mock_bq_client = MagicMock() - mock_bq.Client.return_value = mock_bq_client - - mock_job = MagicMock() - mock_bq_client.query.return_value = mock_job - mock_job.__iter__.return_value = rsu_bsmdata_data.bq_bsm_data_response - - point_list = [[1, 2], [3, 4]] - start = "2023-07-01T00:00:00Z" - end = "2023-07-02T00:00:00Z" - - response, code = query_bsm_data_bq(point_list, start, end) - expected_response = rsu_bsmdata_data.processed_bsm_message_data - - assert response[0]["properties"]["id"] == expected_response[0]["properties"]["id"] - assert ( - response[0]["properties"]["time"] == expected_response[0]["properties"]["time"] - ) - assert code == 200 # Expect a success status code diff --git a/services/api/tests/src/test_rsu_commands.py b/services/api/tests/src/test_rsu_commands.py index 44f32fe9..1e769f4e 100644 --- a/services/api/tests/src/test_rsu_commands.py +++ b/services/api/tests/src/test_rsu_commands.py @@ -9,6 +9,7 @@ "manufacturer": "test", "snmp_username": "test", "snmp_password": "test", + "snmp_encrypt_pw": None, "snmp_version": "test", "ssh_username": "test", "ssh_password": "test", @@ -127,11 +128,13 @@ def test_fetch_rsu_info(mock_query_db): mock_query_db.return_value = [ ( { + "rsu_id": 24, "manufacturer_name": "mocked manufacturer_name", "ssh_username": "mocked ssh_username", "ssh_password": "mocked ssh_password", "snmp_username": "mocked snmp_username", "snmp_password": "mocked snmp_password", + "snmp_encrypt_pw": "mocked snmp_encrypt_pw", "snmp_version": "mocked snmp_version", }, ), @@ -143,11 +146,13 @@ def test_fetch_rsu_info(mock_query_db): # check mock_query_db.assert_called_once() expected_result = { + "rsu_id": 24, "manufacturer": "mocked manufacturer_name", "ssh_username": "mocked ssh_username", "ssh_password": "mocked ssh_password", "snmp_username": "mocked snmp_username", "snmp_password": "mocked snmp_password", + "snmp_encrypt_pw": "mocked snmp_encrypt_pw", "snmp_version": "mocked snmp_version", } assert result == expected_result diff --git a/services/api/tests/src/test_rsu_geo_msg_query.py b/services/api/tests/src/test_rsu_geo_msg_query.py new file mode 100644 index 00000000..106cfdb7 --- /dev/null +++ b/services/api/tests/src/test_rsu_geo_msg_query.py @@ -0,0 +1,81 @@ +from unittest.mock import patch, MagicMock +import os +from api.src.rsu_geo_msg_query import query_geo_data_mongo, geo_hash +import api.tests.data.rsu_geo_msg_query_data as rsu_geo_msg_query_data + + +def test_geo_hash(): + result = geo_hash("192.168.1.1", 1616636734, 123.4567, 234.5678) + assert result is not None + + +@patch.dict( + os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "GEO_DB_NAME": "col"} +) +@patch("api.src.rsu_geo_msg_query.MongoClient") +def test_query_geo_data_mongo(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = iter( + rsu_geo_msg_query_data.mongo_geo_data_response + ) + + start = "2023-07-01T00:00:00Z" + end = "2023-07-02T00:00:00Z" + response, code = query_geo_data_mongo( + rsu_geo_msg_query_data.point_list, start, end, "msg_type" + ) + expected_response = rsu_geo_msg_query_data.processed_geo_message_data + + mock_mongo.assert_called() + mock_collection.find.assert_called() + assert code == 200 + assert response == expected_response + + +@patch.dict( + os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "GEO_DB_NAME": "col"} +) +@patch("api.src.rsu_geo_msg_query.MongoClient") +def test_query_geo_data_mongo_filter_failed(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + mock_db.validate_collection.return_value = "valid" + + mock_collection.find.side_effect = Exception("Failed to find") + + start = "2023-07-01T00:00:00Z" + end = "2023-07-02T00:00:00Z" + response, code = query_geo_data_mongo( + rsu_geo_msg_query_data.point_list, start, end, "msg_type" + ) + expected_response = [] + + mock_mongo.assert_called() + mock_collection.find.assert_called() + assert code == 500 + assert response == expected_response + + +@patch.dict( + os.environ, {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "GEO_DB_NAME": "col"} +) +@patch("api.src.rsu_geo_msg_query.MongoClient") +def test_query_geo_data_mongo_failed_to_connect(mock_mongo): + mock_mongo.side_effect = Exception("Failed to connect") + + start = "2023-07-01T00:00:00Z" + end = "2023-07-02T00:00:00Z" + response, code = query_geo_data_mongo( + rsu_geo_msg_query_data.point_list, start, end, "msg_type" + ) + expected_response = [] + + mock_mongo.assert_called() + assert code == 503 + assert response == expected_response diff --git a/services/api/tests/src/test_rsu_geo_query.py b/services/api/tests/src/test_rsu_geo_query.py index 7ccc4734..3909a1d7 100644 --- a/services/api/tests/src/test_rsu_geo_query.py +++ b/services/api/tests/src/test_rsu_geo_query.py @@ -93,3 +93,18 @@ def test_query_rsu_devices(mock_query_db): assert actual_result == ["10.11.81.12"] assert code == 200 + +@patch("api.src.rsu_commands.pgquery.query_db") +def test_query_rsu_devices_with_vendor(mock_query_db): + mock_query_db.return_value = [ + ({"ip": "10.11.81.12"},), + ] + actual_result, code = rsu_geo_query.query_rsu_devices( + {"10.11.81.12"}, + rsu_geo_query_data.point_list_vendor, + vendor="Test" + ) + mock_query_db.assert_called_with(rsu_geo_query_data.rsu_devices_query_vendor) + + assert actual_result == ["10.11.81.12"] + assert code == 200 \ No newline at end of file diff --git a/services/api/tests/src/test_rsu_online_status.py b/services/api/tests/src/test_rsu_online_status.py index af72cb8e..e6df10a0 100644 --- a/services/api/tests/src/test_rsu_online_status.py +++ b/services/api/tests/src/test_rsu_online_status.py @@ -3,7 +3,7 @@ from dateutil.parser import parse from werkzeug.exceptions import HTTPException import api.src.rsu_online_status as rsu_online_status -import api.src.util as util +import common.util as util import api.tests.data.rsu_online_status_data as data import pytest diff --git a/services/api/tests/src/test_rsu_querycounts.py b/services/api/tests/src/test_rsu_querycounts.py index d4f7fe4c..56d9c7c7 100644 --- a/services/api/tests/src/test_rsu_querycounts.py +++ b/services/api/tests/src/test_rsu_querycounts.py @@ -4,7 +4,6 @@ import api.src.rsu_querycounts as rsu_querycounts from api.src.rsu_querycounts import query_rsu_counts_mongo import api.tests.data.rsu_querycounts_data as querycounts_data -import datetime ##################################### Testing Requests ########################################### @@ -17,28 +16,9 @@ def test_options_request(): assert headers["Access-Control-Allow-Methods"] == "GET" -@patch.dict(os.environ, {"COUNTS_DB_TYPE": "BIGQUERY"}) -@patch("api.src.rsu_querycounts.get_organization_rsus") -@patch("api.src.rsu_querycounts.query_rsu_counts_bq") -def test_get_request_bq(mock_query, mock_rsus): - req = MagicMock() - req.args = querycounts_data.request_args_good - req.environ = querycounts_data.request_params_good - counts = rsu_querycounts.RsuQueryCounts() - mock_rsus.return_value = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] - mock_query.return_value = {"Some Data"}, 200 - with patch("api.src.rsu_querycounts.request", req): - (data, code, headers) = counts.get() - assert code == 200 - assert headers["Access-Control-Allow-Origin"] == "test.com" - assert headers["Content-Type"] == "application/json" - assert data == {"Some Data"} - - -@patch.dict(os.environ, {"COUNTS_DB_TYPE": "MONGODB"}) @patch("api.src.rsu_querycounts.get_organization_rsus") @patch("api.src.rsu_querycounts.query_rsu_counts_mongo") -def test_get_request_mongo(mock_query, mock_rsus): +def test_get_request(mock_query, mock_rsus): req = MagicMock() req.args = querycounts_data.request_args_good req.environ = querycounts_data.request_params_good @@ -65,7 +45,7 @@ def test_get_request_invalid_message(): (data, code, headers) = counts.get() assert code == 400 assert headers["Access-Control-Allow-Origin"] == "test.com" - assert data == "Invalid Message Type.\nValid message types: test, anothErtest" + assert data == "Invalid Message Type.\nValid message types: Test, Anothertest" @patch.dict(os.environ, {}, clear=True) @@ -79,7 +59,7 @@ def test_get_request_invalid_message_no_env(): assert headers["Access-Control-Allow-Origin"] == "test.com" assert ( data - == "Invalid Message Type.\nValid message types: BSM, SSM, SPAT, SRM, MAP" + == "Invalid Message Type.\nValid message types: Bsm, Ssm, Spat, Srm, Map" ) @@ -98,47 +78,57 @@ def test_schema_validate_bad_data(): @patch("api.src.rsu_querycounts.pgquery") def test_rsu_counts_get_organization_rsus(mock_pgquery): mock_pgquery.query_db.return_value = [ - ({"ip": "10.11.81.12"},), - ({"ip": "10.11.81.13"},), - ({"ip": "10.11.81.14"},), + ({"ipv4_address": "10.11.81.12", "primary_route": "Route 1"},), + ({"ipv4_address": "10.11.81.13", "primary_route": "Route 1"},), + ({"ipv4_address": "10.11.81.14", "primary_route": "Route 1"},), ] expected_query = ( - "SELECT jsonb_build_object('ip', rd.ipv4_address) " - "FROM public.rsus AS rd " + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rd.ipv4_address, rd.primary_route " + "FROM public.rsus rd " "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " - f"WHERE ron_v.name = 'Test' " - "ORDER BY rd.ipv4_address" + "WHERE ron_v.name = 'Test' " + "ORDER BY primary_route ASC, milepost ASC" + ") as row" ) + actual_result = rsu_querycounts.get_organization_rsus("Test") - mock_pgquery.query_db.assert_called_with(expected_query) - assert actual_result == ["10.11.81.12", "10.11.81.13", "10.11.81.14"] + mock_pgquery.query_db.assert_called_with(expected_query) + assert actual_result == { + "10.11.81.12": "Route 1", + "10.11.81.13": "Route 1", + "10.11.81.14": "Route 1", + } @patch("api.src.rsu_querycounts.pgquery") def test_rsu_counts_get_organization_rsus_empty(mock_pgquery): mock_pgquery.query_db.return_value = [] expected_query = ( - "SELECT jsonb_build_object('ip', rd.ipv4_address) " - "FROM public.rsus AS rd " + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rd.ipv4_address, rd.primary_route " + "FROM public.rsus rd " "JOIN public.rsu_organization_name AS ron_v ON ron_v.rsu_id = rd.rsu_id " - f"WHERE ron_v.name = 'Test' " - "ORDER BY rd.ipv4_address" + "WHERE ron_v.name = 'Test' " + "ORDER BY primary_route ASC, milepost ASC" + ") as row" ) actual_result = rsu_querycounts.get_organization_rsus("Test") mock_pgquery.query_db.assert_called_with(expected_query) - assert actual_result == [] + assert actual_result == {} ##################################### Test query_rsu_counts ########################################### @patch.dict( os.environ, - {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "COUNTS_DB_NAME": "col"}, + {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name"}, ) @patch("api.src.rsu_querycounts.MongoClient") -@patch("api.src.rsu_querycounts.logging") -def test_query_rsu_counts_mongo_success(mock_logging, mock_mongo): +def test_query_rsu_counts_mongo_success(mock_mongo): mock_db = MagicMock() mock_collection = MagicMock() mock_mongo.return_value.__getitem__.return_value = mock_db @@ -146,29 +136,27 @@ def test_query_rsu_counts_mongo_success(mock_logging, mock_mongo): mock_db.validate_collection.return_value = "valid" # Mock data that would be returned from MongoDB - mock_collection.find.return_value = [ - {"ip": "192.168.0.1", "road": "A1", "count": 5}, - {"ip": "192.168.0.2", "road": "A2", "count": 10}, - ] + mock_collection.find_one.return_value = {"count": 5} - allowed_ips = ["192.168.0.1", "192.168.0.2"] - message_type = "TYPE_A" + allowed_ips = {"192.168.0.1": "A1", "192.168.0.2": "A2"} + message_type = "BSM" start = "2022-01-01T00:00:00" end = "2023-01-01T00:00:00" expected_result = { "192.168.0.1": {"road": "A1", "count": 5}, - "192.168.0.2": {"road": "A2", "count": 10}, + "192.168.0.2": {"road": "A2", "count": 5}, } result, status_code = query_rsu_counts_mongo(allowed_ips, message_type, start, end) + assert result == expected_result assert status_code == 200 @patch.dict( os.environ, - {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name", "COUNTS_DB_NAME": "col"}, + {"MONGO_DB_URI": "uri", "MONGO_DB_NAME": "name"}, ) @patch("api.src.rsu_querycounts.MongoClient") @patch("api.src.rsu_querycounts.logging") @@ -184,47 +172,3 @@ def test_query_rsu_counts_mongo_failure(mock_logging, mock_mongo): result, status_code = query_rsu_counts_mongo(allowed_ips, message_type, start, end) assert result == {} assert status_code == 503 - - -@patch("api.src.rsu_querycounts.bigquery") -def test_rsu_counts_multiple_result(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ - querycounts_data.rsu_one, - querycounts_data.rsu_two, - querycounts_data.rsu_three, - ] - expected_rsu_data = querycounts_data.rsu_counts_expected_multiple - with patch.dict( - "api.src.rsu_querycounts.os.environ", - {"COUNTS_DB_NAME": "Fake_table", "COUNTS_DB_TYPE": "BIGQUERY"}, - ): - (data, code) = rsu_querycounts.query_rsu_counts_bq( - ["10.11.81.24", "172.16.28.23", "172.16.28.136"], - "BSM", - "2022-05-23T12:00:00", - "2022-05-24T12:00:00", - ) - assert data == expected_rsu_data - assert code == 200 - - -@patch("api.src.rsu_querycounts.bigquery") -def test_rsu_counts_limited_rsus(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ - querycounts_data.rsu_one, - querycounts_data.rsu_two, - querycounts_data.rsu_three, - ] - expected_rsu_data = querycounts_data.rsu_counts_expected_limited_rsus - with patch.dict( - "api.src.rsu_querycounts.os.environ", - {"COUNTS_DB_NAME": "Fake_table", "COUNTS_DB_TYPE": "BIGQUERY"}, - ): - (data, code) = rsu_querycounts.query_rsu_counts_bq( - ["172.16.28.23", "172.16.28.136"], - "BSM", - "2022-05-23T12:00:00", - "2022-05-24T12:00:00", - ) - assert data == expected_rsu_data - assert code == 200 diff --git a/services/api/tests/src/test_rsu_querymsgfwd.py b/services/api/tests/src/test_rsu_querymsgfwd.py new file mode 100644 index 00000000..fe14ab3a --- /dev/null +++ b/services/api/tests/src/test_rsu_querymsgfwd.py @@ -0,0 +1,64 @@ +from unittest.mock import patch, MagicMock +import pytest +import os +import api.src.rsu_querymsgfwd as rsu_querymsgfwd +import api.tests.data.rsu_querymsgfwd_data as rsu_querymsgfwd_data + +##################################### Testing Requests ########################################### + + +def test_options_request(): + query_msgfwd = rsu_querymsgfwd.RsuQueryMsgFwd() + (body, code, headers) = query_msgfwd.options() + assert body == "" + assert code == 204 + assert headers["Access-Control-Allow-Methods"] == "GET" + + +@patch("api.src.rsu_querymsgfwd.query_snmp_msgfwd") +def test_get_request(mock_query): + req = MagicMock() + req.environ = rsu_querymsgfwd_data.request_environ + req.args = rsu_querymsgfwd_data.request_args_good + query_msgfwd = rsu_querymsgfwd.RsuQueryMsgFwd() + mock_query.return_value = {"Some Data"}, 200 + with patch("api.src.rsu_querymsgfwd.request", req): + (data, code, headers) = query_msgfwd.get() + assert code == 200 + assert headers["Access-Control-Allow-Origin"] == "test.com" + assert headers["Content-Type"] == "application/json" + assert data == {"Some Data"} + + +################################### Testing Data Validation ######################################### + + +def test_schema_validate_bad_data(): + req = MagicMock() + req.environ = rsu_querymsgfwd_data.request_environ + req.args = rsu_querymsgfwd_data.request_args_bad_message + query_msgfwd = rsu_querymsgfwd.RsuQueryMsgFwd() + with patch("api.src.rsu_querymsgfwd.request", req): + with pytest.raises(Exception): + assert query_msgfwd.get() + + +###################################### Testing Functions ########################################## + + +@patch("api.src.rsu_querymsgfwd.pgquery") +def test_query_snmp_msgfwd_rsudsrcfwd(mock_pgquery): + mock_pgquery.query_db.return_value = rsu_querymsgfwd_data.return_value_rsuDsrcFwd + result, code = rsu_querymsgfwd.query_snmp_msgfwd("10.0.0.80", "Test") + + assert code == 200 + assert result == rsu_querymsgfwd_data.result_rsuDsrcFwd + + +@patch("api.src.rsu_querymsgfwd.pgquery") +def test_query_snmp_msgfwd_rxtxfwd(mock_pgquery): + mock_pgquery.query_db.return_value = rsu_querymsgfwd_data.return_value_rxtxfwd + result, code = rsu_querymsgfwd.query_snmp_msgfwd("10.0.0.80", "Test") + + assert code == 200 + assert result == rsu_querymsgfwd_data.result_rxtxfwd diff --git a/services/api/tests/src/test_rsu_ssm_srm.py b/services/api/tests/src/test_rsu_ssm_srm.py index e3881a53..27fbd835 100644 --- a/services/api/tests/src/test_rsu_ssm_srm.py +++ b/services/api/tests/src/test_rsu_ssm_srm.py @@ -1,3 +1,4 @@ +import os from unittest.mock import patch, MagicMock import api.src.rsu_ssm_srm as rsu_ssm_srm import api.tests.data.rsu_ssm_srm_data as ssm_srm_data @@ -16,17 +17,17 @@ def test_options_request(): assert headers["Access-Control-Allow-Methods"] == "GET" -@patch("api.src.rsu_ssm_srm.query_ssm_data") -@patch("api.src.rsu_ssm_srm.query_srm_data") +@patch("api.src.rsu_ssm_srm.query_ssm_data_mongo") +@patch("api.src.rsu_ssm_srm.query_srm_data_mongo") def test_get_request(mock_srm, mock_ssm): req = MagicMock() ssm_srm = rsu_ssm_srm.RsuSsmSrmData() mock_ssm.return_value = 200, [] mock_srm.return_value = 200, [ - ssm_srm_data.ssm_record_one, - ssm_srm_data.ssm_record_two, - ssm_srm_data.srm_record_one, - ssm_srm_data.srm_record_three, + ssm_srm_data.srm_processed_one, + ssm_srm_data.srm_processed_two, + ssm_srm_data.srm_processed_one, + ssm_srm_data.srm_processed_three, ] with patch("api.src.rsu_ssm_srm.request", req): (data, code, headers) = ssm_srm.get() @@ -34,57 +35,82 @@ def test_get_request(mock_srm, mock_ssm): assert headers["Access-Control-Allow-Origin"] == "test.com" assert headers["Content-Type"] == "application/json" assert data == [ - ssm_srm_data.ssm_record_one, - ssm_srm_data.srm_record_one, - ssm_srm_data.ssm_record_two, - ssm_srm_data.srm_record_three, + ssm_srm_data.srm_processed_two, + ssm_srm_data.srm_processed_one, + ssm_srm_data.srm_processed_one, + ssm_srm_data.srm_processed_three, ] #################################### Test query_ssm_data ######################################## -@patch("api.src.rsu_ssm_srm.bigquery") +@patch.dict( + os.environ, + {"MONGO_DB_NAME": "name", "SSM_DB_NAME": "ssm_collection"}, +) +@patch("api.src.rsu_ssm_srm.MongoClient") @patch("api.src.rsu_ssm_srm.datetime") -def test_query_ssm_data_query(mock_date, mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [] +def test_query_ssm_data_query(mock_date, mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [] + mock_date.now.return_value = datetime.strptime( "2022/12/14 00:00:00", "%Y/%m/%d %H:%M:%S" ).astimezone(UTC) - with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SSM_DB_NAME": "Fake_table"}): - rsu_ssm_srm.query_ssm_data([]) - mock_bigquery.Client.return_value.query.assert_called_with( - ssm_srm_data.ssm_expected_query - ) + rsu_ssm_srm.query_ssm_data_mongo([]) + + mock_mongo.assert_called() + mock_collection.find.assert_called() -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_ssm_data_no_data(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [] + +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_ssm_data_no_data(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SSM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_ssm_data([]) + (code, data) = rsu_ssm_srm.query_ssm_data_mongo([]) assert data == [] assert code == 200 -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_ssm_data_single_result(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ssm_srm_data.ssm_record_one] +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_ssm_data_single_result(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [ssm_srm_data.ssm_record_one] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SSM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_ssm_data([]) + (code, data) = rsu_ssm_srm.query_ssm_data_mongo([]) assert data == ssm_srm_data.ssm_single_result_expected assert code == 200 -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_ssm_data_multiple_result(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_ssm_data_multiple_result(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [ ssm_srm_data.ssm_record_one, ssm_srm_data.ssm_record_two, ssm_srm_data.ssm_record_three, ] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SSM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_ssm_data([]) + (code, data) = rsu_ssm_srm.query_ssm_data_mongo([]) assert data == ssm_srm_data.ssm_multiple_result_expected assert code == 200 @@ -92,46 +118,69 @@ def test_query_ssm_data_multiple_result(mock_bigquery): ##################################### Test query_srm_data ########################################### -@patch("api.src.rsu_ssm_srm.bigquery") +@patch.dict( + os.environ, + {"MONGO_DB_NAME": "name", "SRM_DB_NAME": "srm_collection"}, +) +@patch("api.src.rsu_ssm_srm.MongoClient") @patch("api.src.rsu_ssm_srm.datetime") -def test_query_srm_data_query(mock_date, mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [] +def test_query_srm_data_query(mock_date, mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [] mock_date.now.return_value = datetime.strptime( "2022/12/14 00:00:00", "%Y/%m/%d %H:%M:%S" ).astimezone(UTC) with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SRM_DB_NAME": "Fake_table"}): - rsu_ssm_srm.query_srm_data([]) - mock_bigquery.Client.return_value.query.assert_called_with( - ssm_srm_data.srm_expected_query - ) + rsu_ssm_srm.query_srm_data_mongo([]) + mock_mongo.assert_called() + mock_collection.find.assert_called() + +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_srm_data_no_data(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_srm_data_no_data(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [] + mock_collection.find.return_value = [] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SRM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_srm_data([]) + (code, data) = rsu_ssm_srm.query_srm_data_mongo([]) assert data == [] assert code == 200 -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_srm_data_single_result(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ssm_srm_data.srm_record_one] +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_srm_data_single_result(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [ssm_srm_data.srm_record_one] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SRM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_srm_data([]) + (code, data) = rsu_ssm_srm.query_srm_data_mongo([]) assert data == ssm_srm_data.srm_single_result_expected assert code == 200 -@patch("api.src.rsu_ssm_srm.bigquery") -def test_query_srm_data_multiple_result(mock_bigquery): - mock_bigquery.Client.return_value.query.return_value = [ +@patch("api.src.rsu_ssm_srm.MongoClient") +def test_query_srm_data_multiple_result(mock_mongo): + mock_db = MagicMock() + mock_collection = MagicMock() + mock_mongo.return_value.__getitem__.return_value = mock_db + mock_db.__getitem__.return_value = mock_collection + + mock_collection.find.return_value = [ ssm_srm_data.srm_record_one, ssm_srm_data.srm_record_two, ssm_srm_data.srm_record_three, ] with patch.dict("api.src.rsu_ssm_srm.os.environ", {"SRM_DB_NAME": "Fake_table"}): - (code, data) = rsu_ssm_srm.query_srm_data([]) + (code, data) = rsu_ssm_srm.query_srm_data_mongo([]) assert data == ssm_srm_data.srm_multiple_result_expected assert code == 200 diff --git a/services/api/tests/src/test_snmpcredential.py b/services/api/tests/src/test_snmpcredential.py deleted file mode 100644 index 90245d42..00000000 --- a/services/api/tests/src/test_snmpcredential.py +++ /dev/null @@ -1,7 +0,0 @@ -from api.src import snmpcredential - - -def test_get_authstring(): - snmp_creds = {"username": "testuser", "password": "testpassword"} - expected = "-u testuser -a SHA -A testpassword -x AES -X testpassword -l authpriv" - assert snmpcredential.get_authstring(snmp_creds) == expected diff --git a/services/azure-pipelines.yml b/services/azure-pipelines.yml new file mode 100644 index 00000000..097468df --- /dev/null +++ b/services/azure-pipelines.yml @@ -0,0 +1,26 @@ +# Pipeline for creating and pushing artifacts for all services + +trigger: + branches: + include: + - develop + paths: + include: + - 'services/*' + +pool: + vmImage: ubuntu-latest + +steps: + - task: CopyFiles@2 + inputs: + SourceFolder: 'services' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + # Publish the artifacts directory for consumption in publish pipeline + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'jpo-cvmanager-services' + publishLocation: 'Container' diff --git a/services/common/emailSender.py b/services/common/emailSender.py new file mode 100644 index 00000000..a1c85fde --- /dev/null +++ b/services/common/emailSender.py @@ -0,0 +1,67 @@ +import logging +import smtplib, ssl +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + + +class EmailSender: + def __init__(self, smtp_server, port): + self.smtp_server = smtp_server + self.port = port + self.context = ssl._create_unverified_context() + self.server = smtplib.SMTP(self.smtp_server, self.port) + + def send( + self, + sender, + recipient, + subject, + message, + replyEmail, + username, + password, + pretty=False, + ): + try: + # prepare email + toSend = "" + if pretty: + toSend = self.preparePrettyEmailToSend( + sender, recipient, subject, message + ) + else: + toSend = self.prepareEmailToSend( + sender, recipient, subject, message, replyEmail + ) + + self.server.starttls(context=self.context) # start TLS encryption + self.server.ehlo() # say hello + self.server.login(username, password) + + # send email + self.server.sendmail(sender, recipient, toSend) + logging.debug(f"Email sent to {recipient}") + except Exception as e: + logging.error(e) + finally: + self.server.quit() + + def prepareEmailToSend(self, sender, recipient, subject, message, replyEmail): + emailHeaders = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n" % ( + sender, + recipient, + subject, + ) + if not replyEmail: + toSend = emailHeaders + message + else: + toSend = emailHeaders + message + "\r\n\r\nReply-To: " + replyEmail + return toSend + + def preparePrettyEmailToSend(self, sender, recipient, subject, html_message): + toSend = MIMEMultipart() + toSend["Subject"] = subject + toSend["From"] = sender + toSend["To"] = recipient + toSend.attach(MIMEText(html_message, "html")) + return toSend.as_string() diff --git a/services/api/src/rsufwdsnmpset.py b/services/common/rsufwdsnmpset.py similarity index 96% rename from services/api/src/rsufwdsnmpset.py rename to services/common/rsufwdsnmpset.py index 6e524c43..4501ff20 100644 --- a/services/api/src/rsufwdsnmpset.py +++ b/services/common/rsufwdsnmpset.py @@ -2,9 +2,9 @@ import subprocess from datetime import datetime import logging -import snmpcredential -import util -import snmperrorcheck +import common.snmpcredential as snmpcredential +import common.util as util +import common.snmperrorcheck as snmperrorcheck def ip_to_hex(ip): @@ -406,6 +406,10 @@ def config_del(rsu_ip, snmp_version, snmp_creds, msg_type, rsu_index): snmp_mods += "NTCIP1218-v01:rsuReceivedMsgStatus.{index} i 6 ".format( index=rsu_index ) + if msg_type.lower() == "tim": + snmp_mods += "NTCIP1218-v01:rsuXmitMsgFwdingStatus.{index} i 6 ".format( + index=rsu_index + ) # Perform configurations logging.info(f'Running SNMPSET deletion "{snmp_mods}"') @@ -480,9 +484,20 @@ def config_init( "E0000016", raw=True, ) + if msg_type.lower() == "tim": + return config_rsudsrcfwd( + rsu_ip, + manufacturer, + snmp_creds, + dest_ip, + "47900", + index, + "8003", + raw=True + ) else: return ( - "Supported message type is currently only BSM, SPaT, MAP, SSM and SRM", + "Supported message type is currently only BSM, SPaT, MAP, SSM, SRM and TIM", 501, ) elif snmp_version == "1218": @@ -507,9 +522,13 @@ def config_init( return config_txrxmsg( rsu_ip, snmp_creds, dest_ip, "44930", index, "E0000016", False ) + if msg_type.lower() == "tim": + return config_txrxmsg( + rsu_ip, snmp_creds, dest_ip, "47900", index, "8003", False + ) else: return ( - "Supported message type is currently only BSM, SPaT, MAP, SSM and SRM", + "Supported message type is currently only BSM, SPaT, MAP, SSM, SRM and TIM", 501, ) else: diff --git a/services/api/src/rsufwdsnmpwalk.py b/services/common/rsufwdsnmpwalk.py similarity index 51% rename from services/api/src/rsufwdsnmpwalk.py rename to services/common/rsufwdsnmpwalk.py index da7be605..e86fd629 100644 --- a/services/api/src/rsufwdsnmpwalk.py +++ b/services/common/rsufwdsnmpwalk.py @@ -1,130 +1,68 @@ import subprocess import logging -import snmpcredential -import snmperrorcheck - - -def message_type(val): - # Check for which J2735 PSID matches val - # BSM - 20 - # SPaT - 8002 - # MAP - E0000017 - # SSM - E0000015 - # SRM - E0000016 - # Hex octets are spaced out in the output and are always 4 octets long - if val == '" "' or val == "00 00 00 20" or val == "00 00 00 32": - return "BSM" - elif val == "00 00 80 02" or val == "80 02" or val == "00 03 27 70": - return "SPaT" - elif val == "E0 00 00 17" or val == "37 58 09 64 07": - return "MAP" - elif val == "E0 00 00 15" or val == "37 58 09 64 06": - return "SSM" - elif val == "E0 00 00 16" or val == "37 58 09 64 05": - return "SRM" - return "Other" - - -# Little endian -def ip(val): - hex = val.split() - ipaddr = ( - f"{str(int(hex[-4], 16))}." - f"{str(int(hex[-3], 16))}." - f"{str(int(hex[-2], 16))}." - f"{str(int(hex[-1], 16))}" - ) - return ipaddr - - -def yunex_ip(val): - # Yunex RSUs can display IPs in 2 forms: - # As regular IPv4 address: "10.0.0.1" - # As (weird) IPv6/IPv4 hybrid: "::ffff:10.0.0.1" - # This supports both cases by first trimming the quotes - trimmed_val = val[1:-1] - if ":" in trimmed_val: - trimmed_val = trimmed_val.split(":")[-1] - return trimmed_val - - -def protocol(val): - if val == "1": - return "TCP" - elif val == "2": - return "UDP" - return "Other" - - -def fwdon(val): - if val == "1": - return "On" - return "Off" - - -def active(val): - # This value represents an active state - # Currently 1 and 4 are supported - # 1 - active - # 4 - create (represents active to Commsignia models) - if val == "1" or val == "4": - return "Enabled" - return "Disabled" - - -def toint(val): - return int(val) - - -def startend(val): - hex = val.split() - year = str(int(hex[0] + hex[1], 16)) - month = str(int(hex[2], 16)) - month = month if len(month) == 2 else "0" + month - day = str(int(hex[3], 16)) - day = day if len(day) == 2 else "0" + day - hour = str(int(hex[4], 16)) - hour = hour if len(hour) == 2 else "0" + hour - min = str(int(hex[5], 16)) - min = min if len(min) == 2 else "0" + min - return f"{year}-{month}-{day} {hour}:{min}" +import common.snmpcredential as snmpcredential +import common.snmperrorcheck as snmperrorcheck +import common.snmpwalk_helpers as snmpwalk_helpers # SNMP property to string name and processing function # Supports SNMP RSU 4.1 Spec and NTCIP 1218 SNMP tables prop_namevalue = { # These values are based off the RSU 4.1 Spec - "iso.0.15628.4.1.7.1.2": ("Message Type", message_type), - "iso.0.15628.4.1.7.1.3": ("IP", ip), - "iso.0.15628.4.1.7.1.4": ("Port", toint), - "iso.0.15628.4.1.7.1.5": ("Protocol", protocol), - "iso.0.15628.4.1.7.1.6": ("RSSI", toint), - "iso.0.15628.4.1.7.1.7": ("Frequency", toint), - "iso.0.15628.4.1.7.1.8": ("Start DateTime", startend), - "iso.0.15628.4.1.7.1.9": ("End DateTime", startend), - "iso.0.15628.4.1.7.1.10": ("Forwarding", fwdon), - "iso.0.15628.4.1.7.1.11": ("Config Active", active), + "iso.0.15628.4.1.7.1.2": ("Message Type", snmpwalk_helpers.message_type_rsu41), + "iso.0.15628.4.1.7.1.3": ("IP", snmpwalk_helpers.ip_rsu41), + "iso.0.15628.4.1.7.1.4": ("Port", int), + "iso.0.15628.4.1.7.1.5": ("Protocol", snmpwalk_helpers.protocol), + "iso.0.15628.4.1.7.1.6": ("RSSI", int), + "iso.0.15628.4.1.7.1.7": ("Frequency", int), + "iso.0.15628.4.1.7.1.8": ("Start DateTime", snmpwalk_helpers.startend_rsu41), + "iso.0.15628.4.1.7.1.9": ("End DateTime", snmpwalk_helpers.startend_rsu41), + "iso.0.15628.4.1.7.1.10": ("Forwarding", snmpwalk_helpers.fwdon), + "iso.0.15628.4.1.7.1.11": ("Config Active", snmpwalk_helpers.active), + # ----- # These values are based off the NTCIP 1218 rsuReceivedMsgTable table - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.2": ("Message Type", message_type), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.3": ("IP", yunex_ip), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.4": ("Port", toint), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.5": ("Protocol", protocol), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.6": ("RSSI", toint), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.7": ("Frequency", toint), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.8": ("Start DateTime", startend), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.9": ("End DateTime", startend), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.10": ("Config Active", active), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.11": ("Full WSMP", active), - "iso.3.6.1.4.1.1206.4.2.18.5.2.1.12": ("Yunex Filter", active), + "NTCIP1218-v01::rsuReceivedMsgPsid": ( + "Message Type", + snmpwalk_helpers.message_type_ntcip1218, + ), + "NTCIP1218-v01::rsuReceivedMsgDestIpAddr": ("IP", snmpwalk_helpers.ip_ntcip1218), + "NTCIP1218-v01::rsuReceivedMsgDestPort": ("Port", int), + "NTCIP1218-v01::rsuReceivedMsgProtocol": ("Protocol", snmpwalk_helpers.protocol), + "NTCIP1218-v01::rsuReceivedMsgRssi": ("RSSI", snmpwalk_helpers.rssi_ntcip1218), + "NTCIP1218-v01::rsuReceivedMsgInterval": ("Frequency", int), + "NTCIP1218-v01::rsuReceivedMsgDeliveryStart": ( + "Start DateTime", + snmpwalk_helpers.startend_ntcip1218, + ), + "NTCIP1218-v01::rsuReceivedMsgDeliveryStop": ( + "End DateTime", + snmpwalk_helpers.startend_ntcip1218, + ), + "NTCIP1218-v01::rsuReceivedMsgStatus": ("Config Active", snmpwalk_helpers.active), + "NTCIP1218-v01::rsuReceivedMsgSecure": ("Full WSMP", snmpwalk_helpers.active), + "NTCIP1218-v01::rsuReceivedMsgAuthMsgInterval": ( + "Security Filter", + snmpwalk_helpers.active, + ), + # ----- # These values are based off the NTCIP 1218 rsuXmitMsgFwdingTable table - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.2": ("Message Type", message_type), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.3": ("IP", yunex_ip), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.4": ("Port", toint), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.5": ("Protocol", protocol), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.6": ("Start DateTime", startend), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.7": ("End DateTime", startend), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.8": ("Full WSMP", active), - "iso.3.6.1.4.1.1206.4.2.18.20.2.1.9": ("Config Active", active), + "NTCIP1218-v01::rsuXmitMsgFwdingPsid": ( + "Message Type", + snmpwalk_helpers.message_type_ntcip1218, + ), + "NTCIP1218-v01::rsuXmitMsgFwdingDestIpAddr": ("IP", snmpwalk_helpers.ip_ntcip1218), + "NTCIP1218-v01::rsuXmitMsgFwdingDestPort": ("Port", int), + "NTCIP1218-v01::rsuXmitMsgFwdingProtocol": ("Protocol", snmpwalk_helpers.protocol), + "NTCIP1218-v01::rsuXmitMsgFwdingDeliveryStart": ( + "Start DateTime", + snmpwalk_helpers.startend_ntcip1218, + ), + "NTCIP1218-v01::rsuXmitMsgFwdingDeliveryStop": ( + "End DateTime", + snmpwalk_helpers.startend_ntcip1218, + ), + "NTCIP1218-v01::rsuXmitMsgFwdingSecure": ("Full WSMP", snmpwalk_helpers.active), + "NTCIP1218-v01::rsuXmitMsgFwdingStatus": ("Config Active", snmpwalk_helpers.active), } @@ -194,22 +132,22 @@ def snmpwalk_txrxmsg(snmp_creds, rsu_ip): output = "" try: # Create the SNMPWalk command based on the road - cmd = "snmpwalk -v 3 {auth} {rsuip} 1.3.6.1.4.1.1206.4.2.18.5.2.1".format( + cmd = "snmpwalk -v 3 {auth} {rsuip} NTCIP1218-v01:rsuReceivedMsgTable".format( auth=snmpcredential.get_authstring(snmp_creds), rsuip=rsu_ip ) # Example console output of a single configuration for rsuReceivedMsgTable - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.2.1 = STRING: " " #BSM - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.3.1 = STRING: "10.235.1.36" #destination ip - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.4.1 = INTEGER: 46800 #port - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.5.1 = INTEGER: 2 #UDP - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.6.1 = INTEGER: -100 #rssi - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.7.1 = INTEGER: 1 #Forward every message - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.8.1 = Hex-STRING: 07 E6 01 01 00 00 00 00 # 2022-01-01 00:00:00.00 - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.9.1 = Hex-STRING: 07 E8 09 01 00 00 00 00 # 2024-01-01 00:00:00.00 - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.10.1 = INTEGER: 1 # turn this configuration on - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.11.1 = INTEGER: 0 # 0 - Only forward payload. 1 - Forward entire WSMP message. - # iso.3.6.1.4.1.1206.4.2.18.5.2.1.12.1 = INTEGER: 0 # 0 means off. Only 0 is supported. + # NTCIP1218-v01::rsuReceivedMsgPsid.1 = STRING: 20000000 # BSM + # NTCIP1218-v01::rsuReceivedMsgDestIpAddr.1 = STRING: 10.235.1.135 + # NTCIP1218-v01::rsuReceivedMsgDestPort.1 = INTEGER: 46800 + # NTCIP1218-v01::rsuReceivedMsgProtocol.1 = INTEGER: udp(2) + # NTCIP1218-v01::rsuReceivedMsgRssi.1 = INTEGER: -100 dBm + # NTCIP1218-v01::rsuReceivedMsgInterval.1 = INTEGER: 1 + # NTCIP1218-v01::rsuReceivedMsgDeliveryStart.1 = STRING: 2024-1-30,2:57:0.0 + # NTCIP1218-v01::rsuReceivedMsgDeliveryStop.1 = STRING: 2034-1-30,2:57:0.0 + # NTCIP1218-v01::rsuReceivedMsgStatus.1 = INTEGER: active(1) + # NTCIP1218-v01::rsuReceivedMsgSecure.1 = INTEGER: 0 # 0 - Only forward unsigned payload. 1 - Forward entire signed WSMP message. + # NTCIP1218-v01::rsuReceivedMsgAuthMsgInterval.1 = INTEGER: 0 # 0 means off. Only 0 is supported. logging.info(f"Running snmpwalk: {cmd}") output = subprocess.run(cmd, shell=True, capture_output=True, check=True) output = output.stdout.decode("utf-8").split("\n")[:-1] @@ -222,30 +160,26 @@ def snmpwalk_txrxmsg(snmp_creds, rsu_ip): # Placeholder for possible other failed scenarios # A proper rsuReceivedMsgTable configuration will be exactly 11 lines of output. # Any RSU with an output of less than 11 can be assumed to be an RSU with - # no rsuReceivedMsgTable configurations, or that some form error occurred in - # reading an RSU's SNMP configuration data. In either scenario, simply returning an - # empty response will suffice for the first implementation. + # no rsuReceivedMsgTable configurations. if len(output) >= 11: snmp_config = {} # Parse each line of the output to build out readable SNMP configurations for line in output: # split configuration line into a property and value - prop, raw_value = line.strip().split(" = ") - # grab the configuration substring value for the property id while removing the index value - prop_substr = prop[: -(len(prop.split(".")[-1]) + 1)] - # grab the index value for the config - key = prop.split(".")[-1] + prop, value = line.strip().split(" = ") + # grab the property name and index + prop_name, prop_index = prop.split(".") # If the index value already exists in the dict, ensure to add the new configuration value to it to build out a full SNMP configuration - config = snmp_config[key] if key in snmp_config else {} + config = snmp_config[prop_index] if prop_index in snmp_config else {} # Assign the processed value of the the property to the readable property value and store the info based on the index value # The value is processed based on the type of property it is # The readable property name is based on the property - config[prop_namevalue[prop_substr][0]] = prop_namevalue[prop_substr][1]( - raw_value.split(": ")[1] + config[prop_namevalue[prop_name][0]] = prop_namevalue[prop_name][1]( + value.split(": ")[1] ) - snmp_config[key] = config + snmp_config[prop_index] = config snmpwalk_results["rsuReceivedMsgTable"] = snmp_config @@ -253,19 +187,19 @@ def snmpwalk_txrxmsg(snmp_creds, rsu_ip): output = "" try: # Create the SNMPWalk command based on the road - cmd = "snmpwalk -v 3 {auth} {rsuip} 1.3.6.1.4.1.1206.4.2.18.20.2.1".format( + cmd = "snmpwalk -v 3 {auth} {rsuip} NTCIP1218-v01:rsuXmitMsgFwdingTable".format( auth=snmpcredential.get_authstring(snmp_creds), rsuip=rsu_ip ) # Example console output of a single configuration for rsuXmitMsgFwdingTable - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.2.1 = Hex-STRING: 80 02 #SPaT - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.3.1 = STRING: "10.235.1.36" #destination ip - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.4.1 = INTEGER: 46800 #port - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.5.1 = INTEGER: 2 #UDP - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.6.1 = Hex-STRING: 07 E6 01 01 00 00 00 00 # 2022-01-01 00:00:00.00 - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.7.1 = Hex-STRING: 07 E8 09 01 00 00 00 00 # 2024-01-01 00:00:00.00 - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.8.1 = INTEGER: 0 # 0 - Only forward payload. 1 - Forward entire WSMP message. - # iso.3.6.1.4.1.1206.4.2.18.20.2.1.9.1 = INTEGER: 1 # turn this configuration on + # NTCIP1218-v01::rsuXmitMsgFwdingPsid.1 = STRING: e0000017 #MAP + # NTCIP1218-v01::rsuXmitMsgFwdingDestIpAddr.1 = STRING: 10.235.1.135 + # NTCIP1218-v01::rsuXmitMsgFwdingDestPort.1 = INTEGER: 44920 + # NTCIP1218-v01::rsuXmitMsgFwdingProtocol.1 = INTEGER: udp(2) + # NTCIP1218-v01::rsuXmitMsgFwdingDeliveryStart.1 = STRING: 2024-2-1,3:36:0.0 + # NTCIP1218-v01::rsuXmitMsgFwdingDeliveryStop.1 = STRING: 2034-2-1,3:36:0.0 + # NTCIP1218-v01::rsuXmitMsgFwdingSecure.1 = INTEGER: 0 # 0 - Only forward unsigned payload. 1 - Forward entire signed WSMP message. + # NTCIP1218-v01::rsuXmitMsgFwdingStatus.1 = INTEGER: active(1) logging.info(f"Running snmpwalk: {cmd}") output = subprocess.run(cmd, shell=True, capture_output=True, check=True) output = output.stdout.decode("utf-8").split("\n")[:-1] @@ -286,22 +220,20 @@ def snmpwalk_txrxmsg(snmp_creds, rsu_ip): # Parse each line of the output to build out readable SNMP configurations for line in output: - # split configuration line into a property and value - prop, raw_value = line.strip().split(" = ") - # grab the configuration substring value for the property id while removing the index value - prop_substr = prop[: -(len(prop.split(".")[-1]) + 1)] - # grab the index value for the config - key = prop.split(".")[-1] + ## split configuration line into a property and value + prop, value = line.strip().split(" = ") + # grab the property name and index + prop_name, prop_index = prop.split(".") # If the index value already exists in the dict, ensure to add the new configuration value to it to build out a full SNMP configuration - config = snmp_config[key] if key in snmp_config else {} + config = snmp_config[prop_index] if prop_index in snmp_config else {} # Assign the processed value of the the property to the readable property value and store the info based on the index value # The value is processed based on the type of property it is # The readable property name is based on the property - config[prop_namevalue[prop_substr][0]] = prop_namevalue[prop_substr][1]( - raw_value.split(": ")[1] + config[prop_namevalue[prop_name][0]] = prop_namevalue[prop_name][1]( + value.split(": ")[1] ) - snmp_config[key] = config + snmp_config[prop_index] = config snmpwalk_results["rsuXmitMsgFwdingTable"] = snmp_config diff --git a/services/common/snmpcredential.py b/services/common/snmpcredential.py new file mode 100644 index 00000000..2a137887 --- /dev/null +++ b/services/common/snmpcredential.py @@ -0,0 +1,20 @@ +def get_authstring(snmp_creds): + # If "encrypt_pw" isn't in the dictionary, get the value from "password" + # This must be handled separately before checking the value of "encrypt_pw" to avoid KeyErrors + if "encrypt_pw" not in snmp_creds: + encrypt_pw = snmp_creds["password"] + # If "encrypt_pw" is set to an empty string or None, get the value from "password" + elif not snmp_creds["encrypt_pw"]: + encrypt_pw = snmp_creds["password"] + else: + encrypt_pw = snmp_creds["encrypt_pw"] + + snmp_authstring = ( + "-u {user} -a SHA -A {pw} -x AES -X {encrypt_pw} -l authpriv".format( + user=snmp_creds["username"], + pw=snmp_creds["password"], + encrypt_pw=encrypt_pw, + ) + ) + + return snmp_authstring diff --git a/services/api/src/snmperrorcheck.py b/services/common/snmperrorcheck.py similarity index 100% rename from services/api/src/snmperrorcheck.py rename to services/common/snmperrorcheck.py diff --git a/services/common/snmpwalk_helpers.py b/services/common/snmpwalk_helpers.py new file mode 100644 index 00000000..e4fb0f9c --- /dev/null +++ b/services/common/snmpwalk_helpers.py @@ -0,0 +1,112 @@ +# Check for which J2735 PSID matches val +# BSM - 20 +# SPaT - 8002 +# TIM - 8003 +# MAP - E0000017 +# SSM - E0000015 +# SRM - E0000016 +def message_type_rsu41(val): + # Various formats PSIDs have been observed to be returned in + # Can depend upon vendor or even firmware version of the same vendor's RSU + if val == '" "' or val == "00 00 00 20" or val == "00 00 00 32": + return "BSM" + elif val == "00 00 80 02" or val == "80 02" or val == "00 03 27 70": + return "SPaT" + elif val == "00 00 80 03" or val == "80 03" or val == "00 03 27 71": + return "TIM" + elif val == "E0 00 00 17" or val == "37 58 09 64 07": + return "MAP" + elif val == "E0 00 00 15" or val == "37 58 09 64 06": + return "SSM" + elif val == "E0 00 00 16" or val == "37 58 09 64 05": + return "SRM" + return "Other" + + +def message_type_ntcip1218(val): + if val == "20000000": + return "BSM" + elif val == "80020000": + return "SPaT" + elif val == "80030000": + return "TIM" + elif val.lower() == "e0000017": + return "MAP" + elif val.lower() == "e0000015": + return "SSM" + elif val.lower() == "e0000016": + return "SRM" + return "Other" + + +# Little endian +def ip_rsu41(val): + hex = val.split() + ipaddr = ( + f"{str(int(hex[-4], 16))}." + f"{str(int(hex[-3], 16))}." + f"{str(int(hex[-2], 16))}." + f"{str(int(hex[-1], 16))}" + ) + return ipaddr + + +def ip_ntcip1218(val): + return val.strip() + + +def protocol(val): + if val == "1" or val == "tcp(1)": + return "TCP" + elif val == "2" or val == "udp(2)": + return "UDP" + return "Other" + + +def rssi_ntcip1218(val): + return int(val.split(" ")[0]) + + +def fwdon(val): + if val == "1": + return "On" + return "Off" + + +def active(val): + # This value represents an active state + # Currently 1 and 4 are supported + # 1 - active + # 4 - create (represents active to older Commsignia models) + # active(1) - active for NTCIP 1218 response + if val == "1" or val == "4" or val == "active(1)": + return "Enabled" + return "Disabled" + + +def startend_rsu41(val): + hex = val.split() + year = str(int(hex[0] + hex[1], 16)) + month = str(int(hex[2], 16)) + month = month if len(month) == 2 else "0" + month + day = str(int(hex[3], 16)) + day = day if len(day) == 2 else "0" + day + hour = str(int(hex[4], 16)) + hour = hour if len(hour) == 2 else "0" + hour + min = str(int(hex[5], 16)) + min = min if len(min) == 2 else "0" + min + return f"{year}-{month}-{day} {hour}:{min}" + + +def startend_ntcip1218(val): + date, time = val.split(",") + # Parse out the year, month and day. Pad 0s if necessary + year, month, day = date.split("-") + month = "0" + month if len(month) == 1 else month + day = "0" + day if len(day) == 1 else day + # Parse out the hours, minute and second. Pad 0s if necessary + hour, minute = time.split(":")[0], time.split(":")[1] + hour = "0" + hour if len(hour) == 1 else hour + minute = "0" + minute if len(minute) == 1 else minute + # Return the processed datetime string + return f"{year}-{month}-{day} {hour}:{minute}" diff --git a/services/common/tests/data/test_update_rsu_snm_pg_data.py b/services/common/tests/data/test_update_rsu_snm_pg_data.py new file mode 100644 index 00000000..a6ca4873 --- /dev/null +++ b/services/common/tests/data/test_update_rsu_snm_pg_data.py @@ -0,0 +1,280 @@ +# General data + +snmp_config_data = [ + { + "rsu_id": 1, + "msgfwd_type": 2, + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + { + "rsu_id": 2, + "msgfwd_type": 3, + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "5.5.5.5", + "dest_port": 44920, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, +] + +snmp_config_data_msgfwd_type_str = [ + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + { + "rsu_id": 2, + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "5.5.5.5", + "dest_port": 44920, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, +] + +msgfwd_types = { + "rsuDsrcFwd": 1, + "rsuReceivedMsg": 2, + "rsuXmitMsgFwding": 3, +} + +# test_update_postgresql + +sample_rsu_snmp_configs_obj_1 = { + 1: [ + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], + 2: [ + { + "rsu_id": 2, + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "5.5.5.5", + "dest_port": 44920, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], + 3: "Unable to retrieve latest SNMP config", +} + +sample_rsu_snmp_configs_obj_2 = { + 1: [], + 2: "Unable to retrieve latest SNMP config", +} + +sample_rsu_snmp_configs_obj_3 = { + 1: [ + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], +} + +snmp_config_data_msgfwd_type_str_2 = [ + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, +] + +# test_get_snmp_configs + +rsu_list = [ + { + "rsu_id": 1, + "ipv4_address": "5.5.5.5", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 2, + "ipv4_address": "6.6.6.6", + "snmp_version": "1218", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 3, + "ipv4_address": "7.7.7.7", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, + { + "rsu_id": 4, + "ipv4_address": "8.8.8.8", + "snmp_version": "41", + "snmp_username": "username", + "snmp_password": "password", + }, +] + +side_effect_return_values = [ + ( + { + "RsuFwdSnmpwalk": { + "rsuReceivedMsgTable": { + 1: { + "Message Type": "BSM", + "IP": "10.0.0.5", + "Port": 46800, + "Start DateTime": "2024-02-05 00:00", + "End DateTime": "2034-02-05 00:00", + "Config Active": "Enabled", + } + }, + "rsuXmitMsgFwdingTable": {}, + } + }, + 200, + ), + ( + { + "RsuFwdSnmpwalk": { + "rsuReceivedMsgTable": { + 1: { + "Message Type": "BSM", + "IP": "10.0.0.8", + "Port": 46800, + "Start DateTime": "2024-02-05 00:00", + "End DateTime": "2034-02-05 00:00", + "Config Active": "Enabled", + } + }, + "rsuXmitMsgFwdingTable": { + 1: { + "Message Type": "MAP", + "IP": "10.0.0.8", + "Port": 44920, + "Start DateTime": "2024-02-05 00:00", + "End DateTime": "2034-02-05 00:00", + "Config Active": "Enabled", + } + }, + } + }, + 200, + ), + ( + { + "RsuFwdSnmpwalk": { + 1: { + "Message Type": "BSM", + "IP": "10.0.0.8", + "Port": 46800, + "Start DateTime": "2024-02-05 00:00", + "End DateTime": "2034-02-05 00:00", + "Config Active": "Enabled", + } + } + }, + 200, + ), + ( + { + "RsuFwdSnmpwalk": "Authentication failure (incorrect password, community or key)" + }, + 500, + ), +] + +get_snmp_configs_expected = { + 1: [ + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "10.0.0.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], + 2: [ + { + "rsu_id": 2, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "10.0.0.8", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + { + "rsu_id": 2, + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "10.0.0.8", + "dest_port": 44920, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], + 3: [ + { + "rsu_id": 3, + "msgfwd_type": "rsuDsrcFwd", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "10.0.0.8", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + }, + ], + 4: "Unable to retrieve latest SNMP config", +} diff --git a/services/api/tests/src/test_emailSender.py b/services/common/tests/test_emailSender.py similarity index 92% rename from services/api/tests/src/test_emailSender.py rename to services/common/tests/test_emailSender.py index 33cec476..7eee56c7 100644 --- a/services/api/tests/src/test_emailSender.py +++ b/services/common/tests/test_emailSender.py @@ -1,5 +1,5 @@ from unittest.mock import MagicMock -from api.src.emailSender import EmailSender +from common.emailSender import EmailSender EMAIL_TO_SEND_FROM = "test@test.test" @@ -38,7 +38,7 @@ def test_send(): # assert emailSender.server.starttls.assert_called_once() - assert emailSender.server.ehlo.call_count == 2 + emailSender.server.ehlo.assert_called_once() emailSender.server.login.assert_called_once() emailSender.server.sendmail.assert_called_once() emailSender.server.quit.assert_called_once() diff --git a/services/api/tests/src/test_rsufwdsnmpset.py b/services/common/tests/test_rsufwdsnmpset.py similarity index 81% rename from services/api/tests/src/test_rsufwdsnmpset.py rename to services/common/tests/test_rsufwdsnmpset.py index 893f4d16..32b2b39c 100644 --- a/services/api/tests/src/test_rsufwdsnmpset.py +++ b/services/common/tests/test_rsufwdsnmpset.py @@ -1,12 +1,12 @@ import datetime from unittest.mock import MagicMock, call, patch, Mock, create_autospec -from api.src import rsufwdsnmpset +from common import rsufwdsnmpset import subprocess # static values rsu_ip = "192.168.0.20" -snmp_creds = {"username": "test_username", "password": "test_password"} +snmp_creds = {"username": "test_username", "password": "test_password", "encrypt_pw": None} dest_ip = "192.168.0.10" rsu_index = 1 @@ -23,7 +23,7 @@ def test_hex_datetime(): assert rsufwdsnmpset.hex_datetime(dt) == expected -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.subprocess.run") def test_set_rsu_status_operate(mock_run): # mock mock_run.return_value = Mock() @@ -44,7 +44,7 @@ def test_set_rsu_status_operate(mock_run): assert response == expected_response -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.subprocess.run") def test_set_rsu_status_standby(mock_run): # mock mock_run.return_value = Mock() @@ -65,7 +65,7 @@ def test_set_rsu_status_standby(mock_run): assert response == expected_response -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.subprocess.run") def test_perform_snmp_mods(subprocess_run): # mock subprocess_run.return_value = Mock() @@ -92,8 +92,8 @@ def test_perform_snmp_mods(subprocess_run): assert response == expected_response -@patch("api.src.rsufwdsnmpset.subprocess.run") -@patch("api.src.rsufwdsnmpset.perform_snmp_mods") +@patch("common.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.perform_snmp_mods") def test_config_txrxmsg_tx(mock_perform_snmp_mods, mock_subprocess_run): # mock mock_subprocess_run.return_value = Mock() @@ -114,8 +114,8 @@ def test_config_txrxmsg_tx(mock_perform_snmp_mods, mock_subprocess_run): assert result == expected_result -@patch("api.src.rsufwdsnmpset.subprocess.run") -@patch("api.src.rsufwdsnmpset.perform_snmp_mods") +@patch("common.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.perform_snmp_mods") def test_config_txrxmsg_no_tx(mock_perform_snmp_mods, mock_subprocess_run): # mock mock_subprocess_run.return_value = Mock() @@ -136,7 +136,7 @@ def test_config_txrxmsg_no_tx(mock_perform_snmp_mods, mock_subprocess_run): assert result == expected_result -@patch("api.src.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.set_rsu_status") def test_config_rsudsrcfwd(mock_set_rsu_status): manufacturer = "test_manufacturer" udp_port = 1234 @@ -157,8 +157,8 @@ def test_config_rsudsrcfwd(mock_set_rsu_status): ) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_rsu41(mock_subprocess_run, mock_set_rsu_status): # mock subprocess.run mock_subprocess_run.return_value = Mock() @@ -185,8 +185,8 @@ def test_config_del_rsu41(mock_subprocess_run, mock_set_rsu_status): mock_subprocess_run.assert_called_once() -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_rsu41_set_rsu_status_failure( mock_subprocess_run, mock_set_rsu_status ): @@ -218,8 +218,8 @@ def test_config_del_rsu41_set_rsu_status_failure( assert result == ("failure", 500) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_ntcip1218_bsm(mock_subprocess_run, mock_set_rsu_status): # mock mock_subprocess_run.return_value = Mock() @@ -245,8 +245,8 @@ def test_config_del_ntcip1218_bsm(mock_subprocess_run, mock_set_rsu_status): assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_ntcip1218_spat(mock_subprocess_run, mock_set_rsu_status): # mock mock_subprocess_run.return_value = Mock() @@ -272,8 +272,8 @@ def test_config_del_ntcip1218_spat(mock_subprocess_run, mock_set_rsu_status): assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_ntcip1218_map(mock_subprocess_run, mock_set_rsu_status): # mock mock_subprocess_run.return_value = Mock() @@ -299,8 +299,8 @@ def test_config_del_ntcip1218_map(mock_subprocess_run, mock_set_rsu_status): assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_ntcip1218_ssm(mock_subprocess_run, mock_set_rsu_status): # mock mock_subprocess_run.return_value = Mock() @@ -326,8 +326,8 @@ def test_config_del_ntcip1218_ssm(mock_subprocess_run, mock_set_rsu_status): assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) -@patch("api.src.rsufwdsnmpset.set_rsu_status") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") def test_config_del_ntcip1218_srm(mock_subprocess_run, mock_set_rsu_status): # mock mock_subprocess_run.return_value = Mock() @@ -353,7 +353,34 @@ def test_config_del_ntcip1218_srm(mock_subprocess_run, mock_set_rsu_status): assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) -@patch("api.src.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.set_rsu_status") +@patch("common.rsufwdsnmpset.subprocess.run") +def test_config_del_ntcip1218_tim(mock_subprocess_run, mock_set_rsu_status): + # mock + mock_subprocess_run.return_value = Mock() + mock_subprocess_run.return_value.stdout = Mock() + mock_subprocess_run.return_value.stdout.decode.return_value = "test_output" + mock_set_rsu_status.return_value = "success" + + # call + snmp_version = "1218" + msg_type = "tim" + result = rsufwdsnmpset.config_del( + rsu_ip, snmp_version, snmp_creds, msg_type, rsu_index + ) + + # check + mock_set_rsu_status.assert_not_called() + mock_subprocess_run.assert_called_once_with( + "snmpset -v 3 -u test_username -a SHA -A test_password -x AES -X test_password -l authpriv 192.168.0.20 NTCIP1218-v01:rsuXmitMsgFwdingStatus.1 i 6 ", + shell=True, + capture_output=True, + check=True, + ) + assert result == ("Successfully deleted the NTCIP 1218 SNMPSET configuration", 200) + + +@patch("common.rsufwdsnmpset.set_rsu_status") def test_config_del_unsupported_snmp_version(mock_set_rsu_status): # prepare args snmp_version = "test_version" @@ -375,8 +402,8 @@ def test_config_del_unsupported_snmp_version(mock_set_rsu_status): mock_set_rsu_status.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_commsignia_bsm(mock_config_msgfwd_yunex, mock_config_msgfwd): mock_config_msgfwd.return_value = "success" snmp_version = "41" @@ -393,8 +420,8 @@ def test_config_init_commsignia_bsm(mock_config_msgfwd_yunex, mock_config_msgfwd mock_config_msgfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_commsignia_spat(mock_config_msgfwd_yunex, mock_config_msgfwd): mock_config_msgfwd.return_value = "success" snmp_version = "41" @@ -411,8 +438,8 @@ def test_config_init_commsignia_spat(mock_config_msgfwd_yunex, mock_config_msgfw mock_config_msgfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_commsignia_map(mock_config_msgfwd_yunex, mock_config_msgfwd): mock_config_msgfwd.return_value = "success" snmp_version = "41" @@ -436,8 +463,8 @@ def test_config_init_commsignia_map(mock_config_msgfwd_yunex, mock_config_msgfwd mock_config_msgfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_commsignia_ssm(mock_config_msgfwd_yunex, mock_config_msgfwd): mock_config_msgfwd.return_value = "success" snmp_version = "41" @@ -461,8 +488,8 @@ def test_config_init_commsignia_ssm(mock_config_msgfwd_yunex, mock_config_msgfwd mock_config_msgfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_commsignia_srm(mock_config_msgfwd_yunex, mock_config_msgfwd): mock_config_msgfwd.return_value = "success" snmp_version = "41" @@ -486,8 +513,33 @@ def test_config_init_commsignia_srm(mock_config_msgfwd_yunex, mock_config_msgfwd mock_config_msgfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") +def test_config_init_commsignia_tim(mock_config_msgfwd_yunex, mock_config_msgfwd): + mock_config_msgfwd.return_value = "success" + snmp_version = "41" + manufacturer = "Commsignia" + msg_type = "TIM" + result = rsufwdsnmpset.config_init( + rsu_ip, manufacturer, snmp_version, snmp_creds, dest_ip, msg_type, rsu_index + ) + expected_result = "success" + assert result == expected_result + mock_config_msgfwd.assert_called_once_with( + rsu_ip, + manufacturer, + snmp_creds, + dest_ip, + "47900", + rsu_index, + "8003", + raw=True, + ) + mock_config_msgfwd_yunex.assert_not_called() + + +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_unsupported_msg_type_rsu41( mock_config_msfwd_yunex, mock_config_msgfwd ): @@ -498,7 +550,7 @@ def test_config_init_unsupported_msg_type_rsu41( rsu_ip, manufacturer, snmp_version, snmp_creds, dest_ip, msg_type, rsu_index ) expected_result = ( - "Supported message type is currently only BSM, SPaT, MAP, SSM and SRM", + "Supported message type is currently only BSM, SPaT, MAP, SSM, SRM and TIM", 501, ) assert result == expected_result @@ -506,8 +558,8 @@ def test_config_init_unsupported_msg_type_rsu41( mock_config_msfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_unsupported_msg_type_ntcip1218( mock_config_msfwd_yunex, mock_config_msgfwd ): @@ -518,7 +570,7 @@ def test_config_init_unsupported_msg_type_ntcip1218( rsu_ip, manufacturer, snmp_version, snmp_creds, dest_ip, msg_type, rsu_index ) expected_result = ( - "Supported message type is currently only BSM, SPaT, MAP, SSM and SRM", + "Supported message type is currently only BSM, SPaT, MAP, SSM, SRM and TIM", 501, ) assert result == expected_result @@ -526,8 +578,8 @@ def test_config_init_unsupported_msg_type_ntcip1218( mock_config_msfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.config_rsudsrcfwd") -@patch("api.src.rsufwdsnmpset.config_txrxmsg") +@patch("common.rsufwdsnmpset.config_rsudsrcfwd") +@patch("common.rsufwdsnmpset.config_txrxmsg") def test_config_init_unsupported_snmp_version( mock_config_msfwd_yunex, mock_config_msgfwd ): @@ -546,8 +598,8 @@ def test_config_init_unsupported_snmp_version( mock_config_msfwd_yunex.assert_not_called() -@patch("api.src.rsufwdsnmpset.SnmpsetSchema.validate") -@patch("api.src.rsufwdsnmpset.config_init") +@patch("common.rsufwdsnmpset.SnmpsetSchema.validate") +@patch("common.rsufwdsnmpset.config_init") def test_post(mock_config_init, mock_validate): # mock validate mock_validate.return_value = None @@ -590,8 +642,8 @@ def test_post(mock_config_init, mock_validate): assert result == expected_result -@patch("api.src.rsufwdsnmpset.SnmpsetSchema.validate") -@patch("api.src.rsufwdsnmpset.config_init") +@patch("common.rsufwdsnmpset.SnmpsetSchema.validate") +@patch("common.rsufwdsnmpset.config_init") def test_post_error(mock_config_init, mock_validate): # mock validate mock_validate.return_value = "error" @@ -623,8 +675,8 @@ def test_post_error(mock_config_init, mock_validate): ) -@patch("api.src.rsufwdsnmpset.SnmpsetDeleteSchema.validate") -@patch("api.src.rsufwdsnmpset.config_del") +@patch("common.rsufwdsnmpset.SnmpsetDeleteSchema.validate") +@patch("common.rsufwdsnmpset.config_del") def test_delete(mock_config_del, mock_validate): # mock validate mock_validate.return_value = None @@ -664,8 +716,8 @@ def test_delete(mock_config_del, mock_validate): assert result == expected_result -@patch("api.src.rsufwdsnmpset.SnmpsetDeleteSchema.validate") -@patch("api.src.rsufwdsnmpset.config_del") +@patch("common.rsufwdsnmpset.SnmpsetDeleteSchema.validate") +@patch("common.rsufwdsnmpset.config_del") def test_delete_error(mock_config_del, mock_validate): # mock validate mock_validate.return_value = "error" @@ -697,13 +749,13 @@ def test_delete_error(mock_config_del, mock_validate): assert result == expected_result -@patch("api.src.rsufwdsnmpset.set_rsu_status", return_value="success") -@patch("api.src.rsufwdsnmpset.perform_snmp_mods") +@patch("common.rsufwdsnmpset.set_rsu_status", return_value="success") +@patch("common.rsufwdsnmpset.perform_snmp_mods") def test_config_rsudsrcfwd_raw_false(mock_perform_snmp_mods, mock_set_rsu_status): # Set up test data rsu_ip = "192.168.1.1" manufacturer = "Commsignia" - snmp_creds = {"username": "username", "password": "password"} + snmp_creds = {"username": "username", "password": "password", "encrypt_pw": None} dest_ip = "192.168.1.2" index = 1 psid = "20" @@ -726,12 +778,12 @@ def test_config_rsudsrcfwd_raw_false(mock_perform_snmp_mods, mock_set_rsu_status mock_perform_snmp_mods.assert_called_once() -@patch("api.src.rsufwdsnmpset.set_rsu_status", return_value="success") -@patch("api.src.rsufwdsnmpset.perform_snmp_mods") +@patch("common.rsufwdsnmpset.set_rsu_status", return_value="success") +@patch("common.rsufwdsnmpset.perform_snmp_mods") def test_config_rsudsrcfwd_raw_true(mock_perform_snmp_mods, mock_set_rsu_status): # Set up test data rsu_ip = "192.168.1.1" - snmp_creds = {"username": "username", "password": "password"} + snmp_creds = {"username": "username", "password": "password", "encrypt_pw": None} dest_ip = "192.168.1.2" index = 1 psid = "20" @@ -752,11 +804,9 @@ def raise_called_process_error(*args, **kwargs): raise error +@patch("common.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string") @patch( - "api.src.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string" -) -@patch( - "api.src.rsufwdsnmpset.snmperrorcheck.check_error_type", + "common.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="error message", ) @patch("subprocess.run", side_effect=raise_called_process_error) @@ -774,16 +824,12 @@ def test_set_rsu_status_exception(mock_run, mock_check_error_type, mock_get_auth assert result == "error message" +@patch("common.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string") @patch( - "api.src.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string" -) -@patch( - "api.src.rsufwdsnmpset.snmperrorcheck.check_error_type", + "common.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="error message", ) -@patch( - "api.src.rsufwdsnmpset.perform_snmp_mods", side_effect=raise_called_process_error -) +@patch("common.rsufwdsnmpset.perform_snmp_mods", side_effect=raise_called_process_error) def test_config_txrxmsg_exception( mock_perform_snmp_mods, mock_check_error_type, mock_get_authstring ): @@ -806,17 +852,13 @@ def test_config_txrxmsg_exception( assert response == "error message" +@patch("common.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string") @patch( - "api.src.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string" -) -@patch( - "api.src.rsufwdsnmpset.snmperrorcheck.check_error_type", + "common.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="error message", ) -@patch( - "api.src.rsufwdsnmpset.perform_snmp_mods", side_effect=raise_called_process_error -) -@patch("api.src.rsufwdsnmpset.set_rsu_status", return_value="success") +@patch("common.rsufwdsnmpset.perform_snmp_mods", side_effect=raise_called_process_error) +@patch("common.rsufwdsnmpset.set_rsu_status", return_value="success") def test_config_rsudsrcfwd_exception( mock_set_rsu_status, mock_perform_snmp_mods, @@ -846,15 +888,13 @@ def test_config_rsudsrcfwd_exception( assert response == "error message" +@patch("common.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string") @patch( - "api.src.rsufwdsnmpset.snmpcredential.get_authstring", return_value="auth_string" -) -@patch( - "api.src.rsufwdsnmpset.snmperrorcheck.check_error_type", + "common.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="error message", ) -@patch("api.src.rsufwdsnmpset.subprocess.run", side_effect=raise_called_process_error) -@patch("api.src.rsufwdsnmpset.set_rsu_status", return_value="success") +@patch("common.rsufwdsnmpset.subprocess.run", side_effect=raise_called_process_error) +@patch("common.rsufwdsnmpset.set_rsu_status", return_value="success") def test_config_del_rsu41_exception( mock_set_rsu_status, mock_run, mock_check_error_type, mock_get_authstring ): @@ -878,10 +918,10 @@ def test_config_del_rsu41_exception( assert response == "error message" -@patch("api.src.rsufwdsnmpset.set_rsu_status", return_value="success") -@patch("api.src.rsufwdsnmpset.subprocess.run") +@patch("common.rsufwdsnmpset.set_rsu_status", return_value="success") +@patch("common.rsufwdsnmpset.subprocess.run") @patch( - "api.src.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="test error" + "common.rsufwdsnmpset.snmperrorcheck.check_error_type", return_value="test error" ) def test_config_del_ntcip1218_exception( mock_check_error_type, mock_run, mock_set_rsu_status @@ -889,7 +929,7 @@ def test_config_del_ntcip1218_exception( # Setup rsu_ip = "192.168.1.1" snmp_version = "1218" - snmp_creds = {"username": "username", "password": "password"} + snmp_creds = {"username": "username", "password": "password", "encrypt_pw": None} msg_type = ( "bsm" # This can be any of the following: ['bsm', 'spat', 'map', 'ssm', 'srm'] ) diff --git a/services/api/tests/src/test_rsufwdsnmpwalk.py b/services/common/tests/test_rsufwdsnmpwalk.py similarity index 67% rename from services/api/tests/src/test_rsufwdsnmpwalk.py rename to services/common/tests/test_rsufwdsnmpwalk.py index ed39480c..e2c38c32 100644 --- a/services/api/tests/src/test_rsufwdsnmpwalk.py +++ b/services/common/tests/test_rsufwdsnmpwalk.py @@ -1,94 +1,12 @@ from unittest.mock import Mock, patch -from api.src import rsufwdsnmpwalk +from common import rsufwdsnmpwalk source_ip = "192.168.0.10" rsu_ip = "192.168.0.20" snmp_creds = {"ip": source_ip, "username": "public", "password": "public"} -def test_message_type(): - # test BSM PSIDs - val = '" "' - assert rsufwdsnmpwalk.message_type(val) == "BSM" - val = "00 00 00 20" - assert rsufwdsnmpwalk.message_type(val) == "BSM" - - # test SPAT PSIDs - val = "00 00 80 02" - assert rsufwdsnmpwalk.message_type(val) == "SPaT" - val = "80 02" - assert rsufwdsnmpwalk.message_type(val) == "SPaT" - - # test MAP PSID - val = "E0 00 00 17" - assert rsufwdsnmpwalk.message_type(val) == "MAP" - - # test SSM PSID - val = "E0 00 00 15" - assert rsufwdsnmpwalk.message_type(val) == "SSM" - - # test SRM PSID - val = "E0 00 00 16" - assert rsufwdsnmpwalk.message_type(val) == "SRM" - - # test other PSID - val = "00 00 00 00" - assert rsufwdsnmpwalk.message_type(val) == "Other" - - -def test_ip(): - val = "c0 a8 00 0a" - assert rsufwdsnmpwalk.ip(val) == "192.168.0.10" - - -def test_yunex_ip(): - val = '"10.0.0.1"' - assert rsufwdsnmpwalk.yunex_ip(val) == "10.0.0.1" - - -def test_protocol(): - val = "1" - assert rsufwdsnmpwalk.protocol(val) == "TCP" - val = "2" - assert rsufwdsnmpwalk.protocol(val) == "UDP" - val = "14" - assert rsufwdsnmpwalk.protocol(val) == "Other" - - -def test_fwdon(): - val = "1" - assert rsufwdsnmpwalk.fwdon(val) == "On" - val = "0" - assert rsufwdsnmpwalk.fwdon(val) == "Off" - - -def test_active(): - val = "1" - assert rsufwdsnmpwalk.active(val) == "Enabled" - val = "4" - assert rsufwdsnmpwalk.active(val) == "Enabled" - val = "17" - assert rsufwdsnmpwalk.active(val) == "Disabled" - - -def test_toint(): - mystr = "123" - assert rsufwdsnmpwalk.toint(mystr) == 123 - - -def test_startend(): - # prepare hex input - hex_input = "05 06 07 08 09 10" - - # call function - output = rsufwdsnmpwalk.startend(hex_input) - - # verify - expected_output = "1286-07-08 09:16" - assert output == expected_output - - -@patch("api.src.rsufwdsnmpwalk.subprocess.run") +@patch("common.rsufwdsnmpwalk.subprocess.run") def test_snmpwalk_rsudsrcfwd_no_snmp_config(mock_subprocess_run): # mock mock_subprocess_run.return_value = Mock() @@ -96,7 +14,7 @@ def test_snmpwalk_rsudsrcfwd_no_snmp_config(mock_subprocess_run): mock_subprocess_run.return_value.stdout.decode.return_value = "test" # prepare input - snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public"} + snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public", "encrypt_pw": None} rsu_ip = "192.168.0.20" # call function @@ -108,7 +26,7 @@ def test_snmpwalk_rsudsrcfwd_no_snmp_config(mock_subprocess_run): assert output == expected_output -@patch("api.src.rsufwdsnmpwalk.subprocess.run") +@patch("common.rsufwdsnmpwalk.subprocess.run") def test_snmpwalk_rsudsrcfwd_with_snmp_config(mock_subprocess_run): # mock mock_subprocess_run.return_value = Mock() @@ -118,7 +36,7 @@ def test_snmpwalk_rsudsrcfwd_with_snmp_config(mock_subprocess_run): ) # prepare input - snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public"} + snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public", "encrypt_pw": None} rsu_ip = "192.168.0.20" # call function @@ -132,7 +50,7 @@ def test_snmpwalk_rsudsrcfwd_with_snmp_config(mock_subprocess_run): def test_snmpwalk_rsudsrcfwd_exception(): # prepare input - snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public"} + snmp_creds = {"ip": "192.168.0.10", "username": "public", "password": "public", "encrypt_pw": None} rsu_ip = "192.168.0.20" # call function @@ -153,7 +71,7 @@ def test_snmpwalk_rsudsrcfwd_exception(): assert output in expected_possible_outputs -@patch("api.src.rsufwdsnmpwalk.subprocess.run") +@patch("common.rsufwdsnmpwalk.subprocess.run") def test_snmpwalk_txrxmsg(mock_subprocess_run): # mock mock_subprocess_run.return_value = Mock() @@ -162,7 +80,7 @@ def test_snmpwalk_txrxmsg(mock_subprocess_run): # prepare input source_ip = "192.168.0.10" - snmp_creds = {"ip": source_ip, "username": "public", "password": "public"} + snmp_creds = {"ip": source_ip, "username": "public", "password": "public", "encrypt_pw": None} rsu_ip = "192.168.0.20" # call function @@ -177,7 +95,7 @@ def test_snmpwalk_txrxmsg(mock_subprocess_run): def test_snmpwalk_txrxmsg_exception(): # prepare input source_ip = "192.168.0.10" - snmp_creds = {"ip": source_ip, "username": "public", "password": "public"} + snmp_creds = {"ip": source_ip, "username": "public", "password": "public", "encrypt_pw": None} rsu_ip = "192.168.0.20" # call function @@ -198,8 +116,8 @@ def test_snmpwalk_txrxmsg_exception(): assert output in expected_possible_outputs -@patch("api.src.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") -@patch("api.src.rsufwdsnmpwalk.snmpwalk_txrxmsg") +@patch("common.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") +@patch("common.rsufwdsnmpwalk.snmpwalk_txrxmsg") def test_get_rsu41(mock_snmpwalk_txrxmsg, mock_snmpwalk_rsudsrcfwd): # prepare input request = { @@ -217,8 +135,8 @@ def test_get_rsu41(mock_snmpwalk_txrxmsg, mock_snmpwalk_rsudsrcfwd): mock_snmpwalk_txrxmsg.assert_not_called() -@patch("api.src.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") -@patch("api.src.rsufwdsnmpwalk.snmpwalk_txrxmsg") +@patch("common.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") +@patch("common.rsufwdsnmpwalk.snmpwalk_txrxmsg") def test_get_ntcip1218(mock_snmpwalk_txrxmsg, mock_snmpwalk_rsudsrcfwd): # prepare input request = { @@ -236,8 +154,8 @@ def test_get_ntcip1218(mock_snmpwalk_txrxmsg, mock_snmpwalk_rsudsrcfwd): mock_snmpwalk_txrxmsg.assert_called_once_with(snmp_creds, rsu_ip) -@patch("api.src.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") -@patch("api.src.rsufwdsnmpwalk.snmpwalk_txrxmsg") +@patch("common.rsufwdsnmpwalk.snmpwalk_rsudsrcfwd") +@patch("common.rsufwdsnmpwalk.snmpwalk_txrxmsg") def test_get_exception(mock_snmpwalk_txrxmsg, mock_snmpwalk_rsudsrcfwd): # prepare input request = { diff --git a/services/common/tests/test_snmpcredential.py b/services/common/tests/test_snmpcredential.py new file mode 100644 index 00000000..916dd555 --- /dev/null +++ b/services/common/tests/test_snmpcredential.py @@ -0,0 +1,12 @@ +from common import snmpcredential + + +def test_get_authstring(): + snmp_creds = {"username": "testuser", "password": "testpassword", "encrypt_pw": "encryptpassword"} + expected = "-u testuser -a SHA -A testpassword -x AES -X encryptpassword -l authpriv" + assert snmpcredential.get_authstring(snmp_creds) == expected + +def test_get_authstring_no_encrypt(): + snmp_creds = {"username": "testuser", "password": "testpassword", "encrypt_pw": None} + expected = "-u testuser -a SHA -A testpassword -x AES -X testpassword -l authpriv" + assert snmpcredential.get_authstring(snmp_creds) == expected \ No newline at end of file diff --git a/services/api/tests/src/test_snmperrorcheck.py b/services/common/tests/test_snmperrorcheck.py similarity index 95% rename from services/api/tests/src/test_snmperrorcheck.py rename to services/common/tests/test_snmperrorcheck.py index 03ceaa3e..5f52b5c0 100644 --- a/services/api/tests/src/test_snmperrorcheck.py +++ b/services/common/tests/test_snmperrorcheck.py @@ -1,4 +1,4 @@ -from api.src import snmperrorcheck +from common import snmperrorcheck def test_check_error_type(): diff --git a/services/common/tests/test_snmpwalk_helpers.py b/services/common/tests/test_snmpwalk_helpers.py new file mode 100644 index 00000000..8b9dedd3 --- /dev/null +++ b/services/common/tests/test_snmpwalk_helpers.py @@ -0,0 +1,125 @@ +from unittest.mock import Mock, patch +from common import snmpwalk_helpers + + +def test_message_type_rsu41(): + # test BSM PSIDs + val = '" "' + assert snmpwalk_helpers.message_type_rsu41(val) == "BSM" + val = "00 00 00 20" + assert snmpwalk_helpers.message_type_rsu41(val) == "BSM" + + # test SPAT PSIDs + val = "00 00 80 02" + assert snmpwalk_helpers.message_type_rsu41(val) == "SPaT" + val = "80 02" + assert snmpwalk_helpers.message_type_rsu41(val) == "SPaT" + + # test TIM PSID + val = "00 00 80 03" + assert snmpwalk_helpers.message_type_rsu41(val) == "TIM" + + # test MAP PSID + val = "E0 00 00 17" + assert snmpwalk_helpers.message_type_rsu41(val) == "MAP" + + # test SSM PSID + val = "E0 00 00 15" + assert snmpwalk_helpers.message_type_rsu41(val) == "SSM" + + # test SRM PSID + val = "E0 00 00 16" + assert snmpwalk_helpers.message_type_rsu41(val) == "SRM" + + # test other PSID + val = "00 00 00 00" + assert snmpwalk_helpers.message_type_rsu41(val) == "Other" + + +def test_message_type_ntcip1218(): + # test BSM PSID + val = "20000000" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "BSM" + + # test SPAT PSID + val = "80020000" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "SPaT" + + # test TIM PSID + val = "80030000" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "TIM" + + # test MAP PSID + val = "e0000017" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "MAP" + + # test SSM PSID + val = "e0000015" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "SSM" + + # test SRM PSID + val = "e0000016" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "SRM" + + # test other PSID + val = "00000000" + assert snmpwalk_helpers.message_type_ntcip1218(val) == "Other" + + +def test_ip(): + val = "c0 a8 00 0a" + assert snmpwalk_helpers.ip_rsu41(val) == "192.168.0.10" + + +def test_ip_ntcip1218(): + val = "10.0.0.1" + assert snmpwalk_helpers.ip_ntcip1218(val) == "10.0.0.1" + + +def test_protocol(): + val = "1" + assert snmpwalk_helpers.protocol(val) == "TCP" + val = "2" + assert snmpwalk_helpers.protocol(val) == "UDP" + val = "14" + assert snmpwalk_helpers.protocol(val) == "Other" + + +def test_fwdon(): + val = "1" + assert snmpwalk_helpers.fwdon(val) == "On" + val = "0" + assert snmpwalk_helpers.fwdon(val) == "Off" + + +def test_active(): + val = "1" + assert snmpwalk_helpers.active(val) == "Enabled" + val = "4" + assert snmpwalk_helpers.active(val) == "Enabled" + val = "17" + assert snmpwalk_helpers.active(val) == "Disabled" + + +def test_startend_rsu41(): + # prepare hex input + hex_input = "05 06 07 08 09 10" + + # call function + output = snmpwalk_helpers.startend_rsu41(hex_input) + + # verify + expected_output = "1286-07-08 09:16" + assert output == expected_output + + +def test_startend_ntcip1218(): + # prepare hex input + str_input = "2024-02-05,9:5:31.45" + + # call function + output = snmpwalk_helpers.startend_ntcip1218(str_input) + + # verify + expected_output = "2024-02-05 09:05" + assert output == expected_output diff --git a/services/common/tests/test_update_rsu_snmp_pg.py b/services/common/tests/test_update_rsu_snmp_pg.py new file mode 100644 index 00000000..d55dd6d9 --- /dev/null +++ b/services/common/tests/test_update_rsu_snmp_pg.py @@ -0,0 +1,192 @@ +from mock import MagicMock, call, patch +from common import update_rsu_snmp_pg +from common.tests.data import test_update_rsu_snm_pg_data + + +@patch("common.update_rsu_snmp_pg.pgquery.write_db") +def test_insert_config_list(mock_write_db): + # call + update_rsu_snmp_pg.insert_config_list(test_update_rsu_snm_pg_data.snmp_config_data) + + # check + expected_query = ( + "INSERT INTO public.snmp_msgfwd_config(" + "rsu_id, msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active) " + "VALUES " + "(1, 2, 1, 'BSM', '5.5.5.5', 46800, '2024-02-05 00:00', '2034-02-05 00:00', '1'), " + "(2, 3, 1, 'MAP', '5.5.5.5', 44920, '2024-02-05 00:00', '2034-02-05 00:00', '1')" + ) + mock_write_db.assert_called_with(expected_query) + + +@patch("common.update_rsu_snmp_pg.pgquery.write_db") +def test_delete_config_list(mock_write_db): + # call + update_rsu_snmp_pg.delete_config_list(test_update_rsu_snm_pg_data.snmp_config_data) + + # check + mock_write_db.assert_has_calls( + [ + call( + "DELETE FROM public.snmp_msgfwd_config WHERE rsu_id=1 AND msgfwd_type=2 AND snmp_index=1" + ), + call( + "DELETE FROM public.snmp_msgfwd_config WHERE rsu_id=2 AND msgfwd_type=3 AND snmp_index=1" + ), + ] + ) + + +@patch("common.update_rsu_snmp_pg.pgquery.query_db") +def test_get_msgfwd_types(mock_query_db): + mock_query_db.return_value = [ + ({"snmp_msgfwd_type_id": 1, "name": "rsuDsrcFwd"},), + ({"snmp_msgfwd_type_id": 2, "name": "rsuReceivedMsg"},), + ({"snmp_msgfwd_type_id": 3, "name": "rsuXmitMsgFwding"},), + ] + + # call + result = update_rsu_snmp_pg.get_msgfwd_types() + + # check + assert result == test_update_rsu_snm_pg_data.msgfwd_types + + +@patch("common.update_rsu_snmp_pg.pgquery.query_db") +def test_get_config_list(mock_query_db): + mock_query_db.return_value = [ + ( + { + "rsu_id": 1, + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05T00:00:00", + "end_datetime": "2034-02-05T00:00:00", + "active": "1", + }, + ), + ( + { + "rsu_id": 2, + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": 1, + "message_type": "MAP", + "dest_ipv4": "5.5.5.5", + "dest_port": 44920, + "start_datetime": "2024-02-05T00:00:00", + "end_datetime": "2034-02-05T00:00:00", + "active": "1", + }, + ), + ] + + # call + result = update_rsu_snmp_pg.get_config_list() + + # check + assert result == test_update_rsu_snm_pg_data.snmp_config_data_msgfwd_type_str + + +@patch("common.update_rsu_snmp_pg.get_config_list") +@patch("common.update_rsu_snmp_pg.get_msgfwd_types") +@patch("common.update_rsu_snmp_pg.delete_config_list") +@patch("common.update_rsu_snmp_pg.insert_config_list") +def test_update_postgresql_add( + mock_insert_config_list, + mock_delete_config_list, + mock_get_msgfwd_types, + mock_get_config_list, +): + mock_get_config_list.return_value = [] + mock_get_msgfwd_types.return_value = test_update_rsu_snm_pg_data.msgfwd_types + + # call + update_rsu_snmp_pg.update_postgresql( + test_update_rsu_snm_pg_data.sample_rsu_snmp_configs_obj_1 + ) + + # check + assert mock_delete_config_list.call_count == 0 + mock_insert_config_list.assert_called_with( + test_update_rsu_snm_pg_data.snmp_config_data + ) + + +@patch("common.update_rsu_snmp_pg.get_config_list") +@patch("common.update_rsu_snmp_pg.get_msgfwd_types") +@patch("common.update_rsu_snmp_pg.delete_config_list") +@patch("common.update_rsu_snmp_pg.insert_config_list") +def test_update_postgresql_delete( + mock_insert_config_list, + mock_delete_config_list, + mock_get_msgfwd_types, + mock_get_config_list, +): + mock_get_config_list.return_value = ( + test_update_rsu_snm_pg_data.snmp_config_data_msgfwd_type_str + ) + mock_get_msgfwd_types.return_value = test_update_rsu_snm_pg_data.msgfwd_types + + # call + update_rsu_snmp_pg.update_postgresql( + test_update_rsu_snm_pg_data.sample_rsu_snmp_configs_obj_2 + ) + + # check + mock_delete_config_list.assert_called_with( + [ + { + "rsu_id": 1, + "msgfwd_type": 2, + "snmp_index": 1, + "message_type": "BSM", + "dest_ipv4": "5.5.5.5", + "dest_port": 46800, + "start_datetime": "2024-02-05 00:00", + "end_datetime": "2034-02-05 00:00", + "active": "1", + } + ] + ) + assert mock_insert_config_list.call_count == 0 + + +@patch("common.update_rsu_snmp_pg.get_config_list") +@patch("common.update_rsu_snmp_pg.get_msgfwd_types") +@patch("common.update_rsu_snmp_pg.delete_config_list") +@patch("common.update_rsu_snmp_pg.insert_config_list") +def test_update_postgresql_nothing( + mock_insert_config_list, + mock_delete_config_list, + mock_get_msgfwd_types, + mock_get_config_list, +): + mock_get_config_list.return_value = ( + test_update_rsu_snm_pg_data.snmp_config_data_msgfwd_type_str_2 + ) + mock_get_msgfwd_types.return_value = test_update_rsu_snm_pg_data.msgfwd_types + + # call + update_rsu_snmp_pg.update_postgresql( + test_update_rsu_snm_pg_data.sample_rsu_snmp_configs_obj_3 + ) + + # check + assert mock_delete_config_list.call_count == 0 + assert mock_insert_config_list.call_count == 0 + + +@patch("common.update_rsu_snmp_pg.rsufwdsnmpwalk.get") +def test_get_snmp_configs(mock_rsufwdsnmpwalk_get): + mock_rsufwdsnmpwalk_get.side_effect = ( + test_update_rsu_snm_pg_data.side_effect_return_values + ) + + # call + result = update_rsu_snmp_pg.get_snmp_configs(test_update_rsu_snm_pg_data.rsu_list) + + # verify + assert result == test_update_rsu_snm_pg_data.get_snmp_configs_expected diff --git a/services/api/tests/src/test_util.py b/services/common/tests/test_util.py similarity index 98% rename from services/api/tests/src/test_util.py rename to services/common/tests/test_util.py index 719dcf16..2f1bf952 100644 --- a/services/api/tests/src/test_util.py +++ b/services/common/tests/test_util.py @@ -1,7 +1,7 @@ import datetime import pytz -from api.src import util +from common import util def test_format_date_utc(): diff --git a/services/common/update_rsu_snmp_pg.py b/services/common/update_rsu_snmp_pg.py new file mode 100644 index 00000000..c71e989b --- /dev/null +++ b/services/common/update_rsu_snmp_pg.py @@ -0,0 +1,222 @@ +import os +import logging +import common.pgquery as pgquery +import common.rsufwdsnmpwalk as rsufwdsnmpwalk +from datetime import datetime + + +def insert_config_list(snmp_config_list): + query = ( + "INSERT INTO public.snmp_msgfwd_config(" + "rsu_id, msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active) " + "VALUES" + ) + + for snmp_config in snmp_config_list: + query += ( + f" ({snmp_config['rsu_id']}, {snmp_config['msgfwd_type']}, {snmp_config['snmp_index']}, " + f"'{snmp_config['message_type']}', '{snmp_config['dest_ipv4']}', {snmp_config['dest_port']}, " + f"'{snmp_config['start_datetime']}', '{snmp_config['end_datetime']}', '{snmp_config['active']}')," + ) + + pgquery.write_db(query[:-1]) + + +def delete_config_list(snmp_config_list): + for snmp_config in snmp_config_list: + query = ( + "DELETE FROM public.snmp_msgfwd_config " + f"WHERE rsu_id={snmp_config['rsu_id']} AND msgfwd_type={snmp_config['msgfwd_type']} AND snmp_index={snmp_config['snmp_index']}" + ) + + pgquery.write_db(query) + + +def get_msgfwd_types(): + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT snmp_msgfwd_type_id, name " + "FROM public.snmp_msgfwd_type" + ") as row" + ) + + # Query PostgreSQL for the list of SNMP message forwarding types + data = pgquery.query_db(query) + + msgfwd_types = {} + for row in data: + row = dict(row[0]) + msgfwd_types[row["name"]] = row["snmp_msgfwd_type_id"] + + return msgfwd_types + + +def get_config_list(rsu_obj={}): + query = ( + "SELECT to_jsonb(row) " + "FROM (" + "SELECT rsu_id, smt.name msgfwd_type, snmp_index, message_type, dest_ipv4, dest_port, start_datetime, end_datetime, active " + "FROM public.snmp_msgfwd_config smc " + "JOIN public.snmp_msgfwd_type smt ON smc.msgfwd_type = smt.snmp_msgfwd_type_id" + ) + + # If an rsu_obj was provided, only return the RSU information for the subset + if len(rsu_obj) > 0: + query += " WHERE " + for rsu_id in rsu_obj: + query += f"rsu_id = {rsu_id} OR " + # Trim off the last " OR " which is 4 characters long + query = query[:-4] + + query += ") as row" + + # Query PostgreSQL for the list of SNMP message forwarding configurations tracked in PostgreSQL + data = pgquery.query_db(query) + + config_list = [] + for row in data: + row = dict(row[0]) + row["start_datetime"] = datetime.strptime( + row["start_datetime"], "%Y-%m-%dT%H:%M:%S" + ).strftime("%Y-%m-%d %H:%M") + row["end_datetime"] = datetime.strptime( + row["end_datetime"], "%Y-%m-%dT%H:%M:%S" + ).strftime("%Y-%m-%d %H:%M") + config_list.append(row) + + return config_list + + +def update_postgresql(rsu_snmp_configs_obj, subset=False): + # Pull all recorded message forwarding configurations from PostgreSQL + # If the rsu_snmp_configs_obj is only a subset of all of the RSUs in PostgreSQL, only get the relevant configs + if subset: + recorded_config_list = get_config_list(rsu_snmp_configs_obj) + else: + recorded_config_list = get_config_list() + msgfwd_types = get_msgfwd_types() + + # Perform a diff on the active and recorded configurations + # Altered configurations will be removed and then added for simplicity + configs_to_remove = [] + configs_to_add = [] + + # Determine configurations to be deleted + for recorded_config in recorded_config_list: + if recorded_config["rsu_id"] not in rsu_snmp_configs_obj: + logging.warn(f"Unknown RSU with id of: {recorded_config['rsu_id']}") + # Swap msgfwd_type string with PostgreSQL id + recorded_config["msgfwd_type"] = msgfwd_types[ + recorded_config["msgfwd_type"] + ] + configs_to_remove.append(recorded_config) + continue + + # Maintain configuration data on offline RSUs + if ( + rsu_snmp_configs_obj[recorded_config["rsu_id"]] + == "Unable to retrieve latest SNMP config" + ): + continue + + if recorded_config not in rsu_snmp_configs_obj[recorded_config["rsu_id"]]: + logging.debug(f"Configuration is no longer active: {recorded_config}") + # Swap msgfwd_type string with PostgreSQL id + recorded_config["msgfwd_type"] = msgfwd_types[ + recorded_config["msgfwd_type"] + ] + configs_to_remove.append(recorded_config) + + # Determine configurations to be added + for snmp_configs in rsu_snmp_configs_obj.values(): + if snmp_configs == "Unable to retrieve latest SNMP config": + continue + + for snmp_config in snmp_configs: + if snmp_config not in recorded_config_list: + logging.debug(f"Configuration is new: {snmp_config}") + # Swap msgfwd_type string with PostgreSQL id + snmp_config["msgfwd_type"] = msgfwd_types[snmp_config["msgfwd_type"]] + configs_to_add.append(snmp_config) + + # Make deletions + if len(configs_to_remove) > 0: + delete_config_list(configs_to_remove) + + # Make additions + if len(configs_to_add) > 0: + insert_config_list(configs_to_add) + + +def get_snmp_configs(rsu_list): + config_obj = {} + + for rsu in rsu_list: + request = { + "rsu_ip": rsu["ipv4_address"], + "snmp_version": rsu["snmp_version"], + "snmp_creds": { + "username": rsu["snmp_username"], + "password": rsu["snmp_password"], + }, + } + response, code = rsufwdsnmpwalk.get(request) + + if code != 200: + config_obj[rsu["rsu_id"]] = "Unable to retrieve latest SNMP config" + continue + + config_list = [] + if rsu["snmp_version"] == "41": + # Handle the rsuDsrcFwd configurations + for key, value in response["RsuFwdSnmpwalk"].items(): + config = { + "rsu_id": rsu["rsu_id"], + "msgfwd_type": "rsuDsrcFwd", + "snmp_index": int(key), + "message_type": value["Message Type"], + "dest_ipv4": value["IP"], + "dest_port": value["Port"], + "start_datetime": value["Start DateTime"], + "end_datetime": value["End DateTime"], + "active": "1" if value["Config Active"] == "Enabled" else "0", + } + config_list.append(config) + elif rsu["snmp_version"] == "1218": + # Handle the rsuReceivedMsgTable configurations + for key, value in response["RsuFwdSnmpwalk"]["rsuReceivedMsgTable"].items(): + config = { + "rsu_id": rsu["rsu_id"], + "msgfwd_type": "rsuReceivedMsg", + "snmp_index": int(key), + "message_type": value["Message Type"], + "dest_ipv4": value["IP"], + "dest_port": value["Port"], + "start_datetime": value["Start DateTime"], + "end_datetime": value["End DateTime"], + "active": "1" if value["Config Active"] == "Enabled" else "0", + } + config_list.append(config) + + # Handle the rsuXmitMsgFwdingTable configurations + for key, value in response["RsuFwdSnmpwalk"][ + "rsuXmitMsgFwdingTable" + ].items(): + config = { + "rsu_id": rsu["rsu_id"], + "msgfwd_type": "rsuXmitMsgFwding", + "snmp_index": int(key), + "message_type": value["Message Type"], + "dest_ipv4": value["IP"], + "dest_port": value["Port"], + "start_datetime": value["Start DateTime"], + "end_datetime": value["End DateTime"], + "active": "1" if value["Config Active"] == "Enabled" else "0", + } + logging.info(config) + config_list.append(config) + + config_obj[rsu["rsu_id"]] = config_list + + return config_obj diff --git a/services/api/src/util.py b/services/common/util.py similarity index 100% rename from services/api/src/util.py rename to services/common/util.py diff --git a/services/pytest.ini b/services/pytest.ini index a80ea272..0a2f782e 100644 --- a/services/pytest.ini +++ b/services/pytest.ini @@ -1,10 +1,10 @@ [pytest] pythonpath = . - addons/images/bsm_query + addons/images/geo_msg_query addons/images/count_metric addons/images/firmware_manager addons/images/iss_health_check - addons/images/rsu_ping_fetch + addons/images/rsu_status_check api/src common env = diff --git a/services/requirements.txt b/services/requirements.txt index cfb71caf..46501ebf 100644 --- a/services/requirements.txt +++ b/services/requirements.txt @@ -23,7 +23,7 @@ gunicorn==21.2.0 pytz==2023.3.post1 Werkzeug==3.0.0 uuid==1.30 -multidict==6.0.4 +multidict==6.0.5 python-keycloak==2.16.2 fabric==3.2.2 paramiko==3.3.1 diff --git a/services/api/resources/mibs/IPV6-TC.txt b/services/resources/mibs/IPV6-TC.txt similarity index 100% rename from services/api/resources/mibs/IPV6-TC.txt rename to services/resources/mibs/IPV6-TC.txt diff --git a/services/api/resources/mibs/NTCIP1218-v01.txt b/services/resources/mibs/NTCIP1218-v01.txt similarity index 100% rename from services/api/resources/mibs/NTCIP1218-v01.txt rename to services/resources/mibs/NTCIP1218-v01.txt diff --git a/services/api/resources/mibs/RSU-MIB.txt b/services/resources/mibs/RSU-MIB.txt similarity index 100% rename from services/api/resources/mibs/RSU-MIB.txt rename to services/resources/mibs/RSU-MIB.txt diff --git a/services/api/resources/mibs/SNMPv2-SMI.txt b/services/resources/mibs/SNMPv2-SMI.txt similarity index 100% rename from services/api/resources/mibs/SNMPv2-SMI.txt rename to services/resources/mibs/SNMPv2-SMI.txt diff --git a/services/api/resources/mibs/SNMPv2-TC.txt b/services/resources/mibs/SNMPv2-TC.txt similarity index 100% rename from services/api/resources/mibs/SNMPv2-TC.txt rename to services/resources/mibs/SNMPv2-TC.txt diff --git a/services/api/resources/mibs/SYSLOG-TC-MIB.txt b/services/resources/mibs/SYSLOG-TC-MIB.txt similarity index 100% rename from services/api/resources/mibs/SYSLOG-TC-MIB.txt rename to services/resources/mibs/SYSLOG-TC-MIB.txt diff --git a/services/api/resources/mibs/URI-TC-MIB.txt b/services/resources/mibs/URI-TC-MIB.txt similarity index 100% rename from services/api/resources/mibs/URI-TC-MIB.txt rename to services/resources/mibs/URI-TC-MIB.txt diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 2449348b..0f89e0da 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -18,6 +18,9 @@ ENV REACT_APP_KEYCLOAK_URL $KEYCLOAK_HOST_URL ARG COUNT_MESSAGE_TYPES ENV REACT_APP_COUNT_MESSAGE_TYPES $COUNT_MESSAGE_TYPES +ARG VIEWER_MESSAGE_TYPES +ENV REACT_APP_VIEWER_MESSAGE_TYPES $VIEWER_MESSAGE_TYPES + ARG DOT_NAME ENV REACT_APP_DOT_NAME $DOT_NAME @@ -36,6 +39,15 @@ RUN npm run build # Serve On NGINX FROM nginx:1.19.0 WORKDIR /usr/share/nginx/html + +# Remove the default Nginx configuration file +RUN rm /etc/nginx/conf.d/default.conf +# Copy our configuration file into the image +COPY default.conf /etc/nginx/conf.d/ + RUN rm -rf ./* +COPY public /usr/share/nginx/html +# Copy public directory to root of nginx proxy to serve the static files + COPY --from=builder /app/build . ENTRYPOINT ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/webapp/README.md b/webapp/README.md index c56b13c3..5e022334 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -90,7 +90,7 @@ Re-factoring RSU manager to utilize Redux Toolkit for state management - RsuRebootMenu.js - read selected RSU IP - SnmpsetMenu.js - read selected RSU IP/manufacturer - SnmpwalkMenu.js - read selected RSU IP/manufacturer - - BsmMap.js - Read and update BSM data + - MsgMap.js - Read and update BSM data - Configure.js - Read selected RSU IP/manufacturer - HeatMap.js - Read RSU counts/status - Map.js - Read map data, update display data diff --git a/webapp/azure-pipelines.yml b/webapp/azure-pipelines.yml index e2717556..efa5abac 100644 --- a/webapp/azure-pipelines.yml +++ b/webapp/azure-pipelines.yml @@ -3,7 +3,7 @@ trigger: branches: include: - - main + - develop paths: include: - 'webapp/*' @@ -12,72 +12,15 @@ pool: vmImage: ubuntu-latest steps: - # npm install - - task: Npm@1 - inputs: - command: 'install' - workingDir: 'webapp' - - # set mapbox token from pipeline variable - - task: Bash@3 - inputs: - targetType: 'inline' - workingDirectory: webapp/ - script: | - echo token is $REACT_APP_MAPBOX_TOKEN - - # npm run build dev - - task: Npm@1 - inputs: - workingDir: 'webapp' - command: 'custom' - customCommand: 'run build:all' - - # Copy dev build to staging directory - - task: CopyFiles@2 - inputs: - SourceFolder: 'webapp/build-dev' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-dev/build' - - # Copy test build to staging directory - - task: CopyFiles@2 - inputs: - SourceFolder: 'webapp/build-test' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-test/build' - - # Copy prod build to staging directory - - task: CopyFiles@2 - inputs: - SourceFolder: 'webapp/build-prod' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-prod/build' - - # Copy required yaml definitions to dev staging directory - task: CopyFiles@2 inputs: SourceFolder: 'webapp' - Contents: 'app.yaml' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-dev' - - # Copy required yaml definitions to test staging directory - - task: CopyFiles@2 - inputs: - SourceFolder: 'webapp' - Contents: 'app.yaml' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-test' - - # Copy required yaml definitions to prod staging directory - - task: CopyFiles@2 - inputs: - SourceFolder: 'webapp' - Contents: 'app.yaml' - TargetFolder: '$(Build.ArtifactStagingDirectory)/build-prod' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)' # Publish the artifacts directory for consumption in publish pipeline - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'web_app' + ArtifactName: 'jpo-cvmanager-webapp' publishLocation: 'Container' diff --git a/webapp/default.conf b/webapp/default.conf new file mode 100644 index 00000000..7acbf370 --- /dev/null +++ b/webapp/default.conf @@ -0,0 +1,8 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; # The directory where your page is located + try_files $uri /index.html; # Always fall back to index.html + } +} \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index fe7ceb29..150013f5 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,12 +1,12 @@ { "name": "cv-manager", - "version": "0.1.0", + "version": "1.2.0-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cv-manager", - "version": "0.1.0", + "version": "1.2.0-SNAPSHOT", "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", @@ -51,7 +51,7 @@ "react-table": "^7.8.0", "react-tabs": "^4.2.1", "react-widgets": "5.8.4", - "reactstrap": "^9.1.9", + "reactstrap": "^9.2.2", "styled-components": "^5.3.6", "worker-loader": "^3.0.8", "zustand": "4.3.1" @@ -22747,9 +22747,9 @@ } }, "node_modules/reactstrap": { - "version": "9.1.9", - "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.1.9.tgz", - "integrity": "sha512-kcXHdYLmPK7rXzLotum7RI9uwvDZJ01VtjchAwzfKL8SHFZEvi7+JVsnBojf1ZIswRaTX/s8poAgZFgE8oF0zQ==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.2.tgz", + "integrity": "sha512-4KroiGOdqZLAnMGzHjpErW3G7bLB+QbKzzMLIDXydPIV0y74lpdL7WtXHkLWAGInd97WCPNx4+R0NQDPyzIfhw==", "dependencies": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", diff --git a/webapp/package.json b/webapp/package.json index f21076cb..e7b6c8ba 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -46,7 +46,7 @@ "react-table": "^7.8.0", "react-tabs": "^4.2.1", "react-widgets": "5.8.4", - "reactstrap": "^9.1.9", + "reactstrap": "^9.2.2", "styled-components": "^5.3.6", "worker-loader": "^3.0.8", "zustand": "4.3.1" @@ -84,7 +84,6 @@ "devDependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", - "jest-canvas-mock": "^2.5.2", "@types/jest": "^29.5.8", "@types/node": "^20.6.2", "@types/react": "^17.0.0", @@ -93,6 +92,7 @@ "babel-plugin-transform-export-extensions": "^6.22.0", "eslint": "8.23.1", "jest": "^26.6.0", + "jest-canvas-mock": "^2.5.2", "react-test-renderer": "^17.0.2", "typescript": "^4.9.4" }, diff --git a/webapp/sample.env.local b/webapp/sample.env.local index eb05322e..e12dcdab 100644 --- a/webapp/sample.env.local +++ b/webapp/sample.env.local @@ -13,7 +13,8 @@ REACT_APP_GATEWAY_BASE_URL="http://cvmanager.local.com:8081" # base url or IP for keycloak REACT_APP_KEYCLOAK_URL="http://cvmanager.auth.com:8084/" -COUNT_MESSAGE_TYPES='BSM,SSM,SPAT,SRM,MAP' +REACT_APP_COUNT_MESSAGE_TYPES='BSM,SSM,SPAT,SRM,MAP,PSM' +REACT_APP_VIEWER_MESSAGE_TYPES='BSM,PSM' DOT_NAME="CDOT" # initial mapbox view diff --git a/webapp/src/App.css b/webapp/src/App.css index cc9b5c36..15fe185e 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -48,15 +48,18 @@ header { margin-bottom: -1px; padding: 0.5rem 0.75rem; cursor: pointer; + color: white; + text-decoration: none; } .tab-list-active { font-family: Arial, Helvetica, sans-serif; font-weight: 550; - background-color: #d16d15; + background-color: #b55e12; color: white; - border: solid #d16d15; + border: solid #b55e12; border-width: 1px 1px 0 1px; + border-top: 0.5px solid #ffffff; } #content-grid { @@ -74,3 +77,7 @@ td { border-collapse: collapse; min-width: 200px; } + +.form-label { + color: white; +} diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 595020c7..290ac2b0 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1,40 +1,26 @@ import React, { useEffect } from 'react' import { css } from '@emotion/react' -import RingLoader from 'react-spinners/RingLoader' -import Header from './components/Header' -import Menu from './features/menu/Menu' -import Help from './components/Help' -import Admin from './pages/Admin' -import Grid from '@material-ui/core/Grid' -import Tabs from './components/Tabs' -import Map from './pages/Map' -import RsuMapView from './pages/RsuMapView' import './App.css' import { useSelector, useDispatch } from 'react-redux' import { - selectDisplayMap, - // Actions getRsuData, - getRsuInfoOnly, } from './generalSlices/rsuSlice' -import { selectAuthLoginData, selectRole, selectLoadingGlobal } from './generalSlices/userSlice' -import { SecureStorageManager } from './managers' -import { ReactKeycloakProvider } from '@react-keycloak/web' +import { selectAuthLoginData, selectRouteNotFound } from './generalSlices/userSlice' import keycloak from './keycloak-config' -import { keycloakLogin } from './generalSlices/userSlice' import { ThunkDispatch } from 'redux-thunk' import { RootState } from './store' import { AnyAction } from '@reduxjs/toolkit' - -let loginDispatched = false +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import Dashboard from './Dashboard' +import { NotFound } from './pages/404' +import { theme } from './styles' +import { ThemeProvider } from '@mui/material' const App = () => { const dispatch: ThunkDispatch = useDispatch() - const displayMap = useSelector(selectDisplayMap) const authLoginData = useSelector(selectAuthLoginData) - const userRole = useSelector(selectRole) - const loadingGlobal = useSelector(selectLoadingGlobal) + const routeNotFound = useSelector(selectRouteNotFound) useEffect(() => { keycloak @@ -58,57 +44,20 @@ const App = () => { }, [authLoginData, dispatch]) return ( - { - // Logic to prevent multiple login triggers - if (!loginDispatched && token) { - console.debug('onTokens loginDispatched:') - dispatch(keycloakLogin(token)) - loginDispatched = true - } - setTimeout(() => (loginDispatched = false), 5000) - }} - > -
- -
- {authLoginData && keycloak?.authenticated ? ( - -
- {displayMap ? null : } - {displayMap ? : } -
- {SecureStorageManager.getUserRole() === 'admin' && ( -
-
- -
-
- )} -
- -
-
- ) : ( -
- )} - - -
-
+ + + {routeNotFound ? ( + + ) : ( + + } /> + } /> + } /> + + )} + + ) } -const loadercss = css` - display: block; - margin: 0 auto; - position: absolute; - top: 50%; - left: 50%; - margin-top: -125px; - margin-left: -125px; -` - export default App diff --git a/webapp/src/Dashboard.tsx b/webapp/src/Dashboard.tsx new file mode 100644 index 00000000..a472760d --- /dev/null +++ b/webapp/src/Dashboard.tsx @@ -0,0 +1,123 @@ +import React, { useEffect } from 'react' +import { css } from '@emotion/react' +import RingLoader from 'react-spinners/RingLoader' +import Header from './components/Header' +import Menu from './features/menu/Menu' +import Help from './components/Help' +import Admin from './pages/Admin' +import Grid from '@material-ui/core/Grid' +import Tabs, { TabItem } from './components/Tabs' +import Map from './pages/Map' +import './App.css' +import { useSelector, useDispatch } from 'react-redux' +import { + // Actions + getRsuData, +} from './generalSlices/rsuSlice' +import { selectAuthLoginData, selectLoadingGlobal } from './generalSlices/userSlice' +import { SecureStorageManager } from './managers' +import { ReactKeycloakProvider } from '@react-keycloak/web' +import keycloak from './keycloak-config' +import { keycloakLogin } from './generalSlices/userSlice' +import { ThunkDispatch } from 'redux-thunk' +import { RootState } from './store' +import { AnyAction } from '@reduxjs/toolkit' +import { Routes, Route, Navigate } from 'react-router-dom' +import { NotFound } from './pages/404' + +let loginDispatched = false + +const Dashboard = () => { + const dispatch: ThunkDispatch = useDispatch() + const authLoginData = useSelector(selectAuthLoginData) + const loadingGlobal = useSelector(selectLoadingGlobal) + + useEffect(() => { + keycloak + .updateToken(300) + .then(function (refreshed: boolean) { + if (refreshed) { + console.debug('Token was successfully refreshed') + } else { + console.debug('Token is still valid') + } + }) + .catch(function () { + console.error('Failed to refresh the token, or the session has expired') + }) + }, []) + + useEffect(() => { + // Refresh Data + console.debug('Authorizing the user with the API') + dispatch(getRsuData()) + }, [authLoginData, dispatch]) + + console.log('Auth Role', SecureStorageManager.getUserRole()) + + return ( + { + // Logic to prevent multiple login triggers + if (!loginDispatched && token) { + console.debug('onTokens loginDispatched:') + dispatch(keycloakLogin(token)) + loginDispatched = true + } + setTimeout(() => (loginDispatched = false), 5000) + }} + > +
+ +
+ {authLoginData && keycloak?.authenticated ? ( + <> + + + {SecureStorageManager.getUserRole() !== 'admin' ? <> : } + + +
+
+ + } /> + + + + + } + /> + {/* } /> */} + } /> + } /> + } /> + +
+
+ + ) : ( +
+ )} + + +
+
+ ) +} + +const loadercss = css` + display: block; + margin: 0 auto; + position: absolute; + top: 50%; + left: 50%; + margin-top: -125px; + margin-left: -125px; +` + +export default Dashboard diff --git a/webapp/src/EnvironmentVars.tsx b/webapp/src/EnvironmentVars.tsx index 5b9d7f5f..f4eb6346 100644 --- a/webapp/src/EnvironmentVars.tsx +++ b/webapp/src/EnvironmentVars.tsx @@ -1,53 +1,63 @@ -class EnvironmentVars { - static getBaseApiUrl() { - return process.env.REACT_APP_GATEWAY_BASE_URL - } - - static getMessageTypes() { - const COUNT_MESSAGE_TYPES = process.env.REACT_APP_COUNT_MESSAGE_TYPES - if (!COUNT_MESSAGE_TYPES) { - return [] - } - const messageTypes = COUNT_MESSAGE_TYPES.split(',').map((item) => item.trim()) - return messageTypes - } - - static getMapboxInitViewState() { - const MAPBOX_INIT_LATITUDE = Number(process.env.REACT_APP_MAPBOX_INIT_LATITUDE) - const MAPBOX_INIT_LONGITUDE = Number(process.env.REACT_APP_MAPBOX_INIT_LONGITUDE) - const MAPBOX_INIT_ZOOM = Number(process.env.REACT_APP_MAPBOX_INIT_ZOOM) - - const viewState = { - latitude: MAPBOX_INIT_LATITUDE, - longitude: MAPBOX_INIT_LONGITUDE, - zoom: MAPBOX_INIT_ZOOM, - } - - return viewState - } - - static MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_TOKEN - static KEYCLOAK_HOST_URL = process.env.REACT_APP_KEYCLOAK_URL - static DOT_NAME = process.env.REACT_APP_DOT_NAME - - static rsuInfoEndpoint = `${this.getBaseApiUrl()}/rsuinfo` - static rsuOnlineEndpoint = `${this.getBaseApiUrl()}/rsu-online-status` - static rsuCountsEndpoint = `${this.getBaseApiUrl()}/rsucounts` - static rsuCommandEndpoint = `${this.getBaseApiUrl()}/rsu-command` - static wzdxEndpoint = `${this.getBaseApiUrl()}/wzdx-feed` - static rsuMapInfoEndpoint = `${this.getBaseApiUrl()}/rsu-map-info` - static rsuGeoQueryEndpoint = `${this.getBaseApiUrl()}/rsu-geo-query` - static bsmDataEndpoint = `${this.getBaseApiUrl()}/rsu-bsm-data` - static issScmsStatusEndpoint = `${this.getBaseApiUrl()}/iss-scms-status` - static ssmSrmEndpoint = `${this.getBaseApiUrl()}/rsu-ssm-srm-data` - static authEndpoint = `${this.getBaseApiUrl()}/user-auth` - static adminAddRsu = `${this.getBaseApiUrl()}/admin-new-rsu` - static adminRsu = `${this.getBaseApiUrl()}/admin-rsu` - static adminAddUser = `${this.getBaseApiUrl()}/admin-new-user` - static adminUser = `${this.getBaseApiUrl()}/admin-user` - static adminAddOrg = `${this.getBaseApiUrl()}/admin-new-org` - static adminOrg = `${this.getBaseApiUrl()}/admin-org` - static contactSupport = `${this.getBaseApiUrl()}/contact-support` -} - -export default EnvironmentVars +class EnvironmentVars { + static getBaseApiUrl() { + return process.env.REACT_APP_GATEWAY_BASE_URL + } + + static getMessageTypes() { + const COUNT_MESSAGE_TYPES = process.env.REACT_APP_COUNT_MESSAGE_TYPES + if (!COUNT_MESSAGE_TYPES) { + return [] + } + const messageTypes = COUNT_MESSAGE_TYPES.split(',').map((item) => item.trim()) + return messageTypes + } + + static getMessageViewerTypes() { + const VIEWER_MESSAGE_TYPES = process.env.REACT_APP_VIEWER_MESSAGE_TYPES + if (!VIEWER_MESSAGE_TYPES) { + return ['BSM'] // default to BSM if not set + } + const messageTypes = VIEWER_MESSAGE_TYPES.split(',').map((item) => item.trim()) + return messageTypes + } + + static getMapboxInitViewState() { + const MAPBOX_INIT_LATITUDE = Number(process.env.REACT_APP_MAPBOX_INIT_LATITUDE) + const MAPBOX_INIT_LONGITUDE = Number(process.env.REACT_APP_MAPBOX_INIT_LONGITUDE) + const MAPBOX_INIT_ZOOM = Number(process.env.REACT_APP_MAPBOX_INIT_ZOOM) + + const viewState = { + latitude: MAPBOX_INIT_LATITUDE, + longitude: MAPBOX_INIT_LONGITUDE, + zoom: MAPBOX_INIT_ZOOM, + } + + return viewState + } + + static MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_TOKEN + static KEYCLOAK_HOST_URL = process.env.REACT_APP_KEYCLOAK_URL + static DOT_NAME = process.env.REACT_APP_DOT_NAME + + static rsuInfoEndpoint = `${this.getBaseApiUrl()}/rsuinfo` + static rsuOnlineEndpoint = `${this.getBaseApiUrl()}/rsu-online-status` + static rsuCountsEndpoint = `${this.getBaseApiUrl()}/rsucounts` + static rsuMsgFwdQueryEndpoint = `${this.getBaseApiUrl()}/rsu-msgfwd-query` + static rsuCommandEndpoint = `${this.getBaseApiUrl()}/rsu-command` + static wzdxEndpoint = `${this.getBaseApiUrl()}/wzdx-feed` + static rsuMapInfoEndpoint = `${this.getBaseApiUrl()}/rsu-map-info` + static rsuGeoQueryEndpoint = `${this.getBaseApiUrl()}/rsu-geo-query` + static geoMsgDataEndpoint = `${this.getBaseApiUrl()}/rsu-geo-msg-data` + static issScmsStatusEndpoint = `${this.getBaseApiUrl()}/iss-scms-status` + static ssmSrmEndpoint = `${this.getBaseApiUrl()}/rsu-ssm-srm-data` + static authEndpoint = `${this.getBaseApiUrl()}/user-auth` + static adminAddRsu = `${this.getBaseApiUrl()}/admin-new-rsu` + static adminRsu = `${this.getBaseApiUrl()}/admin-rsu` + static adminAddUser = `${this.getBaseApiUrl()}/admin-new-user` + static adminUser = `${this.getBaseApiUrl()}/admin-user` + static adminAddOrg = `${this.getBaseApiUrl()}/admin-new-org` + static adminOrg = `${this.getBaseApiUrl()}/admin-org` + static contactSupport = `${this.getBaseApiUrl()}/contact-support` +} + +export default EnvironmentVars diff --git a/webapp/src/apis/rsu-api-types.ts b/webapp/src/apis/rsu-api-types.ts index 0a4d4a42..cecf0a8e 100644 --- a/webapp/src/apis/rsu-api-types.ts +++ b/webapp/src/apis/rsu-api-types.ts @@ -73,7 +73,8 @@ export type IssScmsStatus = { } } -export type BsmDataPostBody = { +export type GeoMsgDataPostBody = { + msg_type: string start: string end: string geometry: number[][] @@ -111,3 +112,25 @@ export type SnmpFwdWalkConfig = { Forwarding: string 'Config Active': string } + +export type RsuMsgFwdConfigSingle = { + 'Message Type': string + IP: string + Port: number + 'Start DateTime': string + 'End DateTime': string + 'Config Active': string +} + +export type RsuDsrcFwdConfigs = { + [index: number]: RsuMsgFwdConfigSingle +} + +export type RsuRxTxMsgFwdConfigs = { + rsuReceivedMsgTable: RsuDsrcFwdConfigs + rsuXmitMsgFwdingTable: RsuDsrcFwdConfigs +} + +export type RsuMsgFwdConfigs = { + RsuFwdSnmpwalk: RsuDsrcFwdConfigs | RsuRxTxMsgFwdConfigs +} diff --git a/webapp/src/apis/rsu-api.test.ts b/webapp/src/apis/rsu-api.test.ts index fdf89158..4b3609ad 100644 --- a/webapp/src/apis/rsu-api.test.ts +++ b/webapp/src/apis/rsu-api.test.ts @@ -1,358 +1,358 @@ -import RsuApi from './rsu-api' -import EnvironmentVars from '../EnvironmentVars' -import ApiHelper from './api-helper' - -beforeEach(() => { - fetchMock.mockClear() - fetchMock.doMock() - EnvironmentVars.rsuInfoEndpoint = 'REACT_APP_ENV/rsuinfo' - EnvironmentVars.rsuOnlineEndpoint = 'REACT_APP_ENV/rsu-online-status' - EnvironmentVars.rsuCountsEndpoint = 'REACT_APP_ENV/rsucounts' - EnvironmentVars.rsuCommandEndpoint = 'REACT_APP_ENV/rsu-command' - EnvironmentVars.wzdxEndpoint = 'REACT_APP_ENV/wzdx-feed' - EnvironmentVars.rsuMapInfoEndpoint = 'REACT_APP_ENV/rsu-map-info' - EnvironmentVars.bsmDataEndpoint = 'REACT_APP_ENV/rsu-bsm-data' - EnvironmentVars.issScmsStatusEndpoint = 'REACT_APP_ENV/iss-scms-status' - EnvironmentVars.ssmSrmEndpoint = 'REACT_APP_ENV/rsu-ssm-srm-data' - EnvironmentVars.authEndpoint = 'REACT_APP_ENV/user-auth' - EnvironmentVars.adminAddRsu = 'REACT_APP_ENV/admin-new-rsu' - EnvironmentVars.adminRsu = 'REACT_APP_ENV/admin-rsu' - EnvironmentVars.adminAddUser = 'REACT_APP_ENV/admin-new-user' - EnvironmentVars.adminUser = 'REACT_APP_ENV/admin-user' - EnvironmentVars.adminAddOrg = 'REACT_APP_ENV/admin-new-org' - EnvironmentVars.adminOrg = 'REACT_APP_ENV/admin-org' -}) - -it('Test apiHelper mock', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuInfo', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuInfo With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuOnline', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuOnline('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuOnlineEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuOnline With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuOnline('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuOnlineEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuCounts', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuCounts('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCountsEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuCounts With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuCounts('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCountsEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuAuth', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuAuth('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.authEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuAuth With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuAuth('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.authEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuCommand', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuCommand('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuCommand With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuCommand('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuMapInfo', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuMapInfo('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuMapInfoEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getRsuMapInfo With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getRsuMapInfo('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuMapInfoEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getSsmSrmData', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getSsmSrmData('testToken') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.ssmSrmEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) -}) - -it('Test getSsmSrmData With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getSsmSrmData('testToken', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.ssmSrmEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) -}) - -it('Test getIssScmsStatus', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getIssScmsStatus('testToken', 'testOrg') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.issScmsStatusEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getIssScmsStatus With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getIssScmsStatus('testToken', 'testOrg', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.issScmsStatusEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) -}) - -it('Test getWzdxData', async () => { - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getWzdxData('testToken') - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.wzdxEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) -}) - -it('Test getWzdxData With Params', async () => { - // Set url_ext and query_params - const url_ext = 'url_ext' - const query_params = { query_param: 'test' } - - const expectedResponse = { data: 'Test JSON' } - fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) - const actualResponse = await RsuApi.getWzdxData('testToken', url_ext, query_params) - expect(actualResponse).toEqual(expectedResponse) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.wzdxEndpoint + url_ext + '?query_param=test') - expect(fetchMock.mock.calls[0][1].method).toBe('GET') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) -}) - -it('Test postBsmData', async () => { - const body = { - data: 'Test JSON', - } as any - fetchMock.mockResponseOnce(JSON.stringify(body)) - const actualResponse = await RsuApi.postBsmData('testToken', body) - expect(actualResponse).toEqual({ - body: body, - message: undefined, - status: 200, - }) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.bsmDataEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('POST') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ - Authorization: 'testToken', - 'Content-Type': 'application/json', - }) -}) - -it('Test postBsmData With Params', async () => { - // Set url_ext - const url_ext = 'url_ext' - const body = { - data: 'Test JSON', - } as any - - fetchMock.mockResponseOnce(JSON.stringify(body)) - const actualResponse = await RsuApi.postBsmData('testToken', body, url_ext) - expect(actualResponse).toEqual({ - body: body, - message: undefined, - status: 200, - }) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.bsmDataEndpoint + url_ext) - expect(fetchMock.mock.calls[0][1].method).toBe('POST') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ - Authorization: 'testToken', - 'Content-Type': 'application/json', - }) -}) - -it('Test postRsuData', async () => { - const body = { - data: 'Test JSON', - } as any - - fetchMock.mockResponseOnce(JSON.stringify(body)) - const actualResponse = await RsuApi.postRsuData('testToken', 'testOrg', body) - expect(actualResponse).toEqual({ - body: body, - message: undefined, - status: 200, - }) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint) - expect(fetchMock.mock.calls[0][1].method).toBe('POST') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ - Authorization: 'testToken', - 'Content-Type': 'application/json', - Organization: 'testOrg', - }) -}) - -it('Test postRsuData With Params', async () => { - // Set url_ext - const url_ext = 'url_ext' - const body = { - data: 'Test JSON', - } as any - - fetchMock.mockResponseOnce(JSON.stringify(body)) - const actualResponse = await RsuApi.postRsuData('testToken', 'testOrg', body, url_ext) - expect(actualResponse).toEqual({ - body: body, - message: undefined, - status: 200, - }) - - expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint + url_ext) - expect(fetchMock.mock.calls[0][1].method).toBe('POST') - expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ - Authorization: 'testToken', - 'Content-Type': 'application/json', - Organization: 'testOrg', - }) -}) +import RsuApi from './rsu-api' +import EnvironmentVars from '../EnvironmentVars' +import ApiHelper from './api-helper' + +beforeEach(() => { + fetchMock.mockClear() + fetchMock.doMock() + EnvironmentVars.rsuInfoEndpoint = 'REACT_APP_ENV/rsuinfo' + EnvironmentVars.rsuOnlineEndpoint = 'REACT_APP_ENV/rsu-online-status' + EnvironmentVars.rsuCountsEndpoint = 'REACT_APP_ENV/rsucounts' + EnvironmentVars.rsuCommandEndpoint = 'REACT_APP_ENV/rsu-command' + EnvironmentVars.wzdxEndpoint = 'REACT_APP_ENV/wzdx-feed' + EnvironmentVars.rsuMapInfoEndpoint = 'REACT_APP_ENV/rsu-map-info' + EnvironmentVars.geoMsgDataEndpoint = 'REACT_APP_ENV/rsu-geo-data' + EnvironmentVars.issScmsStatusEndpoint = 'REACT_APP_ENV/iss-scms-status' + EnvironmentVars.ssmSrmEndpoint = 'REACT_APP_ENV/rsu-ssm-srm-data' + EnvironmentVars.authEndpoint = 'REACT_APP_ENV/user-auth' + EnvironmentVars.adminAddRsu = 'REACT_APP_ENV/admin-new-rsu' + EnvironmentVars.adminRsu = 'REACT_APP_ENV/admin-rsu' + EnvironmentVars.adminAddUser = 'REACT_APP_ENV/admin-new-user' + EnvironmentVars.adminUser = 'REACT_APP_ENV/admin-user' + EnvironmentVars.adminAddOrg = 'REACT_APP_ENV/admin-new-org' + EnvironmentVars.adminOrg = 'REACT_APP_ENV/admin-org' +}) + +it('Test apiHelper mock', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuInfo', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuInfo With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuInfo('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuInfoEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuOnline', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuOnline('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuOnlineEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuOnline With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuOnline('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuOnlineEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuCounts', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuCounts('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCountsEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuCounts With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuCounts('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCountsEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuAuth', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuAuth('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.authEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuAuth With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuAuth('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.authEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuCommand', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuCommand('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuCommand With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuCommand('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuMapInfo', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuMapInfo('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuMapInfoEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getRsuMapInfo With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getRsuMapInfo('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuMapInfoEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getSsmSrmData', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getSsmSrmData('testToken') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.ssmSrmEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) +}) + +it('Test getSsmSrmData With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getSsmSrmData('testToken', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.ssmSrmEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) +}) + +it('Test getIssScmsStatus', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getIssScmsStatus('testToken', 'testOrg') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.issScmsStatusEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getIssScmsStatus With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getIssScmsStatus('testToken', 'testOrg', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.issScmsStatusEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken', Organization: 'testOrg' }) +}) + +it('Test getWzdxData', async () => { + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getWzdxData('testToken') + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.wzdxEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) +}) + +it('Test getWzdxData With Params', async () => { + // Set url_ext and query_params + const url_ext = 'url_ext' + const query_params = { query_param: 'test' } + + const expectedResponse = { data: 'Test JSON' } + fetchMock.mockResponseOnce(JSON.stringify(expectedResponse)) + const actualResponse = await RsuApi.getWzdxData('testToken', url_ext, query_params) + expect(actualResponse).toEqual(expectedResponse) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.wzdxEndpoint + url_ext + '?query_param=test') + expect(fetchMock.mock.calls[0][1].method).toBe('GET') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ Authorization: 'testToken' }) +}) + +it('Test postGeoMsgData', async () => { + const body = { + data: 'Test JSON', + } as any + fetchMock.mockResponseOnce(JSON.stringify(body)) + const actualResponse = await RsuApi.postGeoMsgData('testToken', body) + expect(actualResponse).toEqual({ + body: body, + message: undefined, + status: 200, + }) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.geoMsgDataEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('POST') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ + Authorization: 'testToken', + 'Content-Type': 'application/json', + }) +}) + +it('Test postGeoMsgData With Params', async () => { + // Set url_ext + const url_ext = 'url_ext' + const body = { + data: 'Test JSON', + } as any + + fetchMock.mockResponseOnce(JSON.stringify(body)) + const actualResponse = await RsuApi.postGeoMsgData('testToken', body, url_ext) + expect(actualResponse).toEqual({ + body: body, + message: undefined, + status: 200, + }) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.geoMsgDataEndpoint + url_ext) + expect(fetchMock.mock.calls[0][1].method).toBe('POST') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ + Authorization: 'testToken', + 'Content-Type': 'application/json', + }) +}) + +it('Test postRsuData', async () => { + const body = { + data: 'Test JSON', + } as any + + fetchMock.mockResponseOnce(JSON.stringify(body)) + const actualResponse = await RsuApi.postRsuData('testToken', 'testOrg', body) + expect(actualResponse).toEqual({ + body: body, + message: undefined, + status: 200, + }) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint) + expect(fetchMock.mock.calls[0][1].method).toBe('POST') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ + Authorization: 'testToken', + 'Content-Type': 'application/json', + Organization: 'testOrg', + }) +}) + +it('Test postRsuData With Params', async () => { + // Set url_ext + const url_ext = 'url_ext' + const body = { + data: 'Test JSON', + } as any + + fetchMock.mockResponseOnce(JSON.stringify(body)) + const actualResponse = await RsuApi.postRsuData('testToken', 'testOrg', body, url_ext) + expect(actualResponse).toEqual({ + body: body, + message: undefined, + status: 200, + }) + + expect(fetchMock.mock.calls[0][0]).toBe(EnvironmentVars.rsuCommandEndpoint + url_ext) + expect(fetchMock.mock.calls[0][1].method).toBe('POST') + expect(fetchMock.mock.calls[0][1].headers).toStrictEqual({ + Authorization: 'testToken', + 'Content-Type': 'application/json', + Organization: 'testOrg', + }) +}) diff --git a/webapp/src/apis/rsu-api.ts b/webapp/src/apis/rsu-api.ts index 81d7f28a..cad98272 100644 --- a/webapp/src/apis/rsu-api.ts +++ b/webapp/src/apis/rsu-api.ts @@ -3,7 +3,7 @@ import { WZDxWorkZoneFeed } from '../types/wzdx/WzdxWorkZoneFeed42' import apiHelper from './api-helper' import { ApiMsgRespWithCodes, - BsmDataPostBody, + GeoMsgDataPostBody, GetRsuCommandResp, GetRsuUserAuthResp, IssScmsStatus, @@ -12,6 +12,7 @@ import { RsuInfo, RsuMapInfo, RsuMapInfoIpList, + RsuMsgFwdConfigs, RsuOnlineStatusRespMultiple, RsuOnlineStatusRespSingle, SsmSrmData, @@ -55,6 +56,18 @@ class RsuApi { query_params, additional_headers: { Organization: org }, }) + getRsuMsgFwdConfigs = async ( + token: string, + org: string, + url_ext: string = '', + query_params: Record = {} + ): Promise => + apiHelper._getData({ + url: EnvironmentVars.rsuMsgFwdQueryEndpoint + url_ext, + token, + query_params, + additional_headers: { Organization: org }, + }) getRsuAuth = async ( token: string, org: string, @@ -123,8 +136,8 @@ class RsuApi { }) // POST - postBsmData = async (token: string, body: BsmDataPostBody, url_ext: string = ''): Promise> => - apiHelper._postData({ url: EnvironmentVars.bsmDataEndpoint + url_ext, body, token }) + postGeoMsgData = async (token: string, body: Object, url_ext: string = ''): Promise> => + apiHelper._postData({ url: EnvironmentVars.geoMsgDataEndpoint + url_ext, body, token }) // POST postRsuData = async ( diff --git a/webapp/src/components/AdminFormManager.test.tsx b/webapp/src/components/AdminFormManager.test.tsx index 4a97b1aa..0897c273 100644 --- a/webapp/src/components/AdminFormManager.test.tsx +++ b/webapp/src/components/AdminFormManager.test.tsx @@ -4,11 +4,14 @@ import AdminFormManager from './AdminFormManager' import { replaceChaoticIds } from '../utils/test-utils' import { setupStore } from '../store' import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' it('snapshot rsu', () => { const { container } = render( - + + + ) @@ -18,7 +21,9 @@ it('snapshot rsu', () => { it('snapshot user', () => { const { container } = render( - + + + ) @@ -28,7 +33,9 @@ it('snapshot user', () => { it('snapshot organization', () => { const { container } = render( - + + + ) diff --git a/webapp/src/components/AdminOrganizationDeleteMenu.tsx b/webapp/src/components/AdminOrganizationDeleteMenu.tsx index 5939751d..56222eb0 100644 --- a/webapp/src/components/AdminOrganizationDeleteMenu.tsx +++ b/webapp/src/components/AdminOrganizationDeleteMenu.tsx @@ -31,7 +31,7 @@ const AdminOrganizationDeleteMenu = (props: AdminOrganizationDeleteMenuProps) => return (
) diff --git a/webapp/src/components/AdminTable.tsx b/webapp/src/components/AdminTable.tsx index b84f1ffe..eed297ba 100644 --- a/webapp/src/components/AdminTable.tsx +++ b/webapp/src/components/AdminTable.tsx @@ -1,7 +1,7 @@ import React from 'react' import MaterialTable, { Action, Column } from '@material-table/core' import { ThemeProvider } from '@mui/material' -import { theme } from '../styles' +import { tableTheme } from '../styles' import '../features/adminRsuTab/Admin.css' @@ -16,7 +16,7 @@ interface AdminTableProps { const AdminTable = (props: AdminTableProps) => { return (
- + { Your Email { Subject { { {errors.message && {errors.message.message}} - {successMsg &&

{successMsg}

} - {errorState &&

Error: {errorMessage}

} + {successMsg && ( +

+ {successMsg} +

+ )} + {errorState && ( +

+ Error: {errorMessage} +

+ )}
- -
- ))} -

RX Forward Table

- {Object.keys(msgFwdConfig.rsuReceivedMsgTable).map((index) => ( -
- - -
- ))} -
- ) : null} - + +
+ {Object.hasOwn(msgFwdConfig, 'rsuXmitMsgFwdingTable') && + Object.hasOwn(msgFwdConfig, 'rsuReceivedMsgTable') ? ( +
+

TX Forward Table

+ {Object.keys(msgFwdConfig.rsuXmitMsgFwdingTable).map((index) => ( +
+ + +
+ ))} + +

RX Forward Table

+ {Object.keys(msgFwdConfig.rsuReceivedMsgTable).map((index) => ( +
+ + +
+ ))} +
+ ) : ( +
+ {Object.keys(msgFwdConfig).map((index) => ( +
+ + +
+ ))} +
+ )} +
+ + {errorState !== '' ? ( + ) : ( -
- {Object.keys(msgFwdConfig).map((index) => ( -
- - -
- ))} -
+
)} - {errorState !== '' ?

{errorState}

:
}
) diff --git a/webapp/src/components/Tab.test.tsx b/webapp/src/components/Tab.test.tsx index 778e30ef..f33a9f8b 100644 --- a/webapp/src/components/Tab.test.tsx +++ b/webapp/src/components/Tab.test.tsx @@ -2,9 +2,14 @@ import React from 'react' import { render } from '@testing-library/react' import Tab from './Tab' import { replaceChaoticIds } from '../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { - const { container } = render( {}} activeTab={''} label={''} />) + const { container } = render( + + {}} activeTab={''} path={''} label={''} /> + + ) expect(replaceChaoticIds(container)).toMatchSnapshot() }) diff --git a/webapp/src/components/Tab.tsx b/webapp/src/components/Tab.tsx index ca7d6bfc..c877d8aa 100644 --- a/webapp/src/components/Tab.tsx +++ b/webapp/src/components/Tab.tsx @@ -1,25 +1,39 @@ import React from 'react' import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' +import { useLocation } from 'react-router-dom' interface TabProps { activeTab: string label: string + path: string onClick: (label: string) => void } const Tab = (props: TabProps) => { - const { onClick, activeTab, label } = props + const { onClick, path, label } = props + const location = useLocation() + let className = 'tab-list-item' - if (activeTab === label) { + if (location.pathname.includes(path)) { className += ' tab-list-active' } return ( -
  • onClick(label)}> + { + if (e.code === 'Space') { + onClick(label) + } + }} + onClick={() => onClick(label)} + > {label} -
  • + ) } diff --git a/webapp/src/components/Tabs.tsx b/webapp/src/components/Tabs.tsx index 905641b5..0b2d3220 100644 --- a/webapp/src/components/Tabs.tsx +++ b/webapp/src/components/Tabs.tsx @@ -2,14 +2,24 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import Tab from './Tab' +interface TabItemProps { + label: string + path: string +} + +export const TabItem = (props: TabItemProps) => { + return
    +} + interface TabsProps { children: { props: { label: string - children: React.ReactNode + path: string } }[] } + interface TabsState { activeTab: string } @@ -43,19 +53,15 @@ class Tabs extends Component {
      {children.map((child) => { const label = child?.props?.label + const path = child?.props?.path if (label !== undefined) { - return + return } else { return null } })}
    -
    - {children.map((child) => { - if (child?.props?.label !== activeTab) return undefined - return child.props.children - })} -
    +
    ) } diff --git a/webapp/src/components/__snapshots__/AdminFormManager.test.tsx.snap b/webapp/src/components/__snapshots__/AdminFormManager.test.tsx.snap index 101ddd71..9e105ade 100644 --- a/webapp/src/components/__snapshots__/AdminFormManager.test.tsx.snap +++ b/webapp/src/components/__snapshots__/AdminFormManager.test.tsx.snap @@ -143,6 +143,7 @@ exports[`snapshot organization 1`] = ` data-testid="EditIcon" focusable="false" size="20" + style="color: white;" viewBox="0 0 24 24" >

    Rows per page:

    @@ -1437,9 +1439,9 @@ exports[`snapshot user 1`] = ` `; diff --git a/webapp/src/components/__snapshots__/SnmpsetMenu.test.tsx.snap b/webapp/src/components/__snapshots__/SnmpsetMenu.test.tsx.snap index 3dc9b371..e6044266 100644 --- a/webapp/src/components/__snapshots__/SnmpsetMenu.test.tsx.snap +++ b/webapp/src/components/__snapshots__/SnmpsetMenu.test.tsx.snap @@ -59,6 +59,11 @@ exports[`should take a snapshot 1`] = ` > SSM + diff --git a/webapp/src/components/__snapshots__/SnmpwalkItem.test.tsx.snap b/webapp/src/components/__snapshots__/SnmpwalkItem.test.tsx.snap index e104730a..7ff8d130 100644 --- a/webapp/src/components/__snapshots__/SnmpwalkItem.test.tsx.snap +++ b/webapp/src/components/__snapshots__/SnmpwalkItem.test.tsx.snap @@ -31,14 +31,6 @@ exports[`should take a snapshot 1`] = ` Port: -

    -

    - - Protocol: - -

    +
    +
    +
    diff --git a/webapp/src/components/__snapshots__/Tab.test.tsx.snap b/webapp/src/components/__snapshots__/Tab.test.tsx.snap index 739d86a6..7398cfcb 100644 --- a/webapp/src/components/__snapshots__/Tab.test.tsx.snap +++ b/webapp/src/components/__snapshots__/Tab.test.tsx.snap @@ -2,8 +2,9 @@ exports[`should take a snapshot 1`] = `
    -
  • `; diff --git a/webapp/src/components/css/ConfigureItem.css b/webapp/src/components/css/ConfigureItem.css index a2aea8f6..b1661ec3 100644 --- a/webapp/src/components/css/ConfigureItem.css +++ b/webapp/src/components/css/ConfigureItem.css @@ -42,5 +42,5 @@ } strong { - color: #d16d15; + color: #e37616; } diff --git a/webapp/src/components/css/Header.css b/webapp/src/components/css/Header.css index a6a2b05e..3fed288a 100644 --- a/webapp/src/components/css/Header.css +++ b/webapp/src/components/css/Header.css @@ -45,7 +45,7 @@ } .keycloak-button { - background-color: #d16d15; + background-color: #b55e12; color: #fff; padding: 10px 20px; border: none; @@ -88,11 +88,13 @@ #userInfoGrid { padding: 30px; + height: 100px; } #nameText { font-size: 25px; text-align: right; + margin-top: 5px; } #emailText { @@ -118,7 +120,6 @@ font-weight: bold; font-size: 16px; text-align: right; - color: #d16d15; + color: #e98125; background-color: #333; } - diff --git a/webapp/src/components/css/Help.css b/webapp/src/components/css/Help.css index 3c585bb9..3140a779 100644 --- a/webapp/src/components/css/Help.css +++ b/webapp/src/components/css/Help.css @@ -1,8 +1,8 @@ #help { - width: 100vw; - height: fit-content; - background-color: #15317e; + width: '100%'; + height: calc(100vh - 215px); padding-bottom: 5em; + overflow-y: scroll; } .helpHeader { @@ -32,4 +32,98 @@ width: 40%; margin-top: 4em; margin-bottom: 4em; + border: 0.5px solid white; +} + +#ContactSupportMenu { + position: relative; + padding: 1rem; + margin: 1rem; + border: 1px solid #ccc; + border-radius: 1rem; + background-color: #333; +} + +h5 { + font-size: 1.5rem; + font-weight: 400; + margin-bottom: 0.5rem; + text-align: center; + color: #ffffff; +} + +.label { + font-size: 1rem; + font-weight: 400; + margin-bottom: 0.5rem; +} + +.btn { + font-size: 1rem; + font-weight: 400; + margin-bottom: 0.5rem; + background-color: #d16d15; + border-color: #d16d15; + color: #ffffff; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.showbutton { + padding: 0.5rem; + cursor: pointer; +} +#contactsupportbtndiv { + margin-top: 20px; + margin-left: -100px; + font-family: Arial, Helvetica, sans-serif; + cursor: pointer; + border-radius: 3px; +} +.hidebutton { + position: absolute; + top: 0; + right: 0; + margin: 1rem; + padding: 0.5rem; + cursor: pointer; + border-radius: 50%; + border: 1px solid #ccc; +} + +textarea { + resize: vertical; + min-height: 100px; +} + +#supportMenuParent { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + position: fixed; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +#supportMenuContainer { + display: inline-block; + margin-bottom: 10px; + text-align: center; + width: fit-content; + height: fit-content; + text-align: center; +} + +.label { + color: white; + margin-top: 5px; + margin-bottom: 5px; +} + +.form-text { + color: white; } diff --git a/webapp/src/components/css/SnmpItem.css b/webapp/src/components/css/SnmpItem.css index ccb3f577..81c96dd4 100644 --- a/webapp/src/components/css/SnmpItem.css +++ b/webapp/src/components/css/SnmpItem.css @@ -33,5 +33,5 @@ } strong { - color: #d16d15; + color: #e37616; } diff --git a/webapp/src/components/css/SnmpwalkMenu.css b/webapp/src/components/css/SnmpwalkMenu.css index 43415133..3bfff159 100644 --- a/webapp/src/components/css/SnmpwalkMenu.css +++ b/webapp/src/components/css/SnmpwalkMenu.css @@ -8,6 +8,7 @@ padding-bottom: 30px; padding-left: 30px; vertical-align: top; + border: 1px solid white; } #updatediv { @@ -57,18 +58,26 @@ margin-right: 4em; } -#snmptxrxheader { +#snmptxheader { font-family: Arial, Helvetica, sans-serif; font-size: large; color: white; margin-top: 10px; - margin-bottom: 5px; + margin-bottom: -15px; +} + +#snmprxheader { + font-family: Arial, Helvetica, sans-serif; + font-size: large; + color: white; + margin-top: 40px; + margin-bottom: -15px; } #refreshbtn { font-family: Arial, Helvetica, sans-serif; font-weight: 550; - background-color: #d16d15; + background-color: #b55e12; border: none; color: white; padding: 8px 10px; @@ -107,7 +116,7 @@ border-radius: 3px; margin-bottom: 10px; } -.deletbutton { +.deletebutton { margin-left: 180px !important; margin-bottom: -360px !important; } @@ -153,10 +162,12 @@ } #warningtext { + background-color: #fff; font-family: Arial, Helvetica, sans-serif; - color: red; + color: #d10000; margin-top: 10px; font-weight: 550; + padding: 4px; } #msgfwddiv { @@ -183,5 +194,5 @@ #firmwarenoticetext { font-family: Arial, Helvetica, sans-serif; - color: #d16d15; + color: #e98125; } diff --git a/webapp/src/features/adminAddOrganization/AdminAddOrganization.tsx b/webapp/src/features/adminAddOrganization/AdminAddOrganization.tsx index 4ab6ace3..916bf95e 100644 --- a/webapp/src/features/adminAddOrganization/AdminAddOrganization.tsx +++ b/webapp/src/features/adminAddOrganization/AdminAddOrganization.tsx @@ -48,11 +48,23 @@ const AdminAddOrganization = () => { required: 'Please enter the organization name', })} /> - {errors.name &&

    {errors.name.message}

    } + {errors.name && ( +

    + {errors.name.message} +

    + )} - {successMsg &&

    {successMsg}

    } - {errorState &&

    Failed to add organization due to error: {errorMsg}

    } + {successMsg && ( +

    + {successMsg} +

    + )} + {errorState && ( +

    + Failed to add organization due to error: {errorMsg} +

    + )}
    @@ -53,7 +53,7 @@ exports[`should take a snapshot 1`] = ` class="form-control" id="longitude" name="longitude" - placeholder="Enter RSU Longitude" + placeholder="Enter RSU Longitude (Required)" type="text" />
    @@ -70,7 +70,7 @@ exports[`should take a snapshot 1`] = ` class="form-control" id="milepost" name="milepost" - placeholder="Enter RSU Milepost" + placeholder="Enter RSU Milepost (Required)" type="text" /> @@ -109,7 +109,7 @@ exports[`should take a snapshot 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select Route" + value="Select Route (Required)" /> - Select Route + Select Route (Required) @@ -194,7 +194,7 @@ exports[`should take a snapshot 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select RSU Model" + value="Select RSU Model (Required)" /> - Select RSU Model + Select RSU Model (Required) @@ -279,7 +279,7 @@ exports[`should take a snapshot 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select SSH Group" + value="Select SSH Group (Required)" /> - Select SSH Group + Select SSH Group (Required)

    {errors.email.message}

    } + {errors.email && ( +

    + {errors.email.message} +

    + )} First Name - {errors.first_name &&

    {errors.first_name.message}

    } + {errors.first_name && ( +

    + {errors.first_name.message} +

    + )}
    Last Name - {errors.last_name &&

    {errors.last_name.message}

    } + {errors.last_name && ( +

    + {errors.last_name.message} +

    + )}
    - + - + @@ -115,7 +132,7 @@ const AdminAddUser = () => { className="form-multiselect" dataKey="id" textField="name" - placeholder="Select organizations" + placeholder="Select organizations (Required)" data={organizationNames} value={selectedOrganizationNames} onChange={(value) => { @@ -152,11 +169,21 @@ const AdminAddUser = () => { )} {selectedOrganizations.length === 0 && submitAttempt && ( -

    Must select at least one organization

    +

    + Must select at least one organization +

    )} - {successMsg &&

    {successMsg}

    } - {errorState &&

    Failed to add user due to error: {errorMsg}

    } + {successMsg && ( +

    + {successMsg} +

    + )} + {errorState && ( +

    + Failed to add user due to error: {errorMsg} +

    + )}
    @@ -36,7 +36,7 @@ exports[`should take a snapshot 1`] = ` class="form-control" id="first_name" name="first_name" - placeholder="Enter user's first name" + placeholder="Enter user's first name (Required)" type="text" /> @@ -53,7 +53,7 @@ exports[`should take a snapshot 1`] = ` class="form-control" id="last_name" name="last_name" - placeholder="Enter user's last name" + placeholder="Enter user's last name (Required)" type="text" /> @@ -62,6 +62,7 @@ exports[`should take a snapshot 1`] = ` >
    { const { container } = render( - + + + ) diff --git a/webapp/src/features/adminEditOrganization/AdminEditOrganization.tsx b/webapp/src/features/adminEditOrganization/AdminEditOrganization.tsx index 8459fa4e..5959a306 100644 --- a/webapp/src/features/adminEditOrganization/AdminEditOrganization.tsx +++ b/webapp/src/features/adminEditOrganization/AdminEditOrganization.tsx @@ -9,6 +9,7 @@ import { // actions updateStates, editOrganization, + setSuccessMsg, } from './adminEditOrganizationSlice' import { useSelector, useDispatch } from 'react-redux' @@ -16,14 +17,26 @@ import '../adminRsuTab/Admin.css' import 'react-widgets/styles.css' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../../store' -import { adminOrgPatch, selectSelectedOrg } from '../adminOrganizationTab/adminOrganizationTabSlice' +import { + AdminOrgSummary, + adminOrgPatch, + getOrgData, + selectOrgData, + selectSelectedOrg, + setSelectedOrg, +} from '../adminOrganizationTab/adminOrganizationTabSlice' +import { Link, useParams, useNavigate } from 'react-router-dom' +import { ThemeProvider, Typography } from '@mui/material' +import { theme } from '../../styles' const AdminEditOrganization = () => { const dispatch: ThunkDispatch = useDispatch() + const successMsg = useSelector(selectSuccessMsg) const errorState = useSelector(selectErrorState) const errorMsg = useSelector(selectErrorMsg) const selectedOrg = useSelector(selectSelectedOrg) + const orgData = useSelector(selectOrgData) const { register, handleSubmit, @@ -35,38 +48,78 @@ const AdminEditOrganization = () => { }, }) + const { orgName } = useParams<{ orgName: string }>() + const navigate = useNavigate() + useEffect(() => { - updateStates(setValue, selectedOrg.name) - }, [setValue, selectedOrg.name]) + dispatch(getOrgData({ orgName })) + }, [orgName]) + + useEffect(() => { + const selectedOrg = (orgData ?? []).find((organization: AdminOrgSummary) => organization?.name === orgName) + dispatch(setSelectedOrg(selectedOrg)) + }, [orgData]) + + useEffect(() => { + dispatch(getOrgData({ orgName: 'all', all: true, specifiedOrg: undefined })) + }, [dispatch]) + + useEffect(() => { + updateStates(setValue, selectedOrg?.name) + }, [setValue, selectedOrg?.name]) const onSubmit = (data: adminOrgPatch) => { - dispatch(editOrganization({ json: data, setValue, selectedOrg: selectedOrg.name })) + dispatch(editOrganization({ json: data, setValue, selectedOrg: selectedOrg?.name })) } + useEffect(() => { + if (successMsg) navigate('..') + dispatch(setSuccessMsg('')) + }, [successMsg]) + return (
    -
    onSubmit(data))}> - - Organization Name - - {errors.name &&

    {errors.name.message}

    } -
    + {Object.keys(selectedOrg ?? {}).length != 0 ? ( + onSubmit(data))}> + + Organization Name + + {errors.name && ( +

    + {errors.name.message} +

    + )} +
    - {successMsg &&

    {successMsg}

    } - {errorState &&

    Failed to apply changes to organization due to error: {errorMsg}

    } -
    - - -
    -
    + {successMsg && ( +

    + {successMsg} +

    + )} + {errorState && ( +

    + Failed to apply changes to organization due to error: {errorMsg} +

    + )} +
    + + +
    + + ) : ( + + Unknown organization. Either this organization does not exist, or you do not have access to it.{' '} + Organizations + + )}
    ) } diff --git a/webapp/src/features/adminEditOrganization/__snapshots__/AdminEditOrganization.test.tsx.snap b/webapp/src/features/adminEditOrganization/__snapshots__/AdminEditOrganization.test.tsx.snap index efa5688c..a9aa90d3 100644 --- a/webapp/src/features/adminEditOrganization/__snapshots__/AdminEditOrganization.test.tsx.snap +++ b/webapp/src/features/adminEditOrganization/__snapshots__/AdminEditOrganization.test.tsx.snap @@ -3,39 +3,18 @@ exports[`should take a snapshot 1`] = `
    -
    -
    - - -
    - -
    -
    -
    + Organizations + +
    `; diff --git a/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.test.ts b/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.test.ts index 2e067f8f..8cc944db 100644 --- a/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.test.ts +++ b/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.test.ts @@ -65,6 +65,13 @@ describe('async thunks', () => { authLoginData: { token: 'token' }, }, }, + adminOrganizationTab: { + value: { + selectedOrg: { + name: 'prevSelectedOrg', + }, + }, + }, }) const json = { name: 'orgName' } const selectedOrg = 'selectedOrg' @@ -74,9 +81,10 @@ describe('async thunks', () => { global.setTimeout = jest.fn((cb) => cb()) as any try { let resp = await action(dispatch, getState, undefined) + console.error(JSON.stringify(resp)) expect(resp.payload).toEqual({ success: true, message: 'Changes were successfully applied!' }) expect(global.setTimeout).toHaveBeenCalledTimes(1) - expect(dispatch).toHaveBeenCalledTimes(3 + 2) + expect(dispatch).toHaveBeenCalledTimes(4 + 2) expect(setValue).toHaveBeenCalledTimes(2) expect(setValue).toHaveBeenCalledWith('orig_name', 'orgName') expect(setValue).toHaveBeenCalledWith('name', 'orgName') diff --git a/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.tsx b/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.tsx index c5a99f02..2f4ff7fc 100644 --- a/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.tsx +++ b/webapp/src/features/adminEditOrganization/adminEditOrganizationSlice.tsx @@ -3,7 +3,13 @@ import { selectToken } from '../../generalSlices/userSlice' import EnvironmentVars from '../../EnvironmentVars' import apiHelper from '../../apis/api-helper' import { RootState } from '../../store' -import { adminOrgPatch, editOrg, getOrgData } from '../adminOrganizationTab/adminOrganizationTabSlice' +import { + adminOrgPatch, + editOrg, + getOrgData, + selectSelectedOrg, + setSelectedOrg, +} from '../adminOrganizationTab/adminOrganizationTabSlice' const initialState = { successMsg: '', @@ -24,12 +30,14 @@ export const editOrganization = createAsyncThunk( selectedOrg: string setValue: (key: string, value: any) => void }, - { dispatch } + { dispatch, getState } ) => { const { json, selectedOrg, setValue } = payload + const prevSelectedOrg = selectSelectedOrg(getState() as RootState) const patchJson: adminOrgPatch = { - name: selectedOrg, + orig_name: selectedOrg, + name: json.name, users_to_modify: [], } @@ -38,8 +46,9 @@ export const editOrganization = createAsyncThunk( if (data.success) { dispatch(getOrgData({ orgName: 'all', all: true, specifiedOrg: json.name })) setTimeout(() => dispatch(adminEditOrganizationSlice.actions.setSuccessMsg('')), 5000) + dispatch(setSelectedOrg({ ...prevSelectedOrg, name: json.name })) updateStates(setValue, json.name) - return { success: true, message: data.message } + return { success: true, message: data.message == '' ? 'Organization updated successfully' : data.message } } else { setTimeout(() => dispatch(adminEditOrganizationSlice.actions.setSuccessMsg('')), 5000) return { success: false, message: data.message } diff --git a/webapp/src/features/adminEditRsu/AdminEditRsu.test.tsx b/webapp/src/features/adminEditRsu/AdminEditRsu.test.tsx index a8b392aa..69f0e274 100644 --- a/webapp/src/features/adminEditRsu/AdminEditRsu.test.tsx +++ b/webapp/src/features/adminEditRsu/AdminEditRsu.test.tsx @@ -4,11 +4,14 @@ import AdminEditRsu from './AdminEditRsu' import { Provider } from 'react-redux' import { setupStore } from '../../store' import { replaceChaoticIds } from '../../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { const { container } = render( - + + + ) diff --git a/webapp/src/features/adminEditRsu/AdminEditRsu.tsx b/webapp/src/features/adminEditRsu/AdminEditRsu.tsx index ae607e5c..63e35b54 100644 --- a/webapp/src/features/adminEditRsu/AdminEditRsu.tsx +++ b/webapp/src/features/adminEditRsu/AdminEditRsu.tsx @@ -40,6 +40,11 @@ import '../adminRsuTab/Admin.css' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../../store' import { AdminRsu } from '../../types/Rsu' +import { Link, useParams } from 'react-router-dom' +import { selectTableData, updateTableData } from '../adminRsuTab/adminRsuTabSlice' +import { Typography } from '@material-ui/core' +import { ThemeProvider } from '@mui/material' +import { theme } from '../../styles' export type AdminEditRsuFormType = { orig_ip: string @@ -61,11 +66,7 @@ export type AdminEditRsuFormType = { organizations_to_remove: string[] } -interface AdminEditRsuProps { - rsuData: AdminEditRsuFormType -} - -const AdminEditRsu = (props: AdminEditRsuProps) => { +const AdminEditRsu = () => { const dispatch: ThunkDispatch = useDispatch() const successMsg = useSelector(selectSuccessMsg) const apiData = useSelector(selectApiData) @@ -85,6 +86,7 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { const organizations = useSelector(selectOrganizations) const selectedOrganizations = useSelector(selectSelectedOrganizations) const submitAttempt = useSelector(selectSubmitAttempt) + const rsuTableData = useSelector(selectTableData) const { register, @@ -112,11 +114,13 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { }, }) - const { rsuData } = props + const { rsuIp } = useParams<{ rsuIp: string }>() useEffect(() => { - dispatch(getRsuInfo(rsuData.ip)) - }, [dispatch, rsuData.ip]) + if ((rsuTableData ?? []).find((rsu: AdminRsu) => rsu.ip === rsuIp) && Object.keys(apiData).length == 0) { + dispatch(getRsuInfo(rsuIp)) + } + }, [dispatch, rsuIp, rsuTableData]) useEffect(() => { if (apiData && Object.keys(apiData).length !== 0) { @@ -134,13 +138,17 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { dispatch(updateSelectedRoute(selectedRoute)) }, [selectedRoute, dispatch]) + useEffect(() => { + dispatch(updateTableData()) + }, [dispatch]) + const onSubmit = (data: AdminEditRsuFormType) => { dispatch(submitForm(data)) } return (
    - {apiData && ( + {Object.keys(apiData ?? {}).length != 0 ? (
    RSU IP @@ -156,7 +164,16 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { }, })} /> -

    {message}

    } /> + ( +

    + {' '} + {message}{' '} +

    + )} + />
    @@ -175,7 +192,12 @@ const AdminEditRsu = (props: AdminEditRsuProps) => {

    {message}

    } + render={({ message }) => ( +

    + {' '} + {message}{' '} +

    + )} />
    @@ -195,7 +217,12 @@ const AdminEditRsu = (props: AdminEditRsuProps) => {

    {message}

    } + render={({ message }) => ( +

    + {' '} + {message}{' '} +

    + )} /> @@ -215,7 +242,12 @@ const AdminEditRsu = (props: AdminEditRsuProps) => {

    {message}

    } + render={({ message }) => ( +

    + {' '} + {message}{' '} +

    + )} /> @@ -231,7 +263,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { dispatch(setSelectedRoute(value.name)) }} /> - {selectedRoute === '' && submitAttempt &&

    Must select a primary route

    } + {selectedRoute === '' && submitAttempt && ( +

    + Must select a primary route +

    + )} {(() => { if (selectedRoute === 'Other') { return ( @@ -257,7 +293,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { required: 'Please enter the RSU serial number', })} /> - {errors.serial_number &&

    {errors.serial_number.message}

    } + {errors.serial_number && ( +

    + {errors.serial_number.message} +

    + )} @@ -272,7 +312,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { dispatch(setSelectedModel(value.name)) }} /> - {selectedModel === '' && submitAttempt &&

    Must select a RSU model

    } + {selectedModel === '' && submitAttempt && ( +

    + Must select a RSU model +

    + )}
    @@ -284,7 +328,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { required: 'Please enter the SCMS ID', })} /> - {errors.scms_id &&

    {errors.scms_id.message}

    } + {errors.scms_id && ( +

    + {errors.scms_id.message} +

    + )}
    @@ -300,7 +348,9 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { }} /> {selectedSshGroup === '' && submitAttempt && ( -

    Must select a SSH credential group

    +

    + Must select a SSH credential group +

    )}
    @@ -317,7 +367,9 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { }} /> {selectedSnmpGroup === '' && submitAttempt && ( -

    Must select a SNMP credential group

    +

    + Must select a SNMP credential group +

    )} @@ -333,7 +385,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { dispatch(setSelectedSnmpVersion(value.name)) }} /> - {selectedSnmpVersion === '' && submitAttempt &&

    Must select a SNMP version

    } + {selectedSnmpVersion === '' && submitAttempt && ( +

    + Must select a SNMP version +

    + )} @@ -350,12 +406,22 @@ const AdminEditRsu = (props: AdminEditRsuProps) => { }} /> {selectedOrganizations.length === 0 && submitAttempt && ( -

    Must select an organization

    +

    + Must select an organization +

    )}
    - {successMsg &&

    {successMsg}

    } - {errorState &&

    Failed to apply changes due to error: {errorMsg}

    } + {successMsg && ( +

    + {successMsg} +

    + )} + {errorState && ( +

    + Failed to apply changes due to error: {errorMsg} +

    + )}
    @@ -364,6 +430,11 @@ const AdminEditRsu = (props: AdminEditRsuProps) => {
    + ) : ( + + Unknown RSU IP address. Either this RSU does not exist, or you do not have access to it.{' '} + RSUs + )}
    ) diff --git a/webapp/src/features/adminEditRsu/__snapshots__/AdminEditRsu.test.tsx.snap b/webapp/src/features/adminEditRsu/__snapshots__/AdminEditRsu.test.tsx.snap index 73d30b07..f785589a 100644 --- a/webapp/src/features/adminEditRsu/__snapshots__/AdminEditRsu.test.tsx.snap +++ b/webapp/src/features/adminEditRsu/__snapshots__/AdminEditRsu.test.tsx.snap @@ -3,506 +3,18 @@ exports[`should take a snapshot 1`] = `
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    + Unknown RSU IP address. Either this RSU does not exist, or you do not have access to it. + + + RSUs + +
    `; diff --git a/webapp/src/features/adminEditUser/AdminEditUser.test.tsx b/webapp/src/features/adminEditUser/AdminEditUser.test.tsx index edb87c2e..9dd2cac1 100644 --- a/webapp/src/features/adminEditUser/AdminEditUser.test.tsx +++ b/webapp/src/features/adminEditUser/AdminEditUser.test.tsx @@ -4,11 +4,14 @@ import AdminEditUser from './AdminEditUser' import { Provider } from 'react-redux' import { setupStore } from '../../store' import { replaceChaoticIds } from '../../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { const { container } = render( - {}} /> + + + ) diff --git a/webapp/src/features/adminEditUser/AdminEditUser.tsx b/webapp/src/features/adminEditUser/AdminEditUser.tsx index 9368a84e..eb22d12c 100644 --- a/webapp/src/features/adminEditUser/AdminEditUser.tsx +++ b/webapp/src/features/adminEditUser/AdminEditUser.tsx @@ -26,13 +26,12 @@ import '../adminRsuTab/Admin.css' import 'react-widgets/styles.css' import { ThunkDispatch, AnyAction } from '@reduxjs/toolkit' import { RootState } from '../../store' +import { Link, useParams } from 'react-router-dom' +import { getAvailableUsers, selectTableData } from '../adminUserTab/adminUserTabSlice' +import { ThemeProvider, Typography } from '@mui/material' +import { theme } from '../../styles' -interface AdminEditUserProps { - userData: AdminUserWithId - updateUserData: () => void -} - -const AdminEditUser = (props: AdminEditUserProps) => { +const AdminEditUser = () => { const dispatch: ThunkDispatch = useDispatch() const successMsg = useSelector(selectSuccessMsg) const selectedOrganizationNames = useSelector(selectSelectedOrganizationNames) @@ -43,6 +42,7 @@ const AdminEditUser = (props: AdminEditUserProps) => { const errorState = useSelector(selectErrorState) const errorMsg = useSelector(selectErrorMsg) const submitAttempt = useSelector(selectSubmitAttempt) + const userTableData = useSelector(selectTableData) const { register, handleSubmit, @@ -62,11 +62,32 @@ const AdminEditUser = (props: AdminEditUserProps) => { }, }) - const { userData } = props + const { email } = useParams<{ email: string }>() + + useEffect(() => { + if ( + (userTableData ?? []).find((user: AdminUserWithId) => user.email === email) && + Object.keys(apiData ?? {}).length == 0 + ) { + console.log('getUserData') + dispatch(getUserData(email)) + } + console.log( + 'useEffect getUserData', + email, + userTableData, + apiData, + (userTableData ?? []).find((user: AdminUserWithId) => user.email === email), + Object.keys(apiData ?? {}), + Object.keys(apiData ?? {}).length, + (userTableData ?? []).find((user: AdminUserWithId) => user.email === email) && + Object.keys(apiData ?? {}).length == 0 + ) + }, [email, userTableData, dispatch]) useEffect(() => { - dispatch(getUserData(userData.email)) - }, [userData, dispatch]) + dispatch(getAvailableUsers()) + }, [dispatch]) useEffect(() => { if (apiData && Object.keys(apiData).length !== 0) { @@ -77,117 +98,154 @@ const AdminEditUser = (props: AdminEditUserProps) => { setValue('super_user', apiData.user_data.super_user.toString()) setValue('receive_error_emails', apiData.user_data.receive_error_emails.toString()) } + console.log('useEffect apiData', email, userTableData, apiData) }, [apiData, setValue]) const onSubmit = (data: UserApiDataOrgs) => { - dispatch(submitForm({ data, updateUserData: props.updateUserData })) + dispatch(submitForm({ data })) } + console.log('render', email, userTableData, apiData, Object.keys(apiData ?? {}).length) + return (
    -
    - - Email - - {errors.email &&

    {errors.email.message}

    } -
    - - - First Name - - {errors.first_name &&

    {errors.first_name.message}

    } -
    - - - Last Name - - {errors.last_name &&

    {errors.last_name.message}

    } -
    - - - - - - - - - - - Organizations - { - dispatch(updateOrganizations(value)) - }} - /> - - - {selectedOrganizations.length > 0 && ( - - Roles -

    - {selectedOrganizations.map((organization) => { - let role = { role: organization.role } - - return ( - - {organization.name} - { - dispatch(setSelectedRole({ ...organization, role: value.role })) - }} - /> - - ) - })} + {Object.keys(apiData ?? {}).length != 0 ? ( + + + Email + + {errors.email && ( +

    + {errors.email.message} +

    + )} +
    + + + First Name + + {errors.first_name && ( +

    + {errors.first_name.message} +

    + )}
    - )} - - {selectedOrganizations.length === 0 && submitAttempt && ( -

    Must select at least one organization

    - )} - - {successMsg &&

    {successMsg}

    } - {errorState &&

    Failed to apply changes due to error: {errorMsg}

    } -
    - - -
    -
    + + + Last Name + + {errors.last_name && ( +

    + {errors.last_name.message} +

    + )} +
    + + + + + + + + + + + Organizations + { + dispatch(updateOrganizations(value)) + }} + /> + + + {selectedOrganizations.length > 0 && ( + + Roles +

    + {selectedOrganizations.map((organization) => { + let role = { role: organization.role } + + return ( + + {organization.name} + { + dispatch(setSelectedRole({ ...organization, role: value.role })) + }} + /> + + ) + })} + + )} + + {selectedOrganizations.length === 0 && submitAttempt && ( +

    + Must select at least one organization +

    + )} + + {successMsg && ( +

    + {successMsg} +

    + )} + {errorState && ( +

    + Failed to apply changes due to error: {errorMsg} +

    + )} +
    + + +
    + + ) : ( + + Unknown email address. Either this user does not exist, or you do not have permissions to view them.{' '} + Users + + )}
    ) } diff --git a/webapp/src/features/adminEditUser/__snapshots__/AdminEditUser.test.tsx.snap b/webapp/src/features/adminEditUser/__snapshots__/AdminEditUser.test.tsx.snap index deea6334..0bb6fe81 100644 --- a/webapp/src/features/adminEditUser/__snapshots__/AdminEditUser.test.tsx.snap +++ b/webapp/src/features/adminEditUser/__snapshots__/AdminEditUser.test.tsx.snap @@ -3,167 +3,18 @@ exports[`should take a snapshot 1`] = `
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    + Users + +
    `; diff --git a/webapp/src/features/adminEditUser/adminEditUserSlice.test.ts b/webapp/src/features/adminEditUser/adminEditUserSlice.test.ts index 3589ecfb..7fff97ca 100644 --- a/webapp/src/features/adminEditUser/adminEditUserSlice.test.ts +++ b/webapp/src/features/adminEditUser/adminEditUserSlice.test.ts @@ -180,8 +180,7 @@ describe('async thunks', () => { }, }) const json = { data: 'data' } - let updateUserData = jest.fn() - let action = editUser({ json, updateUserData }) + let action = editUser({ json }) global.setTimeout = jest.fn((cb) => cb()) as any try { @@ -194,16 +193,14 @@ describe('async thunks', () => { body: JSON.stringify(json), }) expect(setTimeout).toHaveBeenCalledTimes(1) - expect(updateUserData).toHaveBeenCalledTimes(1) - expect(dispatch).toHaveBeenCalledTimes(1 + 2) + expect(dispatch).toHaveBeenCalledTimes(2 + 2) } catch (e) { ;(global.setTimeout as any).mockClear() throw e } dispatch = jest.fn() - updateUserData = jest.fn() - action = editUser({ json, updateUserData }) + action = editUser({ json }) global.setTimeout = jest.fn((cb) => cb()) as any try { apiHelper._patchData = jest.fn().mockReturnValue({ status: 500, message: 'message' }) @@ -215,7 +212,6 @@ describe('async thunks', () => { body: JSON.stringify(json), }) expect(setTimeout).not.toHaveBeenCalled() - expect(updateUserData).not.toHaveBeenCalled() expect(dispatch).toHaveBeenCalledTimes(0 + 2) } catch (e) { ;(global.setTimeout as any).mockClear() @@ -307,7 +303,7 @@ describe('async thunks', () => { const data = { data: 'data' } as any let updateUserData = jest.fn() - let action = submitForm({ data, updateUserData }) + let action = submitForm({ data }) let resp = await action(dispatch, getState, undefined) expect(resp.payload).toEqual(false) expect(dispatch).toHaveBeenCalledTimes(1 + 2) @@ -326,7 +322,7 @@ describe('async thunks', () => { }, }, }) - action = submitForm({ data, updateUserData }) + action = submitForm({ data }) resp = await action(dispatch, getState, undefined) expect(resp.payload).toEqual(true) expect(dispatch).toHaveBeenCalledTimes(0 + 2) diff --git a/webapp/src/features/adminEditUser/adminEditUserSlice.tsx b/webapp/src/features/adminEditUser/adminEditUserSlice.tsx index af2f4d2f..038d55ec 100644 --- a/webapp/src/features/adminEditUser/adminEditUserSlice.tsx +++ b/webapp/src/features/adminEditUser/adminEditUserSlice.tsx @@ -3,6 +3,7 @@ import { selectToken } from '../../generalSlices/userSlice' import EnvironmentVars from '../../EnvironmentVars' import apiHelper from '../../apis/api-helper' import { RootState } from '../../store' +import { getAvailableUsers } from '../adminUserTab/adminUserTabSlice' type UserDataResp = { success: boolean; message: string; data?: UserApiData } export type UserApiData = { @@ -94,8 +95,8 @@ export const getUserData = createAsyncThunk( export const editUser = createAsyncThunk( 'adminEditUser/editUser', - async (payload: { json: Object; updateUserData: () => void }, { getState, dispatch }) => { - const { json, updateUserData } = payload + async (payload: { json: Object }, { getState, dispatch }) => { + const { json } = payload const currentState = getState() as RootState const token = selectToken(currentState) @@ -107,7 +108,7 @@ export const editUser = createAsyncThunk( switch (data.status) { case 200: - updateUserData() + dispatch(getAvailableUsers()) setTimeout(() => dispatch(adminEditUserSlice.actions.setSuccessMsg('')), 5000) return { success: true, message: 'Changes were successfully applied!' } default: @@ -119,8 +120,8 @@ export const editUser = createAsyncThunk( export const submitForm = createAsyncThunk( 'adminEditUser/submitForm', - async (payload: { data: UserApiDataOrgs; updateUserData: () => void }, { getState, dispatch }) => { - const { data, updateUserData } = payload + async (payload: { data: UserApiDataOrgs }, { getState, dispatch }) => { + const { data } = payload const currentState = getState() as RootState const selectedOrganizations = selectSelectedOrganizations(currentState) const apiData = selectApiData(currentState) @@ -129,7 +130,7 @@ export const submitForm = createAsyncThunk( let submitOrgs = [...selectedOrganizations].map((org) => ({ ...org })) submitOrgs.forEach((elm) => delete elm.id) const tempData = organizationParser(data, submitOrgs, apiData) - dispatch(editUser({ json: tempData, updateUserData })) + dispatch(editUser({ json: tempData })) return false } else { return true diff --git a/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.test.tsx b/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.test.tsx index 07f2e1dc..dd0145ba 100644 --- a/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.test.tsx +++ b/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.test.tsx @@ -4,11 +4,14 @@ import AdminOrganizationTab from './AdminOrganizationTab' import { Provider } from 'react-redux' import { setupStore } from '../../store' import { replaceChaoticIds } from '../../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { const { container } = render( - + + + ) diff --git a/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.tsx b/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.tsx index 3ab0f377..fd0b85bf 100644 --- a/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.tsx +++ b/webapp/src/features/adminOrganizationTab/AdminOrganizationTab.tsx @@ -10,8 +10,6 @@ import Grid from '@mui/material/Grid' import EditIcon from '@mui/icons-material/Edit' import { DropdownList } from 'react-widgets' import { - selectActiveDiv, - selectTitle, selectOrgData, selectSelectedOrg, selectSelectedOrgName, @@ -21,11 +19,9 @@ import { selectErrorMsg, // actions - editOrg, deleteOrg, getOrgData, updateTitle, - setActiveDiv, setSelectedOrg, } from './adminOrganizationTabSlice' import { useSelector, useDispatch } from 'react-redux' @@ -33,11 +29,28 @@ import { useSelector, useDispatch } from 'react-redux' import '../adminRsuTab/Admin.css' import { RootState } from '../../store' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import { NotFound } from '../../pages/404' + +const getTitle = (activeTab: string) => { + if (activeTab === undefined) { + return 'CV Manager Organizations' + } else if (activeTab === 'editOrganization') { + return 'Edit Organization' + } else if (activeTab === 'addOrganization') { + return 'Add Organization' + } + return 'Unknown' +} const AdminOrganizationTab = () => { const dispatch: ThunkDispatch = useDispatch() - const activeDiv = useSelector(selectActiveDiv) - const title = useSelector(selectTitle) + const navigate = useNavigate() + const location = useLocation() + + const activeTab = location.pathname.split('/')[4] + const title = getTitle(activeTab) + const orgData = useSelector(selectOrgData) const selectedOrg = useSelector(selectSelectedOrg) const selectedOrgName = useSelector(selectSelectedOrgName) @@ -46,10 +59,6 @@ const AdminOrganizationTab = () => { const errorState = useSelector(selectErrorState) const errorMsg = useSelector(selectErrorMsg) - const updateOrgData = async (specifiedOrg: string) => { - dispatch(getOrgData({ orgName: 'all', all: true, specifiedOrg })) - } - useEffect(() => { dispatch(getOrgData({ orgName: 'all', all: true, specifiedOrg: undefined })) }, [dispatch]) @@ -64,7 +73,7 @@ const AdminOrganizationTab = () => { useEffect(() => { dispatch(updateTitle()) - }, [activeDiv, dispatch]) + }, [activeTab, dispatch]) const refresh = () => { updateTableData(selectedOrgName) @@ -74,24 +83,18 @@ const AdminOrganizationTab = () => {

    - {activeDiv !== 'organization_table' && ( - )} {title} - {activeDiv === 'organization_table' && [ + {activeTab === undefined && [

    - {errorState &&

    Failed to obtain data due to error: {errorMsg}

    } - - {activeDiv === 'organization_table' && ( -
    - - - dispatch(setSelectedOrg(value))} - /> - - - - - - dispatch(deleteOrg(selectedOrgName))} - selectedOrganization={selectedOrgName} - /> - - - -
    - {activeDiv === 'organization_table' && [ - , - , - ]} -
    -
    + {errorState && ( +

    + Failed to obtain data due to error: {errorMsg} +

    )} - {activeDiv === 'add_organization' && ( -
    - -
    - )} - - {activeDiv === 'edit_organization' && ( -
    - -
    - )} + + + + + dispatch(setSelectedOrg(value))} + /> + + + + + + dispatch(deleteOrg(selectedOrgName))} + selectedOrganization={selectedOrgName} + /> + + + +
    + <> + + + +
    +
    + } + /> + + +
    + } + /> + + +
    + } + /> + + } + /> + ) } diff --git a/webapp/src/features/adminOrganizationTab/__snapshots__/AdminOrganizationTab.test.tsx.snap b/webapp/src/features/adminOrganizationTab/__snapshots__/AdminOrganizationTab.test.tsx.snap index 133b5c04..db7ec845 100644 --- a/webapp/src/features/adminOrganizationTab/__snapshots__/AdminOrganizationTab.test.tsx.snap +++ b/webapp/src/features/adminOrganizationTab/__snapshots__/AdminOrganizationTab.test.tsx.snap @@ -140,6 +140,7 @@ exports[`should take a snapshot 1`] = ` data-testid="EditIcon" focusable="false" size="20" + style="color: white;" viewBox="0 0 24 24" > state.adminOrganizationTab. export const selectTitle = (state: RootState) => state.adminOrganizationTab.value.title export const selectOrgData = (state: RootState) => state.adminOrganizationTab.value.orgData export const selectSelectedOrg = (state: RootState) => state.adminOrganizationTab.value.selectedOrg -export const selectSelectedOrgName = (state: RootState) => state.adminOrganizationTab.value.selectedOrg.name +export const selectSelectedOrgName = (state: RootState) => state.adminOrganizationTab.value.selectedOrg?.name export const selectRsuTableData = (state: RootState) => state.adminOrganizationTab.value.rsuTableData export const selectUserTableData = (state: RootState) => state.adminOrganizationTab.value.userTableData export const selectErrorState = (state: RootState) => state.adminOrganizationTab.value.errorState diff --git a/webapp/src/features/adminRsuTab/Admin.css b/webapp/src/features/adminRsuTab/Admin.css index 280a9486..e682704e 100644 --- a/webapp/src/features/adminRsuTab/Admin.css +++ b/webapp/src/features/adminRsuTab/Admin.css @@ -14,7 +14,7 @@ .errorMsg { max-width: 350px; - color: #f21e08; + color: #f83a25; padding: 2px 0; margin-top: 2px; font-size: 14px; @@ -109,7 +109,7 @@ .admin-button { font-family: Arial, Helvetica, sans-serif; font-weight: 550; - background-color: #d16d15; + background-color: #b55e12; border: none; color: white; padding: 8px 10px; @@ -126,7 +126,7 @@ margin-top: -4px; margin-right: 10px; padding: 6px 6px; - background-color: #d16d15; + background-color: #b55e12; color: white; border: none; border-radius: 10px; @@ -181,7 +181,7 @@ } .expand { - color: #d16d15; + color: #e98125; } .success-msg { @@ -189,7 +189,7 @@ } .error-msg { - color: rgb(255, 0, 0); + color: #f83a25; margin-bottom: 10px; background-color: #1c1d1f; } @@ -234,7 +234,7 @@ margin-right: 10px; float: right; padding: 6px 6px; - background-color: #d16d15; + background-color: #b55e12; color: white; border: none; border-radius: 10px; @@ -250,7 +250,7 @@ margin-left: 10px; float: left; padding: 6px 6px; - background-color: #d16d15; + background-color: #b55e12; color: white; border: none; border-radius: 10px; @@ -286,3 +286,7 @@ direction: ltr; -webkit-font-smoothing: antialiased; } + +.form-check-label { + color: 'white'; +} diff --git a/webapp/src/features/adminRsuTab/AdminRsuTab.test.tsx b/webapp/src/features/adminRsuTab/AdminRsuTab.test.tsx index dc2fb8d5..5895d705 100644 --- a/webapp/src/features/adminRsuTab/AdminRsuTab.test.tsx +++ b/webapp/src/features/adminRsuTab/AdminRsuTab.test.tsx @@ -4,11 +4,14 @@ import AdminRsuTab from './AdminRsuTab' import { Provider } from 'react-redux' import { setupStore } from '../../store' import { replaceChaoticIds } from '../../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('snapshot add', () => { const { container } = render( - + + + ) @@ -20,7 +23,9 @@ it('snapshot add', () => { it('snapshot refresh', () => { const { container } = render( - + + + ) diff --git a/webapp/src/features/adminRsuTab/AdminRsuTab.tsx b/webapp/src/features/adminRsuTab/AdminRsuTab.tsx index 0a9c2ff6..1bb6ff7f 100644 --- a/webapp/src/features/adminRsuTab/AdminRsuTab.tsx +++ b/webapp/src/features/adminRsuTab/AdminRsuTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useEffect, useState } from 'react' import AdminAddRsu from '../adminAddRsu/AdminAddRsu' import AdminEditRsu, { AdminEditRsuFormType } from '../adminEditRsu/AdminEditRsu' import AdminTable from '../../components/AdminTable' @@ -8,15 +8,11 @@ import { confirmAlert } from 'react-confirm-alert' import { Options } from '../../components/AdminDeletionOptions' import { selectLoading, - selectActiveDiv, selectTableData, - selectTitle, selectEditRsuRowData, // actions updateTableData, - setTitle, - setActiveDiv, deleteMultipleRsus, deleteRsu, setEditRsuRowData, @@ -27,14 +23,30 @@ import './Admin.css' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../../store' import { Action } from '@material-table/core' -import { AdminRsu } from '../../types/Rsu' +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import { NotFound } from '../../pages/404' + +const getTitle = (activeTab: string) => { + if (activeTab === undefined) { + return 'CV Manager RSUs' + } else if (activeTab === 'editRsu') { + return 'Edit RSU' + } else if (activeTab === 'addRsu') { + return 'Add RSU' + } + return 'Unknown' +} const AdminRsuTab = () => { const dispatch: ThunkDispatch = useDispatch() + const navigate = useNavigate() + const location = useLocation() + + const activeTab = location.pathname.split('/')[4] + const title = getTitle(activeTab) + console.log('Active Tab:', activeTab) - const activeDiv = useSelector(selectActiveDiv) const tableData = useSelector(selectTableData) - const title = useSelector(selectTitle) const [columns] = useState([ { title: 'Milepost', field: 'milepost', id: 0 }, { title: 'IP Address', field: 'ip', id: 1 }, @@ -42,7 +54,6 @@ const AdminRsuTab = () => { { title: 'RSU Model', field: 'model', id: 3 }, { title: 'Serial Number', field: 'serial_number', id: 4 }, ]) - const editRsuRowData = useSelector(selectEditRsuRowData) const loading = useSelector(selectLoading) @@ -83,17 +94,10 @@ const AdminRsuTab = () => { }, }, ] - useEffect(() => { - dispatch(setActiveDiv('rsu_table')) - }, [dispatch]) - - useEffect(() => { - dispatch(setTitle()) - }, [activeDiv, dispatch]) const onEdit = (row: AdminEditRsuFormType) => { dispatch(setEditRsuRowData(row)) - dispatch(setActiveDiv('edit_rsu')) + navigate('editRsu/' + row.ip) } const onDelete = (row: AdminEditRsuFormType) => { @@ -108,24 +112,26 @@ const AdminRsuTab = () => {

    - {activeDiv !== 'rsu_table' && ( + {activeTab !== undefined && ( )} {title} - {activeDiv === 'rsu_table' && [ + {activeTab === undefined && [

    - {activeDiv === 'rsu_table' && loading === false && ( -
    - -
    - )} - - {activeDiv === 'add_rsu' && ( -
    - -
    - )} - - {activeDiv === 'edit_rsu' && ( -
    - -
    - )} + + + +
    + ) + } + /> + + + + } + /> + + + + } + /> + + } + /> + ) } diff --git a/webapp/src/features/adminRsuTab/__snapshots__/AdminRsuTab.test.tsx.snap b/webapp/src/features/adminRsuTab/__snapshots__/AdminRsuTab.test.tsx.snap index 114b3138..83117811 100644 --- a/webapp/src/features/adminRsuTab/__snapshots__/AdminRsuTab.test.tsx.snap +++ b/webapp/src/features/adminRsuTab/__snapshots__/AdminRsuTab.test.tsx.snap @@ -7,8 +7,31 @@ exports[`snapshot add 1`] = `

    + CV Manager RSUs + - Add RSU

    @@ -74,7 +97,7 @@ exports[`snapshot add 1`] = ` class="form-control" id="latitude" name="latitude" - placeholder="Enter RSU Latitude" + placeholder="Enter RSU Latitude (Required)" type="text" /> @@ -91,7 +114,7 @@ exports[`snapshot add 1`] = ` class="form-control" id="longitude" name="longitude" - placeholder="Enter RSU Longitude" + placeholder="Enter RSU Longitude (Required)" type="text" /> @@ -108,7 +131,7 @@ exports[`snapshot add 1`] = ` class="form-control" id="milepost" name="milepost" - placeholder="Enter RSU Milepost" + placeholder="Enter RSU Milepost (Required)" type="text" /> @@ -147,7 +170,7 @@ exports[`snapshot add 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select Route" + value="Select Route (Required)" /> - Select Route + Select Route (Required)
    @@ -232,7 +255,7 @@ exports[`snapshot add 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select RSU Model" + value="Select RSU Model (Required)" /> - Select RSU Model + Select RSU Model (Required) @@ -317,7 +340,7 @@ exports[`snapshot add 1`] = ` aria-hidden="true" class="rw-detect-autofill rw-sr" tabindex="-1" - value="Select SSH Group" + value="Select SSH Group (Required)" /> - Select SSH Group + Select SSH Group (Required)

    -
    - -
    + RSU IP + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +

    +

    +
    +
    +
    diff --git a/webapp/src/features/adminRsuTab/adminRsuTabSlice.test.ts b/webapp/src/features/adminRsuTab/adminRsuTabSlice.test.ts index 98ab2292..bc57993a 100644 --- a/webapp/src/features/adminRsuTab/adminRsuTabSlice.test.ts +++ b/webapp/src/features/adminRsuTab/adminRsuTabSlice.test.ts @@ -6,15 +6,11 @@ import { deleteMultipleRsus, // reducers - setTitle, - setActiveDiv, setEditRsuRowData, // selectors selectLoading, - selectActiveDiv, selectTableData, - selectTitle, selectColumns, selectEditRsuRowData, } from './adminRsuTabSlice' @@ -27,7 +23,6 @@ describe('admin RSU tab reducer', () => { expect(reducer(undefined, { type: 'unknown' })).toEqual({ loading: false, value: { - activeDiv: 'rsu_table', tableData: [], title: 'RSUs', columns: [ @@ -47,7 +42,6 @@ describe('async thunks', () => { const initialState: RootState['adminRsuTab'] = { loading: null, value: { - activeDiv: null, tableData: null, title: null, columns: null, @@ -231,7 +225,6 @@ describe('reducers', () => { const initialState: RootState['adminRsuTab'] = { loading: null, value: { - activeDiv: null, tableData: null, title: null, columns: null, @@ -239,37 +232,6 @@ describe('reducers', () => { }, } - it('setTitle reducer updates state correctly', async () => { - let activeDiv = 'rsu_table' - let title = 'CV Manager RSUs' - expect(reducer({ ...initialState, value: { ...initialState.value, activeDiv } }, setTitle())).toEqual({ - ...initialState, - value: { ...initialState.value, activeDiv, title }, - }) - - activeDiv = 'edit_rsu' - title = 'Edit RSU' - expect(reducer({ ...initialState, value: { ...initialState.value, activeDiv } }, setTitle())).toEqual({ - ...initialState, - value: { ...initialState.value, activeDiv, title }, - }) - - activeDiv = 'add_rsu' - title = 'Add RSU' - expect(reducer({ ...initialState, value: { ...initialState.value, activeDiv } }, setTitle())).toEqual({ - ...initialState, - value: { ...initialState.value, activeDiv, title }, - }) - }) - - it('setActiveDiv reducer updates state correctly', async () => { - const activeDiv = 'activeDiv' - expect(reducer(initialState, setActiveDiv(activeDiv))).toEqual({ - ...initialState, - value: { ...initialState.value, activeDiv }, - }) - }) - it('setEditRsuRowData reducer updates state correctly', async () => { const editRsuRowData = 'editRsuRowData' expect(reducer(initialState, setEditRsuRowData(editRsuRowData))).toEqual({ @@ -283,7 +245,6 @@ describe('selectors', () => { const initialState = { loading: 'loading', value: { - activeDiv: 'activeDiv', tableData: 'tableData', title: 'title', columns: 'columns', @@ -294,9 +255,7 @@ describe('selectors', () => { it('selectors return the correct value', async () => { expect(selectLoading(state)).toEqual('loading') - expect(selectActiveDiv(state)).toEqual('activeDiv') expect(selectTableData(state)).toEqual('tableData') - expect(selectTitle(state)).toEqual('title') expect(selectColumns(state)).toEqual('columns') expect(selectEditRsuRowData(state)).toEqual('editRsuRowData') }) diff --git a/webapp/src/features/adminRsuTab/adminRsuTabSlice.tsx b/webapp/src/features/adminRsuTab/adminRsuTabSlice.tsx index 90e50699..bc036c6b 100644 --- a/webapp/src/features/adminRsuTab/adminRsuTabSlice.tsx +++ b/webapp/src/features/adminRsuTab/adminRsuTabSlice.tsx @@ -8,7 +8,6 @@ import { AdminEditRsuFormType } from '../adminEditRsu/AdminEditRsu' import { AdminRsu } from '../../types/Rsu' const initialState = { - activeDiv: 'rsu_table', tableData: [] as AdminEditRsuFormType[], title: 'RSUs', columns: [ @@ -93,18 +92,7 @@ export const adminRsuTabSlice = createSlice({ value: initialState, }, reducers: { - setTitle: (state) => { - if (state.value.activeDiv === 'rsu_table') { - state.value.title = 'CV Manager RSUs' - } else if (state.value.activeDiv === 'edit_rsu') { - state.value.title = 'Edit RSU' - } else if (state.value.activeDiv === 'add_rsu') { - state.value.title = 'Add RSU' - } - }, - setActiveDiv: (state, action) => { - state.value.activeDiv = action.payload - }, + setTitle: (state) => {}, setEditRsuRowData: (state, action) => { state.value.editRsuRowData = action.payload }, @@ -133,12 +121,10 @@ export const adminRsuTabSlice = createSlice({ }, }) -export const { setTitle, setActiveDiv, setEditRsuRowData } = adminRsuTabSlice.actions +export const { setEditRsuRowData } = adminRsuTabSlice.actions export const selectLoading = (state: RootState) => state.adminRsuTab.loading -export const selectActiveDiv = (state: RootState) => state.adminRsuTab.value.activeDiv export const selectTableData = (state: RootState) => state.adminRsuTab.value.tableData -export const selectTitle = (state: RootState) => state.adminRsuTab.value.title export const selectColumns = (state: RootState) => state.adminRsuTab.value.columns export const selectEditRsuRowData = (state: RootState) => state.adminRsuTab.value.editRsuRowData diff --git a/webapp/src/features/adminUserTab/AdminUserTab.test.tsx b/webapp/src/features/adminUserTab/AdminUserTab.test.tsx index df5247f8..0242fafa 100644 --- a/webapp/src/features/adminUserTab/AdminUserTab.test.tsx +++ b/webapp/src/features/adminUserTab/AdminUserTab.test.tsx @@ -4,11 +4,14 @@ import AdminUserTab from './AdminUserTab' import { Provider } from 'react-redux' import { setupStore } from '../../store' import { replaceChaoticIds } from '../../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { const { container } = render( - + + + ) diff --git a/webapp/src/features/adminUserTab/AdminUserTab.tsx b/webapp/src/features/adminUserTab/AdminUserTab.tsx index ac7cde62..872409f5 100644 --- a/webapp/src/features/adminUserTab/AdminUserTab.tsx +++ b/webapp/src/features/adminUserTab/AdminUserTab.tsx @@ -8,10 +8,7 @@ import { confirmAlert } from 'react-confirm-alert' import { Options } from '../../components/AdminDeletionOptions' import { selectLoading } from '../../generalSlices/rsuSlice' import { - selectActiveDiv, selectTableData, - selectTitle, - selectEditUserRowData, // actions getAvailableUsers, @@ -26,13 +23,29 @@ import '../adminRsuTab/Admin.css' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../../store' import { Action } from '@material-table/core' +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import { NotFound } from '../../pages/404' + +const getTitle = (activeTab: string) => { + if (activeTab === undefined) { + return 'CV Manager Users' + } else if (activeTab === 'editUser') { + return 'Edit User' + } else if (activeTab === 'addUser') { + return 'Add User' + } + return 'Unknown' +} const AdminUserTab = () => { const dispatch: ThunkDispatch = useDispatch() - const activeDiv = useSelector(selectActiveDiv) + const navigate = useNavigate() + const location = useLocation() + + const activeTab = location.pathname.split('/')[4] + const title = getTitle(activeTab) + const tableData = useSelector(selectTableData) - const title = useSelector(selectTitle) - const editUserRowData = useSelector(selectEditUserRowData) const [columns] = useState([ { title: 'First Name', field: 'first_name', id: 0 }, { title: 'Last Name', field: 'last_name', id: 1 }, @@ -112,34 +125,25 @@ const AdminUserTab = () => { useEffect(() => { dispatch(updateTitle()) - }, [activeDiv, dispatch]) + }, [activeTab, dispatch]) const onEdit = (row: AdminUserWithId) => { dispatch(setEditUserRowData(row)) - dispatch(setActiveDiv('edit_user')) + navigate('editUser/' + row.email) } return (

    - {activeDiv !== 'user_table' && ( - )} {title} - {activeDiv === 'user_table' && [ - ,

    - {activeDiv === 'user_table' && loading === false && ( -
    - -
    - )} - - {activeDiv === 'add_user' && ( -
    - -
    - )} - - {activeDiv === 'edit_user' && ( -
    - -
    - )} + + + +
    + ) + } + /> + + + + } + /> + + + + } + /> + + } + /> + ) } diff --git a/webapp/src/features/menu/DisplayCounts.tsx b/webapp/src/features/menu/DisplayCounts.tsx index 7a4cd3f2..addfd17e 100644 --- a/webapp/src/features/menu/DisplayCounts.tsx +++ b/webapp/src/features/menu/DisplayCounts.tsx @@ -32,7 +32,7 @@ const messageTypeOptions = EnvironmentVars.getMessageTypes().map((type) => { const DisplayCounts = () => { const dispatch: ThunkDispatch = useDispatch() - const msgType = useSelector(selectMsgType) + const countsMsgType = useSelector(selectMsgType) const startDate = useSelector(selectStartDate) const endDate = useSelector(selectEndDate) const requestOut = useSelector(selectRequestOut) @@ -43,12 +43,14 @@ const DisplayCounts = () => { const sortedCountList = useSelector(selectSortedCountList) const dateChanged = (e: Date, type: 'start' | 'end') => { - dispatch(changeDate(e, type, requestOut)) + if (!Number.isNaN(Date.parse(e.toString()))) { + dispatch(changeDate(e, type, requestOut)) + } } const getWarningMessage = (warning: boolean) => warning ? ( - +

    Warning: time ranges greater than 24 hours may have longer load times.

    ) : ( @@ -87,18 +89,25 @@ const DisplayCounts = () => { return (
    -

    {msgType} Counts

    +

    {countsMsgType} Counts

    -
    +
    { + if (e === null) return dateChanged(e.toDate(), 'start') }} - renderInput={(params) => } + renderInput={(params) => ( + + )} />
    @@ -110,16 +119,23 @@ const DisplayCounts = () => { minDateTime={dayjs(startDate)} maxDateTime={dayjs(new Date())} onChange={(e) => { + if (e === null) return dateChanged(e.toDate(), 'end') }} - renderInput={(params) => } + renderInput={(params) => ( + + )} />
    Select end date
    { }, }, }) - RsuApi.postRsuData = jest.fn().mockReturnValue({ status: 200, body: { RsuFwdSnmpwalk: 'test' } }) + RsuApi.getRsuMsgFwdConfigs = jest.fn().mockReturnValue({ RsuFwdSnmpwalk: 'test' }) - const arg = ['1.2.3.4', '2.3.4.5'] + const rsu_ip = '1.2.3.4' - const action = refreshSnmpFwdConfig(arg) + const action = refreshSnmpFwdConfig(rsu_ip) let resp = await action(dispatch, getState, undefined) - expect(RsuApi.postRsuData).toHaveBeenCalledWith( - 'token', - 'name', - { - command: 'rsufwdsnmpwalk', - rsu_ip: arg, - args: {}, - }, - '' - ) + expect(RsuApi.getRsuMsgFwdConfigs).toHaveBeenCalledWith('token', 'name', '', { rsu_ip }) expect(resp.payload).toEqual({ msgFwdConfig: 'test', errorState: '' }) - - RsuApi.postRsuData = jest.fn().mockReturnValue({ status: 400, body: { RsuFwdSnmpwalk: 'test' } }) - resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ msgFwdConfig: {}, errorState: 'test' }) }) it('Updates the state correctly pending', async () => { diff --git a/webapp/src/generalSlices/configSlice.ts b/webapp/src/generalSlices/configSlice.ts index fee6c63e..32a03e82 100644 --- a/webapp/src/generalSlices/configSlice.ts +++ b/webapp/src/generalSlices/configSlice.ts @@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import RsuApi from '../apis/rsu-api' import { selectToken, selectOrganizationName } from './userSlice' import { RootState } from '../store' -import { RsuCommandPostBody, SnmpFwdWalkConfig } from '../apis/rsu-api-types' +import { RsuCommandPostBody, RsuDsrcFwdConfigs, RsuRxTxMsgFwdConfigs, SnmpFwdWalkConfig } from '../apis/rsu-api-types' const initialState = { msgFwdConfig: {} as any, @@ -24,6 +24,22 @@ const initialState = { export const refreshSnmpFwdConfig = createAsyncThunk( 'config/refreshSnmpFwdConfig', + async (rsu_ip: string, { getState, dispatch }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + + const response = await RsuApi.getRsuMsgFwdConfigs(token, organization, '', { rsu_ip }) + + return { + msgFwdConfig: response.RsuFwdSnmpwalk, + errorState: '', + } + } +) + +export const refreshSnmpFwdConfigManual = createAsyncThunk( + 'config/refreshSnmpFwdConfigManual', async (ipList: string[], { getState, dispatch }) => { const currentState = getState() as RootState const token = selectToken(currentState) @@ -190,7 +206,7 @@ export const startFirmwareUpgrade = createAsyncThunk( export const geoRsuQuery = createAsyncThunk( 'config/geoRsuQuery', - async (_, { getState }) => { + async (vendor: string, { getState }) => { const currentState = getState() as RootState const token = selectToken(currentState) const organization = selectOrganizationName(currentState) @@ -202,6 +218,7 @@ export const geoRsuQuery = createAsyncThunk( organization, JSON.stringify({ geometry: configCoordinates, + vendor: vendor, }), '' ) @@ -254,9 +271,7 @@ export const configSlice = createSlice({ builder .addCase(refreshSnmpFwdConfig.pending, (state) => { state.loading = true - state.value.msgFwdConfig = {} as { - [id: string]: SnmpFwdWalkConfig - } + state.value.msgFwdConfig = {} as RsuDsrcFwdConfigs | RsuRxTxMsgFwdConfigs state.value.errorState = '' state.value.snmpFilterMsg = '' state.value.destIp = '' diff --git a/webapp/src/generalSlices/rsuSlice.test.ts b/webapp/src/generalSlices/rsuSlice.test.ts index 7f4114b1..ffe18fcb 100644 --- a/webapp/src/generalSlices/rsuSlice.test.ts +++ b/webapp/src/generalSlices/rsuSlice.test.ts @@ -1,1330 +1,1356 @@ -import reducer from './rsuSlice' -import { - // async thunks - getRsuData, - getRsuInfoOnly, - getRsuLastOnline, - _getRsuInfo, - _getRsuOnlineStatus, - _getRsuCounts, - _getRsuMapInfo, - getSsmSrmData, - getIssScmsStatus, - updateRowData, - updateBsmData, - getMapData, - - // functions - updateMessageType, - - // reducers - selectRsu, - toggleMapDisplay, - clearBsm, - toggleSsmSrmDisplay, - setSelectedSrm, - toggleBsmPointSelect, - updateBsmPoints, - updateBsmDate, - triggerBsmDateError, - changeMessageType, - setBsmFilter, - setBsmFilterStep, - setBsmFilterOffset, - setLoading, - - // selectors - selectLoading, - selectRequestOut, - selectSelectedRsu, - selectRsuManufacturer, - selectRsuIpv4, - selectRsuPrimaryRoute, - selectRsuData, - selectRsuOnlineStatus, - selectRsuCounts, - selectCountList, - selectCurrentSort, - selectStartDate, - selectEndDate, - selectMessageLoading, - selectWarningMessage, - selectMsgType, - selectRsuMapData, - selectMapList, - selectMapDate, - selectDisplayMap, - selectBsmStart, - selectBsmEnd, - selectAddBsmPoint, - selectBsmCoordinates, - selectBsmData, - selectBsmDateError, - selectBsmFilter, - selectBsmFilterStep, - selectBsmFilterOffset, - selectIssScmsStatusData, - selectSsmDisplay, - selectSrmSsmList, - selectSelectedSrm, - selectHeatMapData, -} from './rsuSlice' -import RsuApi from '../apis/rsu-api' -import { RootState } from '../store' - -describe('rsu reducer', () => { - it('should handle initial state', () => { - expect(reducer(undefined, { type: 'unknown' })).toEqual({ - loading: false, - requestOut: false, - value: { - selectedRsu: null, - rsuData: [], - rsuOnlineStatus: {}, - rsuCounts: {}, - countList: [], - currentSort: '', - startDate: '', - endDate: '', - heatMapData: { - features: [], - type: 'FeatureCollection', - }, - messageLoading: false, - warningMessage: false, - msgType: 'BSM', - rsuMapData: {}, - mapList: [], - mapDate: '', - displayMap: false, - bsmStart: '', - bsmEnd: '', - addBsmPoint: false, - bsmCoordinates: [], - bsmData: [], - bsmDateError: false, - bsmFilter: false, - bsmFilterStep: 60, - bsmFilterOffset: 0, - issScmsStatusData: {}, - ssmDisplay: false, - srmSsmList: [], - selectedSrm: [], - }, - }) - }) -}) - -describe('async thunks', () => { - const initialState: RootState['rsu'] = { - loading: null, - requestOut: null, - value: { - selectedRsu: null, - rsuData: null, - rsuOnlineStatus: null, - rsuCounts: null, - countList: null, - currentSort: null, - startDate: null, - endDate: null, - heatMapData: { - features: [], - type: 'FeatureCollection', - }, - messageLoading: null, - warningMessage: null, - msgType: null, - rsuMapData: null, - mapList: null, - mapDate: null, - displayMap: null, - bsmStart: null, - bsmEnd: null, - addBsmPoint: null, - bsmCoordinates: null, - bsmData: null, - bsmDateError: null, - bsmFilter: null, - bsmFilterStep: null, - bsmFilterOffset: null, - issScmsStatusData: null, - ssmDisplay: null, - srmSsmList: null, - selectedSrm: null, - }, - } - - beforeAll(() => { - jest.mock('../apis/rsu-api') - }) - - afterAll(() => { - jest.unmock('../apis/rsu-api') - }) - - describe('getRsuData', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - rsu: { - value: { - rsuOnlineStatus: {}, - startDate: '', - endDate: '', - }, - }, - }) - const action = getRsuData() - - await action(dispatch, getState, undefined) - expect(dispatch).toHaveBeenCalledTimes(4 + 2) // 4 for the 4 dispatched actions, 2 for the pending and fulfilled actions - }) - - it('Updates the state correctly pending', async () => { - let loading = true - let rsuData = [] as any - let rsuOnlineStatus = {} - let rsuCounts = {} - let countList = [] as any - const state = reducer(initialState, { - type: 'rsu/getRsuData/pending', - }) - expect(state).toEqual({ - ...initialState, - loading, - value: { - ...initialState.value, - rsuData, - rsuOnlineStatus, - rsuCounts, - countList, - }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - let loading = false - let rsuCounts = { ipv4_address: { count: 4 } } as any - let rsuData = [ - { - properties: { - ipv4_address: 'ipv4_address', - }, - geometry: { - coordinates: [-104.999824, 39.750392], - }, - }, - ] as any - const state = reducer( - { ...initialState, value: { ...initialState.value, rsuData, rsuCounts } }, - { - type: 'rsu/getRsuData/fulfilled', - } - ) - - let heatMapData = { - features: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [-104.999824, 39.750392], - }, - properties: { - ipv4_address: 'ipv4_address', - count: 4, - }, - }, - ], - type: 'FeatureCollection', - } - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value, rsuData, rsuCounts, heatMapData }, - }) - }) - - it('Updates the state correctly rejected', async () => { - let loading = false - const state = reducer(initialState, { - type: 'rsu/getRsuData/rejected', - }) - expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) - }) - }) - - describe('getRsuInfoOnly', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = getRsuInfoOnly() - - const rsuData = ['1.1.1.1'] - RsuApi.getRsuInfo = jest.fn().mockReturnValue({ rsuList: rsuData }) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual(rsuData) - expect(RsuApi.getRsuInfo).toHaveBeenCalledWith('token', 'name') - }) - - it('Updates the state correctly pending', async () => { - let loading = true - const state = reducer(initialState, { - type: 'rsu/getRsuInfoOnly/pending', - }) - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - let loading = false - const state = reducer(initialState, { - type: 'rsu/getRsuInfoOnly/fulfilled', - }) - expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) - }) - - it('Updates the state correctly rejected', async () => { - let loading = false - const state = reducer(initialState, { - type: 'rsu/getRsuInfoOnly/rejected', - }) - expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) - }) - }) - - describe('getRsuLastOnline', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const rsu_ip = '1.1.1.1' - const action = getRsuLastOnline(rsu_ip) - - RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsu_ip) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual(rsu_ip) - expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name', '', { rsu_ip }) - }) - - it('Updates the state correctly pending', async () => { - let loading = true - const state = reducer(initialState, { - type: 'rsu/getRsuLastOnline/pending', - }) - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - let loading = false - let rsuOnlineStatus = { '1.1.1.1': {} as any } - const payload = { last_online: '2021-03-01T00:00:00.000000Z', ip: '1.1.1.1' } - const state = reducer( - { - ...initialState, - value: { ...initialState.value, rsuOnlineStatus }, - }, - { - type: 'rsu/getRsuLastOnline/fulfilled', - payload: payload, - } - ) - - rsuOnlineStatus = { '1.1.1.1': { last_online: '2021-03-01T00:00:00.000000Z' } } - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value, rsuOnlineStatus }, - }) - }) - - it('Updates the state correctly rejected', async () => { - let loading = false - const state = reducer(initialState, { - type: 'rsu/getRsuLastOnline/rejected', - }) - expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) - }) - }) - - describe('_getRsuInfo', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = _getRsuInfo() - - const rsuList = ['1.1.1.1'] - RsuApi.getRsuInfo = jest.fn().mockReturnValue({ rsuList }) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual(rsuList) - expect(RsuApi.getRsuInfo).toHaveBeenCalledWith('token', 'name') - }) - - it('Updates the state correctly fulfilled', async () => { - const rsuData = 'rsuData' - const state = reducer(initialState, { - type: 'rsu/_getRsuInfo/fulfilled', - payload: rsuData, - }) - expect(state).toEqual({ ...initialState, value: { ...initialState.value, rsuData } }) - }) - }) - - describe('_getRsuOnlineStatus', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = _getRsuOnlineStatus({ - rsuOnlineStatusState: 'rsuOnlineStatusState', - } as any) - - const rsuOnlineStatus = 'rsuOnlineStatus' - RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsuOnlineStatus) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual(rsuOnlineStatus) - expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name') - }) - - it('returns and calls the api correctly default value', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = _getRsuOnlineStatus('rsuOnlineStatusState' as any) - - const rsuOnlineStatus = null as any - RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsuOnlineStatus) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual('rsuOnlineStatusState') - expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name') - }) - - it('Updates the state correctly fulfilled', async () => { - const rsuOnlineStatus = 'rsuOnlineStatus' - const state = reducer(initialState, { - type: 'rsu/_getRsuOnlineStatus/fulfilled', - payload: rsuOnlineStatus, - }) - expect(state).toEqual({ ...initialState, value: { ...initialState.value, rsuOnlineStatus } }) - }) - }) - - describe('_getRsuCounts', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - rsu: { - value: { - msgType: 'BSM', - startDate: '', - endDate: '', - }, - }, - }) - const action = _getRsuCounts() - - const rsuCounts = { - '1.1.1.1': { road: 'road', count: 'count' }, - } - const countList = [ - { - key: '1.1.1.1', - rsu: '1.1.1.1', - road: 'road', - count: 'count', - }, - ] - RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ rsuCounts, countList }) - expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { - message: 'BSM', - start: '', - end: '', - }) - }) - it('returns and calls the api correctly', async () => { - const rsuCounts = { - '1.1.1.1': { road: 'road', count: 'count' }, - } - - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - rsu: { - value: { - msgType: 'BSM', - startDate: '', - endDate: '', - rsuCounts, - }, - }, - }) - - const action = _getRsuCounts() - const countList = [ - { - key: '1.1.1.1', - rsu: '1.1.1.1', - road: 'road', - count: 'count', - }, - ] - RsuApi.getRsuCounts = jest.fn().mockReturnValue(null) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ rsuCounts, countList }) - expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { - message: 'BSM', - start: '', - end: '', - }) - }) - - it('Updates the state correctly fulfilled', async () => { - let rsuCounts = 'rsuCounts' - let countList = 'countList' - const payload = { rsuCounts, countList } - const state = reducer(initialState, { - type: 'rsu/_getRsuCounts/fulfilled', - payload: payload, - }) - - expect(state).toEqual({ - ...initialState, - value: { ...initialState.value, rsuCounts, countList }, - }) - }) - }) - - describe('_getRsuMapInfo', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = _getRsuMapInfo({ - startDate: 'startDate', - endDate: 'endDate', - }) - - RsuApi.getRsuMapInfo = jest.fn().mockReturnValue('rsuMapData') - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ endDate: 'endDate', rsuMapData: 'rsuMapData', startDate: 'startDate' }) - expect(RsuApi.getRsuMapInfo).toHaveBeenCalledWith('token', 'name', '', { ip_list: 'True' }) - }) - - it('Updates the state correctly fulfilled', async () => { - const startDate = 'startDate' - const endDate = 'endDate' - const mapList = 'mapList' - const payload = { startDate, endDate, rsuMapData: mapList } - const state = reducer(initialState, { - type: 'rsu/_getRsuMapInfo/fulfilled', - payload: payload, - }) - - expect(state).toEqual({ - ...initialState, - value: { ...initialState.value, startDate, endDate, mapList }, - }) - }) - }) - - describe('getSsmSrmData', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - }, - }, - }) - const action = getSsmSrmData() - - RsuApi.getSsmSrmData = jest.fn().mockReturnValue('srmSsmList') - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual('srmSsmList') - expect(RsuApi.getSsmSrmData).toHaveBeenCalledWith('token') - }) - - it('Updates the state correctly fulfilled', async () => { - const srmSsmList = 'srmSsmList' - const state = reducer(initialState, { - type: 'rsu/getSsmSrmData/fulfilled', - payload: srmSsmList, - }) - - expect(state).toEqual({ - ...initialState, - value: { ...initialState.value, srmSsmList }, - }) - }) - }) - - describe('getIssScmsStatus', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const action = getIssScmsStatus() - - RsuApi.getIssScmsStatus = jest.fn().mockReturnValue('issScmsStatus') - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual('issScmsStatus') - expect(RsuApi.getIssScmsStatus).toHaveBeenCalledWith('token', 'name') - }) - - it('Updates the state correctly fulfilled', async () => { - const issScmsStatusData = 'issScmsStatus' - const state = reducer(initialState, { - type: 'rsu/getIssScmsStatus/fulfilled', - payload: issScmsStatusData, - }) - - expect(state).toEqual({ - ...initialState, - value: { ...initialState.value, issScmsStatusData }, - }) - }) - - it('Updates the state correctly fulfilled default value', async () => { - const issScmsStatusData = 'issScmsStatus' as any - const state = reducer( - { ...initialState, value: { ...initialState.value, issScmsStatusData } }, - { - type: 'rsu/getIssScmsStatus/fulfilled', - payload: null, - } - ) - - expect(state).toEqual({ - ...initialState, - value: { ...initialState.value, issScmsStatusData }, - }) - }) - }) - - describe('updateRowData', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - }) - const data = { - message: 'message', - start: 1, - end: 86400000, - } - const action = updateRowData(data as any) - - const rsuCounts = { - '1.1.1.1': { road: 'road', count: 'count' }, - } - const countList = [ - { - key: '1.1.1.1', - rsu: '1.1.1.1', - road: 'road', - count: 'count', - }, - ] - RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ - msgType: 'message', - startDate: 1, - endDate: 86400000, - warningMessage: false, - rsuCounts, - countList, - }) - expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', data) - }) - - it('returns and calls the api correctly default values', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - rsu: { - value: { - msgType: 'message', - startDate: 1, - endDate: 86400002, - }, - }, - }) - const data = {} - const action = updateRowData(data) - - const rsuCounts = { - '1.1.1.1': { road: 'road', count: 'count' }, - } - const countList = [ - { - key: '1.1.1.1', - rsu: '1.1.1.1', - road: 'road', - count: 'count', - }, - ] - RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ - msgType: 'message', - startDate: 1, - endDate: 86400002, - warningMessage: true, - rsuCounts, - countList, - }) - expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { - message: 'message', - start: 1, - end: 86400002, - }) - }) - - it('Updates the state correctly pending', async () => { - const requestOut = true - const messageLoading = false - const state = reducer(initialState, { - type: 'rsu/updateRowData/pending', - }) - - expect(state).toEqual({ - ...initialState, - requestOut, - value: { ...initialState.value, messageLoading }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - const rsuCounts = { '1.1.1.1': { count: 5 } } - const countList = 'countList' - const heatMapData = { - type: 'FeatureCollection', - features: [ - { - properties: { - ipv4_address: '1.1.1.1', - }, - }, - { - properties: { - ipv4_address: '1.1.1.2', - }, - }, - ], - } as any - const warningMessage = 'warningMessage' - const requestOut = false - const messageLoading = false - const msgType = 'msgType' - const startDate = 'startDate' - const endDate = 'endDate' - const payload = { - rsuCounts, - countList, - warningMessage, - msgType, - startDate, - endDate, - } - const state = reducer( - { - ...initialState, - value: { - ...initialState.value, - heatMapData, - }, - }, - { - type: 'rsu/updateRowData/fulfilled', - payload: payload, - } - ) - - heatMapData['features'][0]['properties']['count'] = 5 - heatMapData['features'][1]['properties']['count'] = 0 - - expect(state).toEqual({ - ...initialState, - requestOut, - value: { - ...initialState.value, - rsuCounts, - countList, - heatMapData, - warningMessage, - messageLoading, - msgType, - startDate, - endDate, - }, - }) - }) - - it('Updates the state correctly rejected', async () => { - const requestOut = false - const messageLoading = false - const state = reducer(initialState, { - type: 'rsu/updateRowData/rejected', - }) - - expect(state).toEqual({ - ...initialState, - requestOut, - value: { ...initialState.value, messageLoading }, - }) - }) - }) - - describe('updateBsmData', () => { - it('returns and calls the api correctly', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - }, - }, - rsu: { - value: { - bsmStart: 'bsmStart', - bsmEnd: 'bsmEnd', - bsmCoordinates: [1, 2, 3], - }, - }, - }) - const action = updateBsmData() - - RsuApi.postBsmData = jest.fn().mockReturnValue('bsmCounts') - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual('bsmCounts') - expect(RsuApi.postBsmData).toHaveBeenCalledWith( - 'token', - { - start: 'bsmStart', - end: 'bsmEnd', - geometry: [1, 2, 3], - }, - '' - ) - }) - - it('condition blocks execution', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - }, - }, - rsu: { - value: { - bsmStart: '', - bsmEnd: '', - bsmCoordinates: [1, 2], - }, - }, - }) - const action = updateBsmData() - - RsuApi.postBsmData = jest.fn().mockReturnValue('bsmCounts') - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual(undefined) - expect(RsuApi.postBsmData).not.toHaveBeenCalled() - }) - - it('Updates the state correctly pending', async () => { - const addBsmPoint = false - const loading = true - const bsmStart = 1 as any - const bsmEnd = 86400000 as any - const bsmDateError = false - const state = reducer( - { - ...initialState, - value: { ...initialState.value, bsmStart, bsmEnd }, - }, - { - type: 'rsu/updateBsmData/pending', - } - ) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value, addBsmPoint, bsmDateError, bsmStart, bsmEnd }, - }) - }) - - it('Updates the state correctly pending date error', async () => { - const addBsmPoint = false - const loading = true - const bsmStart = 1 as any - const bsmEnd = 86400002 as any - const bsmDateError = true - const state = reducer( - { - ...initialState, - value: { ...initialState.value, bsmStart, bsmEnd }, - }, - { - type: 'rsu/updateBsmData/pending', - } - ) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value, addBsmPoint, bsmDateError, bsmStart, bsmEnd }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - const bsmData = 'bsmData' - const loading = false - const bsmFilter = true - const bsmFilterStep = 60 - const bsmFilterOffset = 0 - const state = reducer(initialState, { - type: 'rsu/updateBsmData/fulfilled', - payload: { body: bsmData }, - }) - - expect(state).toEqual({ - ...initialState, - loading, - value: { - ...initialState.value, - bsmData, - bsmFilter, - bsmFilterStep, - bsmFilterOffset, - }, - }) - }) - - it('Updates the state correctly rejected', async () => { - const loading = false - const state = reducer(initialState, { - type: 'rsu/updateBsmData/rejected', - }) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) - }) - - describe('getMapData', () => { - it('condition blocks execution', async () => { - const dispatch = jest.fn() - const getState = jest.fn().mockReturnValue({ - user: { - value: { - authLoginData: { token: 'token' }, - organization: { name: 'name' }, - }, - }, - rsu: { - value: { - selectedRsu: { properties: { ipv4_address: '1.1.1.1' } }, - }, - }, - }) - const action = getMapData() - - RsuApi.getRsuMapInfo = jest.fn().mockReturnValue({ geojson: 'geojson', date: 'date' }) - let resp = await action(dispatch, getState, undefined) - expect(resp.payload).toEqual({ - rsuMapData: 'geojson', - mapDate: 'date', - }) - expect(RsuApi.getRsuMapInfo).toHaveBeenCalledWith('token', 'name', '', { ip_address: '1.1.1.1' }) - }) - - it('Updates the state correctly pending', async () => { - const loading = true - const state = reducer(initialState, { - type: 'rsu/getMapData/pending', - }) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) - - it('Updates the state correctly fulfilled', async () => { - const loading = false - const rsuMapData = 'rsuMapData' - const mapDate = 'mapDate' - const state = reducer(initialState, { - type: 'rsu/getMapData/fulfilled', - payload: { rsuMapData, mapDate }, - }) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value, rsuMapData, mapDate }, - }) - }) - - it('Updates the state correctly rejected', async () => { - const loading = false - const state = reducer(initialState, { - type: 'rsu/getMapData/rejected', - }) - - expect(state).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) - }) -}) - -describe('functions', () => { - it('updateMessageType', async () => { - const dispatch = jest.fn() - - updateMessageType('messageType' as any)(dispatch) - expect(dispatch).toHaveBeenCalledTimes(2) - }) -}) - -describe('reducers', () => { - const initialState: RootState['rsu'] = { - loading: null, - requestOut: null, - value: { - selectedRsu: null, - rsuData: null, - rsuOnlineStatus: null, - rsuCounts: null, - countList: null, - currentSort: null, - startDate: null, - endDate: null, - heatMapData: { - features: [], - type: 'FeatureCollection', - }, - messageLoading: null, - warningMessage: null, - msgType: null, - rsuMapData: null, - mapList: null, - mapDate: null, - displayMap: null, - bsmStart: null, - bsmEnd: null, - addBsmPoint: null, - bsmCoordinates: null, - bsmData: null, - bsmDateError: null, - bsmFilter: null, - bsmFilterStep: null, - bsmFilterOffset: null, - issScmsStatusData: null, - ssmDisplay: null, - srmSsmList: null, - selectedSrm: null, - }, - } - - it('selectRsu reducer updates state correctly', async () => { - const selectedRsu = 'selectedRsu' - expect(reducer(initialState, selectRsu(selectedRsu))).toEqual({ - ...initialState, - value: { ...initialState.value, selectedRsu }, - }) - }) - - it('toggleMapDisplay reducer updates state correctly', async () => { - expect( - reducer({ ...initialState, value: { ...initialState.value, displayMap: true } }, toggleMapDisplay()) - ).toEqual({ - ...initialState, - value: { ...initialState.value, displayMap: false }, - }) - }) - - it('clearBsm reducer updates state correctly', async () => { - expect(reducer(initialState, clearBsm())).toEqual({ - ...initialState, - value: { ...initialState.value, bsmCoordinates: [], bsmData: [], bsmStart: '', bsmEnd: '', bsmDateError: false }, - }) - }) - - it('toggleSsmSrmDisplay reducer updates state correctly', async () => { - expect( - reducer({ ...initialState, value: { ...initialState.value, ssmDisplay: true } }, toggleSsmSrmDisplay()) - ).toEqual({ - ...initialState, - value: { ...initialState.value, ssmDisplay: false }, - }) - }) - - it('setSelectedSrm reducer updates state correctly', async () => { - let selectedSrm = { selectedSrm: 1 } - expect(reducer(initialState, setSelectedSrm(selectedSrm))).toEqual({ - ...initialState, - value: { ...initialState.value, selectedSrm: [selectedSrm] }, - }) - - expect(reducer(initialState, setSelectedSrm({}))).toEqual({ - ...initialState, - value: { ...initialState.value, selectedSrm: [] }, - }) - }) - - it('toggleBsmPointSelect reducer updates state correctly', async () => { - expect( - reducer({ ...initialState, value: { ...initialState.value, addBsmPoint: true } }, toggleBsmPointSelect()) - ).toEqual({ - ...initialState, - value: { ...initialState.value, addBsmPoint: false }, - }) - }) - - it('updateBsmPoints reducer updates state correctly', async () => { - const bsmCoordinates = 'bsmCoordinates' - expect(reducer(initialState, updateBsmPoints(bsmCoordinates))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmCoordinates }, - }) - }) - - it('updateBsmDate reducer updates state correctly', async () => { - let type = 'start' - const date = 'date' - expect(reducer(initialState, updateBsmDate({ type, date }))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmStart: 'date' }, - }) - - type = 'end' - expect(reducer(initialState, updateBsmDate({ type, date }))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmEnd: 'date' }, - }) - }) - - it('triggerBsmDateError reducer updates state correctly', async () => { - expect(reducer(initialState, triggerBsmDateError())).toEqual({ - ...initialState, - value: { ...initialState.value, bsmDateError: true }, - }) - }) - - it('changeMessageType reducer updates state correctly', async () => { - const msgType = 'msgType' - expect(reducer(initialState, changeMessageType(msgType))).toEqual({ - ...initialState, - value: { ...initialState.value, msgType }, - }) - }) - - it('setBsmFilter reducer updates state correctly', async () => { - const bsmFilter = 'bsmFilter' - expect(reducer(initialState, setBsmFilter(bsmFilter))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmFilter }, - }) - }) - - it('setBsmFilterStep reducer updates state correctly', async () => { - const bsmFilterStep = 'bsmFilterStep' - expect(reducer(initialState, setBsmFilterStep(bsmFilterStep))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmFilterStep }, - }) - }) - - it('setBsmFilterOffset reducer updates state correctly', async () => { - const bsmFilterOffset = 'bsmFilterOffset' - expect(reducer(initialState, setBsmFilterOffset(bsmFilterOffset))).toEqual({ - ...initialState, - value: { ...initialState.value, bsmFilterOffset }, - }) - }) - - it('setLoading reducer updates state correctly', async () => { - const loading = 'loading' - expect(reducer(initialState, setLoading(loading))).toEqual({ - ...initialState, - loading, - value: { ...initialState.value }, - }) - }) -}) - -describe('selectors', () => { - const initialState = { - loading: 'loading', - requestOut: 'requestOut', - value: { - selectedRsu: { - properties: { - manufacturer_name: 'manufacturer_name', - ipv4_address: 'ipv4_address', - primary_route: 'primary_route', - }, - }, - rsuData: 'rsuData', - rsuOnlineStatus: 'rsuOnlineStatus', - rsuCounts: 'rsuCounts', - countList: 'countList', - currentSort: 'currentSort', - startDate: 'startDate', - endDate: 'endDate', - heatMapData: 'heatMapData', - messageLoading: 'messageLoading', - warningMessage: 'warningMessage', - msgType: 'msgType', - rsuMapData: 'rsuMapData', - mapList: 'mapList', - mapDate: 'mapDate', - displayMap: 'displayMap', - bsmStart: 'bsmStart', - bsmEnd: 'bsmEnd', - addBsmPoint: 'addBsmPoint', - bsmCoordinates: 'bsmCoordinates', - bsmData: 'bsmData', - bsmDateError: 'bsmDateError', - bsmFilter: 'bsmFilter', - bsmFilterStep: 'bsmFilterStep', - bsmFilterOffset: 'bsmFilterOffset', - issScmsStatusData: 'issScmsStatusData', - ssmDisplay: 'ssmDisplay', - srmSsmList: 'srmSsmList', - selectedSrm: 'selectedSrm', - }, - } - const rsuState = { rsu: initialState } as any - - it('selectors return the correct value', async () => { - expect(selectLoading(rsuState)).toEqual('loading') - expect(selectRequestOut(rsuState)).toEqual('requestOut') - - expect(selectSelectedRsu(rsuState)).toEqual(initialState.value.selectedRsu) - expect(selectRsuManufacturer(rsuState)).toEqual('manufacturer_name') - expect(selectRsuIpv4(rsuState)).toEqual('ipv4_address') - expect(selectRsuPrimaryRoute(rsuState)).toEqual('primary_route') - expect(selectRsuData(rsuState)).toEqual('rsuData') - expect(selectRsuOnlineStatus(rsuState)).toEqual('rsuOnlineStatus') - expect(selectRsuCounts(rsuState)).toEqual('rsuCounts') - expect(selectCountList(rsuState)).toEqual('countList') - expect(selectCurrentSort(rsuState)).toEqual('currentSort') - expect(selectStartDate(rsuState)).toEqual('startDate') - expect(selectEndDate(rsuState)).toEqual('endDate') - expect(selectMessageLoading(rsuState)).toEqual('messageLoading') - expect(selectWarningMessage(rsuState)).toEqual('warningMessage') - expect(selectMsgType(rsuState)).toEqual('msgType') - expect(selectRsuMapData(rsuState)).toEqual('rsuMapData') - expect(selectMapList(rsuState)).toEqual('mapList') - expect(selectMapDate(rsuState)).toEqual('mapDate') - expect(selectDisplayMap(rsuState)).toEqual('displayMap') - expect(selectBsmStart(rsuState)).toEqual('bsmStart') - expect(selectBsmEnd(rsuState)).toEqual('bsmEnd') - expect(selectAddBsmPoint(rsuState)).toEqual('addBsmPoint') - expect(selectBsmCoordinates(rsuState)).toEqual('bsmCoordinates') - expect(selectBsmData(rsuState)).toEqual('bsmData') - expect(selectBsmDateError(rsuState)).toEqual('bsmDateError') - expect(selectBsmFilter(rsuState)).toEqual('bsmFilter') - expect(selectBsmFilterStep(rsuState)).toEqual('bsmFilterStep') - expect(selectBsmFilterOffset(rsuState)).toEqual('bsmFilterOffset') - expect(selectIssScmsStatusData(rsuState)).toEqual('issScmsStatusData') - expect(selectSsmDisplay(rsuState)).toEqual('ssmDisplay') - expect(selectSrmSsmList(rsuState)).toEqual('srmSsmList') - expect(selectSelectedSrm(rsuState)).toEqual('selectedSrm') - expect(selectHeatMapData(rsuState)).toEqual('heatMapData') - }) -}) +import reducer from './rsuSlice' +import { + // async thunks + getRsuData, + getRsuInfoOnly, + getRsuLastOnline, + _getRsuInfo, + _getRsuOnlineStatus, + _getRsuCounts, + _getRsuMapInfo, + getSsmSrmData, + getIssScmsStatus, + updateRowData, + updateGeoMsgData, + getMapData, + + // functions + updateMessageType, + + // reducers + selectRsu, + toggleMapDisplay, + clearGeoMsg, + toggleSsmSrmDisplay, + setSelectedSrm, + toggleGeoMsgPointSelect, + updateGeoMsgPoints, + updateGeoMsgDate, + triggerGeoMsgDateError, + changeCountsMsgType, + setGeoMsgFilter, + setGeoMsgFilterStep, + setGeoMsgFilterOffset, + setLoading, + + // selectors + selectLoading, + selectRequestOut, + selectSelectedRsu, + selectRsuManufacturer, + selectRsuIpv4, + selectRsuPrimaryRoute, + selectRsuData, + selectRsuOnlineStatus, + selectRsuCounts, + selectCountList, + selectCurrentSort, + selectStartDate, + selectEndDate, + selectMessageLoading, + selectWarningMessage, + selectMsgType, + selectRsuMapData, + selectMapList, + selectMapDate, + selectDisplayMap, + selectGeoMsgStart, + selectGeoMsgEnd, + selectAddGeoMsgPoint, + selectGeoMsgCoordinates, + selectGeoMsgData, + selectGeoMsgDateError, + selectGeoMsgFilter, + selectGeoMsgFilterStep, + selectGeoMsgFilterOffset, + selectIssScmsStatusData, + selectSsmDisplay, + selectSrmSsmList, + selectSelectedSrm, + selectHeatMapData, +} from './rsuSlice' +import RsuApi from '../apis/rsu-api' +import { RootState } from '../store' + +// Mock luxon to return a fixed date time to make the tests deterministic +jest.mock('luxon', () => { + const actualLuxon = jest.requireActual('luxon') + return { + ...actualLuxon, + DateTime: { + ...actualLuxon.DateTime, + local: () => actualLuxon.DateTime.fromISO('2024-04-01T00:00:00.000-06:00'), + }, + } +}) + +const { DateTime } = require('luxon') +const currentDate = DateTime.local().setZone(DateTime.local().zoneName) + +describe('rsu reducer', () => { + it('should handle initial state', () => { + expect(reducer(undefined, { type: 'unknown' })).toEqual({ + loading: false, + requestOut: false, + value: { + selectedRsu: null, + rsuData: [], + rsuOnlineStatus: {}, + rsuCounts: {}, + countList: [], + currentSort: '', + startDate: '', + endDate: '', + heatMapData: { + features: [], + type: 'FeatureCollection', + }, + messageLoading: false, + warningMessage: false, + countsMsgType: 'BSM', + rsuMapData: {}, + mapList: [], + mapDate: '', + displayMap: false, + geoMsgType: 'BSM', + geoMsgStart: currentDate.minus({ days: 1 }).toString(), + geoMsgEnd: currentDate.toString(), + addGeoMsgPoint: false, + geoMsgCoordinates: [], + geoMsgData: [], + geoMsgDateError: false, + geoMsgFilter: false, + geoMsgFilterStep: 60, + geoMsgFilterOffset: 0, + issScmsStatusData: {}, + ssmDisplay: false, + srmSsmList: [], + selectedSrm: [], + }, + }) + }) +}) + +describe('async thunks', () => { + const initialState: RootState['rsu'] = { + loading: null, + requestOut: null, + value: { + selectedRsu: null, + rsuData: null, + rsuOnlineStatus: null, + rsuCounts: null, + countList: null, + currentSort: null, + startDate: null, + endDate: null, + heatMapData: { + features: [], + type: 'FeatureCollection', + }, + messageLoading: null, + warningMessage: null, + countsMsgType: null, + geoMsgType: null, + rsuMapData: null, + mapList: null, + mapDate: null, + displayMap: null, + geoMsgStart: null, + geoMsgEnd: null, + addGeoMsgPoint: null, + geoMsgCoordinates: null, + geoMsgData: null, + geoMsgDateError: null, + geoMsgFilter: null, + geoMsgFilterStep: null, + geoMsgFilterOffset: null, + issScmsStatusData: null, + ssmDisplay: null, + srmSsmList: null, + selectedSrm: null, + }, + } + + beforeAll(() => { + jest.mock('../apis/rsu-api') + }) + + afterAll(() => { + jest.unmock('../apis/rsu-api') + }) + + describe('getRsuData', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + rsu: { + value: { + rsuOnlineStatus: {}, + startDate: '', + endDate: '', + }, + }, + }) + const action = getRsuData() + + await action(dispatch, getState, undefined) + expect(dispatch).toHaveBeenCalledTimes(4 + 2) // 4 for the 4 dispatched actions, 2 for the pending and fulfilled actions + }) + + it('Updates the state correctly pending', async () => { + let loading = true + let rsuData = [] as any + let rsuOnlineStatus = {} + let rsuCounts = {} + let countList = [] as any + const state = reducer(initialState, { + type: 'rsu/getRsuData/pending', + }) + expect(state).toEqual({ + ...initialState, + loading, + value: { + ...initialState.value, + rsuData, + rsuOnlineStatus, + rsuCounts, + countList, + }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + let loading = false + let rsuCounts = { ipv4_address: { count: 4 } } as any + let rsuData = [ + { + properties: { + ipv4_address: 'ipv4_address', + }, + geometry: { + coordinates: [-104.999824, 39.750392], + }, + }, + ] as any + const state = reducer( + { ...initialState, value: { ...initialState.value, rsuData, rsuCounts } }, + { + type: 'rsu/getRsuData/fulfilled', + } + ) + + let heatMapData = { + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-104.999824, 39.750392], + }, + properties: { + ipv4_address: 'ipv4_address', + count: 4, + }, + }, + ], + type: 'FeatureCollection', + } + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value, rsuData, rsuCounts, heatMapData }, + }) + }) + + it('Updates the state correctly rejected', async () => { + let loading = false + const state = reducer(initialState, { + type: 'rsu/getRsuData/rejected', + }) + expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) + }) + }) + + describe('getRsuInfoOnly', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = getRsuInfoOnly() + + const rsuData = ['1.1.1.1'] + RsuApi.getRsuInfo = jest.fn().mockReturnValue({ rsuList: rsuData }) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual(rsuData) + expect(RsuApi.getRsuInfo).toHaveBeenCalledWith('token', 'name') + }) + + it('Updates the state correctly pending', async () => { + let loading = true + const state = reducer(initialState, { + type: 'rsu/getRsuInfoOnly/pending', + }) + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + let loading = false + const state = reducer(initialState, { + type: 'rsu/getRsuInfoOnly/fulfilled', + }) + expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) + }) + + it('Updates the state correctly rejected', async () => { + let loading = false + const state = reducer(initialState, { + type: 'rsu/getRsuInfoOnly/rejected', + }) + expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) + }) + }) + + describe('getRsuLastOnline', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const rsu_ip = '1.1.1.1' + const action = getRsuLastOnline(rsu_ip) + + RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsu_ip) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual(rsu_ip) + expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name', '', { rsu_ip }) + }) + + it('Updates the state correctly pending', async () => { + let loading = true + const state = reducer(initialState, { + type: 'rsu/getRsuLastOnline/pending', + }) + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + let loading = false + let rsuOnlineStatus = { '1.1.1.1': {} as any } + const payload = { last_online: '2021-03-01T00:00:00.000000Z', ip: '1.1.1.1' } + const state = reducer( + { + ...initialState, + value: { ...initialState.value, rsuOnlineStatus }, + }, + { + type: 'rsu/getRsuLastOnline/fulfilled', + payload: payload, + } + ) + + rsuOnlineStatus = { '1.1.1.1': { last_online: '2021-03-01T00:00:00.000000Z' } } + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value, rsuOnlineStatus }, + }) + }) + + it('Updates the state correctly rejected', async () => { + let loading = false + const state = reducer(initialState, { + type: 'rsu/getRsuLastOnline/rejected', + }) + expect(state).toEqual({ ...initialState, loading, value: { ...initialState.value } }) + }) + }) + + describe('_getRsuInfo', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = _getRsuInfo() + + const rsuList = ['1.1.1.1'] + RsuApi.getRsuInfo = jest.fn().mockReturnValue({ rsuList }) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual(rsuList) + expect(RsuApi.getRsuInfo).toHaveBeenCalledWith('token', 'name') + }) + + it('Updates the state correctly fulfilled', async () => { + const rsuData = 'rsuData' + const state = reducer(initialState, { + type: 'rsu/_getRsuInfo/fulfilled', + payload: rsuData, + }) + expect(state).toEqual({ ...initialState, value: { ...initialState.value, rsuData } }) + }) + }) + + describe('_getRsuOnlineStatus', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = _getRsuOnlineStatus({ + rsuOnlineStatusState: 'rsuOnlineStatusState', + } as any) + + const rsuOnlineStatus = 'rsuOnlineStatus' + RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsuOnlineStatus) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual(rsuOnlineStatus) + expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name') + }) + + it('returns and calls the api correctly default value', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = _getRsuOnlineStatus('rsuOnlineStatusState' as any) + + const rsuOnlineStatus = null as any + RsuApi.getRsuOnline = jest.fn().mockReturnValue(rsuOnlineStatus) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual('rsuOnlineStatusState') + expect(RsuApi.getRsuOnline).toHaveBeenCalledWith('token', 'name') + }) + + it('Updates the state correctly fulfilled', async () => { + const rsuOnlineStatus = 'rsuOnlineStatus' + const state = reducer(initialState, { + type: 'rsu/_getRsuOnlineStatus/fulfilled', + payload: rsuOnlineStatus, + }) + expect(state).toEqual({ ...initialState, value: { ...initialState.value, rsuOnlineStatus } }) + }) + }) + + describe('_getRsuCounts', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + rsu: { + value: { + countsMsgType: 'BSM', + startDate: '', + endDate: '', + }, + }, + }) + const action = _getRsuCounts() + + const rsuCounts = { + '1.1.1.1': { road: 'road', count: 'count' }, + } + const countList = [ + { + key: '1.1.1.1', + rsu: '1.1.1.1', + road: 'road', + count: 'count', + }, + ] + RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ rsuCounts, countList }) + expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { + message: 'BSM', + start: '', + end: '', + }) + }) + it('returns and calls the api correctly', async () => { + const rsuCounts = { + '1.1.1.1': { road: 'road', count: 'count' }, + } + + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + rsu: { + value: { + countsMsgType: 'BSM', + startDate: '', + endDate: '', + rsuCounts, + }, + }, + }) + + const action = _getRsuCounts() + const countList = [ + { + key: '1.1.1.1', + rsu: '1.1.1.1', + road: 'road', + count: 'count', + }, + ] + RsuApi.getRsuCounts = jest.fn().mockReturnValue(null) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ rsuCounts, countList }) + expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { + message: 'BSM', + start: '', + end: '', + }) + }) + + it('Updates the state correctly fulfilled', async () => { + let rsuCounts = 'rsuCounts' + let countList = 'countList' + const payload = { rsuCounts, countList } + const state = reducer(initialState, { + type: 'rsu/_getRsuCounts/fulfilled', + payload: payload, + }) + + expect(state).toEqual({ + ...initialState, + value: { ...initialState.value, rsuCounts, countList }, + }) + }) + }) + + describe('_getRsuMapInfo', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = _getRsuMapInfo({ + startDate: 'startDate', + endDate: 'endDate', + }) + + RsuApi.getRsuMapInfo = jest.fn().mockReturnValue('rsuMapData') + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ endDate: 'endDate', rsuMapData: 'rsuMapData', startDate: 'startDate' }) + expect(RsuApi.getRsuMapInfo).toHaveBeenCalledWith('token', 'name', '', { ip_list: 'True' }) + }) + + it('Updates the state correctly fulfilled', async () => { + const startDate = 'startDate' + const endDate = 'endDate' + const mapList = 'mapList' + const payload = { startDate, endDate, rsuMapData: mapList } + const state = reducer(initialState, { + type: 'rsu/_getRsuMapInfo/fulfilled', + payload: payload, + }) + + expect(state).toEqual({ + ...initialState, + value: { ...initialState.value, startDate, endDate, mapList }, + }) + }) + }) + + describe('getSsmSrmData', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + }, + }, + }) + const action = getSsmSrmData() + + RsuApi.getSsmSrmData = jest.fn().mockReturnValue('srmSsmList') + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual('srmSsmList') + expect(RsuApi.getSsmSrmData).toHaveBeenCalledWith('token') + }) + + it('Updates the state correctly fulfilled', async () => { + const srmSsmList = 'srmSsmList' + const state = reducer(initialState, { + type: 'rsu/getSsmSrmData/fulfilled', + payload: srmSsmList, + }) + + expect(state).toEqual({ + ...initialState, + value: { ...initialState.value, srmSsmList }, + }) + }) + }) + + describe('getIssScmsStatus', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const action = getIssScmsStatus() + + RsuApi.getIssScmsStatus = jest.fn().mockReturnValue('issScmsStatus') + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual('issScmsStatus') + expect(RsuApi.getIssScmsStatus).toHaveBeenCalledWith('token', 'name') + }) + + it('Updates the state correctly fulfilled', async () => { + const issScmsStatusData = 'issScmsStatus' + const state = reducer(initialState, { + type: 'rsu/getIssScmsStatus/fulfilled', + payload: issScmsStatusData, + }) + + expect(state).toEqual({ + ...initialState, + value: { ...initialState.value, issScmsStatusData }, + }) + }) + + it('Updates the state correctly fulfilled default value', async () => { + const issScmsStatusData = 'issScmsStatus' as any + const state = reducer( + { ...initialState, value: { ...initialState.value, issScmsStatusData } }, + { + type: 'rsu/getIssScmsStatus/fulfilled', + payload: null, + } + ) + + expect(state).toEqual({ + ...initialState, + value: { ...initialState.value, issScmsStatusData }, + }) + }) + }) + + describe('updateRowData', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + }) + const data = { + message: 'message', + start: 1, + end: 86400000, + } + const action = updateRowData(data as any) + + const rsuCounts = { + '1.1.1.1': { road: 'road', count: 'count' }, + } + const countList = [ + { + key: '1.1.1.1', + rsu: '1.1.1.1', + road: 'road', + count: 'count', + }, + ] + RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ + countsMsgType: 'message', + startDate: 1, + endDate: 86400000, + warningMessage: false, + rsuCounts, + countList, + }) + expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', data) + }) + + it('returns and calls the api correctly default values', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + rsu: { + value: { + countsMsgType: 'message', + startDate: 1, + endDate: 86400002, + }, + }, + }) + const data = {} + const action = updateRowData(data) + + const rsuCounts = { + '1.1.1.1': { road: 'road', count: 'count' }, + } + const countList = [ + { + key: '1.1.1.1', + rsu: '1.1.1.1', + road: 'road', + count: 'count', + }, + ] + RsuApi.getRsuCounts = jest.fn().mockReturnValue(rsuCounts) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ + countsMsgType: 'message', + startDate: 1, + endDate: 86400002, + warningMessage: true, + rsuCounts, + countList, + }) + expect(RsuApi.getRsuCounts).toHaveBeenCalledWith('token', 'name', '', { + message: 'message', + start: 1, + end: 86400002, + }) + }) + + it('Updates the state correctly pending', async () => { + const requestOut = true + const messageLoading = false + const state = reducer(initialState, { + type: 'rsu/updateRowData/pending', + }) + + expect(state).toEqual({ + ...initialState, + requestOut, + value: { ...initialState.value, messageLoading }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + const rsuCounts = { '1.1.1.1': { count: 5 } } + const countList = 'countList' + const heatMapData = { + type: 'FeatureCollection', + features: [ + { + properties: { + ipv4_address: '1.1.1.1', + }, + }, + { + properties: { + ipv4_address: '1.1.1.2', + }, + }, + ], + } as any + const warningMessage = 'warningMessage' + const requestOut = false + const messageLoading = false + const countsMsgType = 'countsMsgType' + const startDate = 'startDate' + const endDate = 'endDate' + const payload = { + rsuCounts, + countList, + warningMessage, + countsMsgType, + startDate, + endDate, + } + const state = reducer( + { + ...initialState, + value: { + ...initialState.value, + heatMapData, + }, + }, + { + type: 'rsu/updateRowData/fulfilled', + payload: payload, + } + ) + + heatMapData['features'][0]['properties']['count'] = 5 + heatMapData['features'][1]['properties']['count'] = 0 + + expect(state).toEqual({ + ...initialState, + requestOut, + value: { + ...initialState.value, + rsuCounts, + countList, + heatMapData, + warningMessage, + messageLoading, + countsMsgType, + startDate, + endDate, + }, + }) + }) + + it('Updates the state correctly rejected', async () => { + const requestOut = false + const messageLoading = false + const state = reducer(initialState, { + type: 'rsu/updateRowData/rejected', + }) + + expect(state).toEqual({ + ...initialState, + requestOut, + value: { ...initialState.value, messageLoading }, + }) + }) + }) + + describe('updateGeoMsgData', () => { + it('returns and calls the api correctly', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + }, + }, + rsu: { + value: { + geoMsgType: 'geoMsgType', + geoMsgStart: 'geoMsgStart', + geoMsgEnd: 'geoMsgEnd', + geoMsgCoordinates: [1, 2, 3], + }, + }, + }) + const action = updateGeoMsgData() + + RsuApi.postGeoMsgData = jest.fn().mockReturnValue('msgCounts') + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual('msgCounts') + expect(RsuApi.postGeoMsgData).toHaveBeenCalledWith( + 'token', + JSON.stringify({ + msg_type: 'geoMsgType', + start: 'geoMsgStart', + end: 'geoMsgEnd', + geometry: [1, 2, 3], + }), + '' + ) + }) + + it('condition blocks execution', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + }, + }, + rsu: { + value: { + geoMsgStart: '', + geoMsgEnd: '', + geoMsgCoordinates: [1, 2], + }, + }, + }) + const action = updateGeoMsgData() + + RsuApi.postGeoMsgData = jest.fn().mockReturnValue('msgCounts') + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual(undefined) + expect(RsuApi.postGeoMsgData).not.toHaveBeenCalled() + }) + + it('Updates the state correctly pending', async () => { + const addGeoMsgPoint = false + const loading = true + const geoMsgStart = 1 as any + const geoMsgEnd = 86400000 as any + const state = reducer( + { + ...initialState, + value: { ...initialState.value, geoMsgStart, geoMsgEnd }, + }, + { + type: 'rsu/updateGeoMsgData/pending', + } + ) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value, addGeoMsgPoint, geoMsgStart, geoMsgEnd }, + }) + }) + + it('Updates the state correctly pending date error', async () => { + const addGeoMsgPoint = false + const loading = true + const geoMsgStart = 1 as any + const geoMsgEnd = 86400002 as any + const state = reducer( + { + ...initialState, + value: { ...initialState.value, geoMsgStart, geoMsgEnd }, + }, + { + type: 'rsu/updateGeoMsgData/pending', + } + ) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value, addGeoMsgPoint, geoMsgStart, geoMsgEnd }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + const geoMsgData = 'geoMsgData' + const loading = false + const geoMsgFilter = true + const geoMsgFilterStep = 60 + const geoMsgFilterOffset = 0 + const state = reducer(initialState, { + type: 'rsu/updateGeoMsgData/fulfilled', + payload: { body: geoMsgData }, + }) + + expect(state).toEqual({ + ...initialState, + loading, + value: { + ...initialState.value, + geoMsgData, + geoMsgFilter, + geoMsgFilterStep, + geoMsgFilterOffset, + }, + }) + }) + + it('Updates the state correctly rejected', async () => { + const loading = false + const state = reducer(initialState, { + type: 'rsu/updateGeoMsgData/rejected', + }) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) + }) + + describe('getMapData', () => { + it('condition blocks execution', async () => { + const dispatch = jest.fn() + const getState = jest.fn().mockReturnValue({ + user: { + value: { + authLoginData: { token: 'token' }, + organization: { name: 'name' }, + }, + }, + rsu: { + value: { + selectedRsu: { properties: { ipv4_address: '1.1.1.1' } }, + }, + }, + }) + const action = getMapData() + + RsuApi.getRsuMapInfo = jest.fn().mockReturnValue({ geojson: 'geojson', date: 'date' }) + let resp = await action(dispatch, getState, undefined) + expect(resp.payload).toEqual({ + rsuMapData: 'geojson', + mapDate: 'date', + }) + expect(RsuApi.getRsuMapInfo).toHaveBeenCalledWith('token', 'name', '', { ip_address: '1.1.1.1' }) + }) + + it('Updates the state correctly pending', async () => { + const loading = true + const state = reducer(initialState, { + type: 'rsu/getMapData/pending', + }) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) + + it('Updates the state correctly fulfilled', async () => { + const loading = false + const rsuMapData = 'rsuMapData' + const mapDate = 'mapDate' + const state = reducer(initialState, { + type: 'rsu/getMapData/fulfilled', + payload: { rsuMapData, mapDate }, + }) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value, rsuMapData, mapDate }, + }) + }) + + it('Updates the state correctly rejected', async () => { + const loading = false + const state = reducer(initialState, { + type: 'rsu/getMapData/rejected', + }) + + expect(state).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) + }) +}) + +describe('functions', () => { + it('updateMessageType', async () => { + const dispatch = jest.fn() + + updateMessageType('messageType' as any)(dispatch) + expect(dispatch).toHaveBeenCalledTimes(2) + }) +}) + +describe('reducers', () => { + const initialState: RootState['rsu'] = { + loading: null, + requestOut: null, + value: { + selectedRsu: null, + rsuData: null, + rsuOnlineStatus: null, + rsuCounts: null, + countList: null, + currentSort: null, + startDate: null, + endDate: null, + heatMapData: { + features: [], + type: 'FeatureCollection', + }, + messageLoading: null, + warningMessage: null, + countsMsgType: null, + geoMsgType: null, + rsuMapData: null, + mapList: null, + mapDate: null, + displayMap: null, + geoMsgStart: null, + geoMsgEnd: null, + addGeoMsgPoint: null, + geoMsgCoordinates: null, + geoMsgData: null, + geoMsgDateError: null, + geoMsgFilter: null, + geoMsgFilterStep: null, + geoMsgFilterOffset: null, + issScmsStatusData: null, + ssmDisplay: null, + srmSsmList: null, + selectedSrm: null, + }, + } + + it('selectRsu reducer updates state correctly', async () => { + const selectedRsu = 'selectedRsu' + expect(reducer(initialState, selectRsu(selectedRsu))).toEqual({ + ...initialState, + value: { ...initialState.value, selectedRsu }, + }) + }) + + it('toggleMapDisplay reducer updates state correctly', async () => { + expect( + reducer({ ...initialState, value: { ...initialState.value, displayMap: true } }, toggleMapDisplay()) + ).toEqual({ + ...initialState, + value: { ...initialState.value, displayMap: false }, + }) + }) + + it('clearGeoMsg reducer updates state correctly', async () => { + expect(reducer(initialState, clearGeoMsg())).toEqual({ + ...initialState, + value: { + ...initialState.value, + geoMsgCoordinates: [], + geoMsgData: [], + geoMsgDateError: false, + }, + }) + }) + + it('toggleSsmSrmDisplay reducer updates state correctly', async () => { + expect( + reducer({ ...initialState, value: { ...initialState.value, ssmDisplay: true } }, toggleSsmSrmDisplay()) + ).toEqual({ + ...initialState, + value: { ...initialState.value, ssmDisplay: false }, + }) + }) + + it('setSelectedSrm reducer updates state correctly', async () => { + let selectedSrm = { selectedSrm: 1 } + expect(reducer(initialState, setSelectedSrm(selectedSrm))).toEqual({ + ...initialState, + value: { ...initialState.value, selectedSrm: [selectedSrm] }, + }) + + expect(reducer(initialState, setSelectedSrm({}))).toEqual({ + ...initialState, + value: { ...initialState.value, selectedSrm: [] }, + }) + }) + + it('toggleGeoMsgPointSelect reducer updates state correctly', async () => { + expect( + reducer({ ...initialState, value: { ...initialState.value, addGeoMsgPoint: true } }, toggleGeoMsgPointSelect()) + ).toEqual({ + ...initialState, + value: { ...initialState.value, addGeoMsgPoint: false }, + }) + }) + + it('updateGeoMsgPoints reducer updates state correctly', async () => { + const geoMsgCoordinates = 'geoMsgCoordinates' + expect(reducer(initialState, updateGeoMsgPoints(geoMsgCoordinates))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgCoordinates }, + }) + }) + + it('updateGeoMsgDate reducer updates state correctly', async () => { + let type = 'start' + const date = 'date' + expect(reducer(initialState, updateGeoMsgDate({ type, date }))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgStart: 'date' }, + }) + + type = 'end' + expect(reducer(initialState, updateGeoMsgDate({ type, date }))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgEnd: 'date' }, + }) + }) + + it('triggerGeoMsgDateError reducer updates state correctly', async () => { + expect(reducer(initialState, triggerGeoMsgDateError())).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgDateError: true }, + }) + }) + + it('changeCountsMsgType reducer updates state correctly', async () => { + const countsMsgType = 'countsMsgType' + expect(reducer(initialState, changeCountsMsgType(countsMsgType))).toEqual({ + ...initialState, + value: { ...initialState.value, countsMsgType }, + }) + }) + + it('setGeoMsgFilter reducer updates state correctly', async () => { + const geoMsgFilter = 'geoMsgFilter' + expect(reducer(initialState, setGeoMsgFilter(geoMsgFilter))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgFilter }, + }) + }) + + it('setGeoMsgFilterStep reducer updates state correctly', async () => { + const geoMsgFilterStep = 'geoMsgFilterStep' + const geoMsgFilterStepDict = { + value: geoMsgFilterStep, + } + expect(reducer(initialState, setGeoMsgFilterStep(geoMsgFilterStepDict))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgFilterStep }, + }) + }) + + it('setGeoMsgFilterOffset reducer updates state correctly', async () => { + const geoMsgFilterOffset = 'geoMsgFilterOffset' + expect(reducer(initialState, setGeoMsgFilterOffset(geoMsgFilterOffset))).toEqual({ + ...initialState, + value: { ...initialState.value, geoMsgFilterOffset }, + }) + }) + + it('setLoading reducer updates state correctly', async () => { + const loading = 'loading' + expect(reducer(initialState, setLoading(loading))).toEqual({ + ...initialState, + loading, + value: { ...initialState.value }, + }) + }) +}) + +describe('selectors', () => { + const initialState = { + loading: 'loading', + requestOut: 'requestOut', + value: { + selectedRsu: { + properties: { + manufacturer_name: 'manufacturer_name', + ipv4_address: 'ipv4_address', + primary_route: 'primary_route', + }, + }, + rsuData: 'rsuData', + rsuOnlineStatus: 'rsuOnlineStatus', + rsuCounts: 'rsuCounts', + countList: 'countList', + currentSort: 'currentSort', + startDate: 'startDate', + endDate: 'endDate', + heatMapData: 'heatMapData', + messageLoading: 'messageLoading', + warningMessage: 'warningMessage', + countsMsgType: 'countsMsgType', + rsuMapData: 'rsuMapData', + mapList: 'mapList', + mapDate: 'mapDate', + displayMap: 'displayMap', + geoMsgStart: 'geoMsgStart', + geoMsgEnd: 'geoMsgEnd', + addGeoMsgPoint: 'addGeoMsgPoint', + geoMsgCoordinates: 'geoMsgCoordinates', + geoMsgData: 'geoMsgData', + geoMsgDateError: 'geoMsgDateError', + geoMsgFilter: 'geoMsgFilter', + geoMsgFilterStep: 'geoMsgFilterStep', + geoMsgFilterOffset: 'geoMsgFilterOffset', + issScmsStatusData: 'issScmsStatusData', + ssmDisplay: 'ssmDisplay', + srmSsmList: 'srmSsmList', + selectedSrm: 'selectedSrm', + }, + } + const rsuState = { rsu: initialState } as any + + it('selectors return the correct value', async () => { + expect(selectLoading(rsuState)).toEqual('loading') + expect(selectRequestOut(rsuState)).toEqual('requestOut') + + expect(selectSelectedRsu(rsuState)).toEqual(initialState.value.selectedRsu) + expect(selectRsuManufacturer(rsuState)).toEqual('manufacturer_name') + expect(selectRsuIpv4(rsuState)).toEqual('ipv4_address') + expect(selectRsuPrimaryRoute(rsuState)).toEqual('primary_route') + expect(selectRsuData(rsuState)).toEqual('rsuData') + expect(selectRsuOnlineStatus(rsuState)).toEqual('rsuOnlineStatus') + expect(selectRsuCounts(rsuState)).toEqual('rsuCounts') + expect(selectCountList(rsuState)).toEqual('countList') + expect(selectCurrentSort(rsuState)).toEqual('currentSort') + expect(selectStartDate(rsuState)).toEqual('startDate') + expect(selectEndDate(rsuState)).toEqual('endDate') + expect(selectMessageLoading(rsuState)).toEqual('messageLoading') + expect(selectWarningMessage(rsuState)).toEqual('warningMessage') + expect(selectMsgType(rsuState)).toEqual('countsMsgType') + expect(selectRsuMapData(rsuState)).toEqual('rsuMapData') + expect(selectMapList(rsuState)).toEqual('mapList') + expect(selectMapDate(rsuState)).toEqual('mapDate') + expect(selectDisplayMap(rsuState)).toEqual('displayMap') + expect(selectGeoMsgStart(rsuState)).toEqual('geoMsgStart') + expect(selectGeoMsgEnd(rsuState)).toEqual('geoMsgEnd') + expect(selectAddGeoMsgPoint(rsuState)).toEqual('addGeoMsgPoint') + expect(selectGeoMsgCoordinates(rsuState)).toEqual('geoMsgCoordinates') + expect(selectGeoMsgData(rsuState)).toEqual('geoMsgData') + expect(selectGeoMsgDateError(rsuState)).toEqual('geoMsgDateError') + expect(selectGeoMsgFilter(rsuState)).toEqual('geoMsgFilter') + expect(selectGeoMsgFilterStep(rsuState)).toEqual('geoMsgFilterStep') + expect(selectGeoMsgFilterOffset(rsuState)).toEqual('geoMsgFilterOffset') + expect(selectIssScmsStatusData(rsuState)).toEqual('issScmsStatusData') + expect(selectSsmDisplay(rsuState)).toEqual('ssmDisplay') + expect(selectSrmSsmList(rsuState)).toEqual('srmSsmList') + expect(selectSelectedSrm(rsuState)).toEqual('selectedSrm') + expect(selectHeatMapData(rsuState)).toEqual('heatMapData') + }) +}) diff --git a/webapp/src/generalSlices/rsuSlice.ts b/webapp/src/generalSlices/rsuSlice.ts index 8d4df692..6381fe49 100644 --- a/webapp/src/generalSlices/rsuSlice.ts +++ b/webapp/src/generalSlices/rsuSlice.ts @@ -1,542 +1,567 @@ -import { AnyAction, createAsyncThunk, createSlice, ThunkDispatch } from '@reduxjs/toolkit' -import RsuApi from '../apis/rsu-api' -import { - ApiMsgRespWithCodes, - IssScmsStatus, - RsuCounts, - RsuInfo, - RsuMapInfo, - RsuMapInfoIpList, - RsuOnlineStatusRespMultiple, - RsuOnlineStatusRespSingle, - RsuProperties, - SsmSrmData, -} from '../apis/rsu-api-types' -import { RootState } from '../store' -import { selectToken, selectOrganizationName } from './userSlice' -import { SelectedSrm } from '../types/Srm' -import { CountsListElement } from '../types/Rsu' -import { MessageType } from '../types/MessageTypes' -const { DateTime } = require('luxon') - -const initialState = { - selectedRsu: null as RsuInfo['rsuList'][0], - rsuData: [] as RsuInfo['rsuList'], - rsuOnlineStatus: {} as RsuOnlineStatusRespMultiple, - rsuCounts: {} as RsuCounts, - countList: [] as CountsListElement[], - currentSort: '', - startDate: '', - endDate: '', - messageLoading: false, - warningMessage: false, - msgType: 'BSM', - rsuMapData: {} as RsuMapInfo['geojson'], - mapList: [] as RsuMapInfoIpList, - mapDate: '' as RsuMapInfo['date'], - displayMap: false, - bsmStart: '', - bsmEnd: '', - addBsmPoint: false, - bsmCoordinates: [] as number[][], - bsmData: [] as Array>, - bsmDateError: false, - bsmFilter: false, - bsmFilterStep: 60, - bsmFilterOffset: 0, - issScmsStatusData: {} as IssScmsStatus, - ssmDisplay: false, - srmSsmList: [] as SsmSrmData, - selectedSrm: [] as SelectedSrm[], - heatMapData: { - type: 'FeatureCollection', - features: [], - } as GeoJSON.FeatureCollection, -} - -export const updateMessageType = - (messageType: MessageType) => async (dispatch: ThunkDispatch) => { - dispatch(changeMessageType(messageType)) - dispatch(updateRowData({ message: messageType })) - } - -export const getRsuData = createAsyncThunk( - 'rsu/getRsuData', - async (_, { getState, dispatch }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - - await Promise.all([ - dispatch(_getRsuInfo()), - dispatch(_getRsuOnlineStatus(currentState.rsu.value.rsuOnlineStatus)), - dispatch(_getRsuCounts()), - dispatch( - _getRsuMapInfo({ - startDate: currentState.rsu.value.startDate, - endDate: currentState.rsu.value.endDate, - }) - ), - ]) - }, - { - condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, - } -) - -export const getRsuInfoOnly = createAsyncThunk('rsu/getRsuInfoOnly', async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - const rsuInfo = await RsuApi.getRsuInfo(token, organization) - const rsuData = rsuInfo.rsuList - return rsuData -}) - -export const getRsuLastOnline = createAsyncThunk('rsu/getRsuLastOnline', async (rsu_ip: string, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - const rsuLastOnline = await RsuApi.getRsuOnline(token, organization, '', { rsu_ip }) - return rsuLastOnline -}) - -export const _getRsuInfo = createAsyncThunk('rsu/_getRsuInfo', async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - const rsuInfo = await RsuApi.getRsuInfo(token, organization) - const rsuData = rsuInfo.rsuList - - return rsuData -}) - -export const _getRsuOnlineStatus = createAsyncThunk( - 'rsu/_getRsuOnlineStatus', - async (rsuOnlineStatusState: RsuOnlineStatusRespMultiple, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - const rsuOnlineStatus = (await RsuApi.getRsuOnline(token, organization)) ?? rsuOnlineStatusState - - return rsuOnlineStatus - } -) - -export const _getRsuCounts = createAsyncThunk('rsu/_getRsuCounts', async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - - const query_params = { - message: currentState.rsu.value.msgType, - start: currentState.rsu.value.startDate, - end: currentState.rsu.value.endDate, - } - const rsuCounts = - (await RsuApi.getRsuCounts(token, organization, '', query_params)) ?? currentState.rsu.value.rsuCounts - const countList = Object.entries(rsuCounts).map(([key, value]) => { - return { - key: key, - rsu: key, - road: value.road, - count: value.count, - } - }) - - return { rsuCounts, countList } -}) - -export const _getRsuMapInfo = createAsyncThunk( - 'rsu/_getRsuMapInfo', - async ({ startDate, endDate }: { startDate: string; endDate: string }, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - let local_date = DateTime.local({ zone: 'America/Denver' }) - let localEndDate = endDate === '' ? local_date.toString() : endDate - let localStartDate = startDate === '' ? local_date.minus({ days: 1 }).toString() : startDate - - const rsuMapData = (await RsuApi.getRsuMapInfo(token, organization, '', { - ip_list: 'True', - })) as RsuMapInfoIpList - - return { - endDate: localEndDate, - startDate: localStartDate, - rsuMapData, - } - } -) - -export const getSsmSrmData = createAsyncThunk('rsu/getSsmSrmData', async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - return await RsuApi.getSsmSrmData(token) -}) - -export const getIssScmsStatus = createAsyncThunk( - 'rsu/getIssScmsStatus', - async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - - return await RsuApi.getIssScmsStatus(token, organization) - }, - { - condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, - } -) - -export const updateRowData = createAsyncThunk( - 'rsu/updateRowData', - async ( - data: { - message?: MessageType - start?: string - end?: string - }, - { getState } - ) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - - const msgType = data.hasOwnProperty('message') ? data['message'] : currentState.rsu.value.msgType - const startDate = data.hasOwnProperty('start') ? data['start'] : currentState.rsu.value.startDate - const endDate = data.hasOwnProperty('end') ? data['end'] : currentState.rsu.value.endDate - - const warningMessage = new Date(endDate).getTime() - new Date(startDate).getTime() > 86400000 - - const rsuCountsData = await RsuApi.getRsuCounts(token, organization, '', { - message: msgType, - start: startDate, - end: endDate, - }) - - var countList = Object.entries(rsuCountsData).map(([key, value]) => { - return { - key: key, - rsu: key, - road: value.road, - count: value.count, - } - }) - - return { - msgType, - startDate, - endDate, - warningMessage, - rsuCounts: rsuCountsData, - countList, - } - }, - { - condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, - } -) - -export const updateBsmData = createAsyncThunk( - 'rsu/updateBsmData', - async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - - try { - const bsmMapData: ApiMsgRespWithCodes>> = await RsuApi.postBsmData( - token, - { - start: currentState.rsu.value.bsmStart, - end: currentState.rsu.value.bsmEnd, - geometry: currentState.rsu.value.bsmCoordinates, - }, - '' - ) - return bsmMapData - } catch (err) { - console.error(err) - } - }, - { - // Will guard thunk from being executed - condition: (_, { getState }) => { - const { rsu } = getState() as RootState - const valid = rsu.value.bsmStart !== '' && rsu.value.bsmEnd !== '' && rsu.value.bsmCoordinates.length > 2 - return valid - }, - } -) - -export const getMapData = createAsyncThunk( - 'rsu/getMapData', - async (_, { getState }) => { - const currentState = getState() as RootState - const token = selectToken(currentState) - const organization = selectOrganizationName(currentState) - const selectedRsu = selectSelectedRsu(currentState) - - const rsuMapData = (await RsuApi.getRsuMapInfo(token, organization, '', { - ip_address: selectedRsu.properties.ipv4_address, - })) as RsuMapInfo - return { - rsuMapData: rsuMapData.geojson, - mapDate: rsuMapData.date, - } - }, - { - condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, - } -) - -export const rsuSlice = createSlice({ - name: 'rsu', - initialState: { - loading: false, - requestOut: false, - value: initialState, - }, - reducers: { - selectRsu: (state, action) => { - state.value.selectedRsu = action.payload - }, - toggleMapDisplay: (state) => { - state.value.displayMap = !state.value.displayMap - }, - clearBsm: (state) => { - state.value.bsmCoordinates = [] - state.value.bsmData = [] - state.value.bsmStart = '' - state.value.bsmEnd = '' - state.value.bsmDateError = false - }, - toggleSsmSrmDisplay: (state) => { - state.value.ssmDisplay = !state.value.ssmDisplay - }, - setSelectedSrm: (state, action) => { - state.value.selectedSrm = Object.keys(action.payload).length === 0 ? [] : [action.payload] - }, - toggleBsmPointSelect: (state) => { - state.value.addBsmPoint = !state.value.addBsmPoint - }, - updateBsmPoints: (state, action) => { - state.value.bsmCoordinates = action.payload - }, - updateBsmDate: (state, action) => { - if (action.payload.type === 'start') state.value.bsmStart = action.payload.date - else state.value.bsmEnd = action.payload.date - }, - triggerBsmDateError: (state) => { - state.value.bsmDateError = true - }, - changeMessageType: (state, action) => { - state.value.msgType = action.payload - }, - setBsmFilter: (state, action) => { - state.value.bsmFilter = action.payload - }, - setBsmFilterStep: (state, action) => { - state.value.bsmFilterStep = action.payload - }, - setBsmFilterOffset: (state, action) => { - state.value.bsmFilterOffset = action.payload - }, - setLoading: (state, action) => { - state.loading = action.payload - }, - }, - extraReducers: (builder) => { - builder - .addCase(getRsuData.pending, (state) => { - state.loading = true - state.value.rsuData = [] - state.value.rsuOnlineStatus = {} - state.value.rsuCounts = {} - state.value.countList = [] - state.value.heatMapData = { - type: 'FeatureCollection', - features: [], - } - }) - .addCase(getRsuData.fulfilled, (state) => { - const heatMapFeatures: GeoJSON.Feature[] = [] - state.value.rsuData.forEach((rsu) => { - heatMapFeatures.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [rsu.geometry.coordinates[0], rsu.geometry.coordinates[1]], - }, - properties: { - ipv4_address: rsu.properties.ipv4_address, - count: - rsu.properties.ipv4_address in state.value.rsuCounts - ? state.value.rsuCounts[rsu.properties.ipv4_address].count - : 0, - }, - }) - }) - state.value.heatMapData.features = heatMapFeatures - state.loading = false - }) - .addCase(getRsuData.rejected, (state) => { - state.loading = false - }) - .addCase(getRsuInfoOnly.pending, (state) => { - state.loading = true - }) - .addCase(getRsuInfoOnly.fulfilled, (state) => { - state.loading = false - }) - .addCase(getRsuInfoOnly.rejected, (state) => { - state.loading = false - }) - .addCase(getRsuLastOnline.pending, (state) => { - state.loading = true - }) - .addCase(getRsuLastOnline.fulfilled, (state, action) => { - state.loading = false - const payload = action.payload as RsuOnlineStatusRespSingle - if (state.value.rsuOnlineStatus.hasOwnProperty(payload.ip)) { - ;(state.value.rsuOnlineStatus as RsuOnlineStatusRespMultiple)[payload.ip]['last_online'] = payload.last_online - } - }) - .addCase(getRsuLastOnline.rejected, (state) => { - state.loading = false - }) - .addCase(_getRsuInfo.fulfilled, (state, action) => { - state.value.rsuData = action.payload - }) - .addCase(_getRsuOnlineStatus.fulfilled, (state, action) => { - state.value.rsuOnlineStatus = action.payload as RsuOnlineStatusRespMultiple - }) - .addCase(_getRsuCounts.fulfilled, (state, action) => { - state.value.rsuCounts = action.payload.rsuCounts - state.value.countList = action.payload.countList - }) - .addCase(_getRsuMapInfo.fulfilled, (state, action) => { - state.value.startDate = action.payload.startDate - state.value.endDate = action.payload.endDate - state.value.mapList = action.payload.rsuMapData - }) - .addCase(getSsmSrmData.pending, (state) => { - state.loading = true - }) - .addCase(getSsmSrmData.rejected, (state) => { - state.loading = false - }) - .addCase(getSsmSrmData.fulfilled, (state, action) => { - state.value.srmSsmList = action.payload - }) - .addCase(getIssScmsStatus.fulfilled, (state, action) => { - state.value.issScmsStatusData = action.payload ?? state.value.issScmsStatusData - }) - .addCase(updateRowData.pending, (state) => { - state.requestOut = true - state.value.messageLoading = false - }) - .addCase(updateRowData.fulfilled, (state, action) => { - if (action.payload === null) return - state.value.rsuCounts = action.payload.rsuCounts - state.value.countList = action.payload.countList - state.value.heatMapData.features.forEach((feat, index) => { - const ip = feat.properties.ipv4_address as string - state.value.heatMapData.features[index].properties.count = - ip in action.payload.rsuCounts ? action.payload.rsuCounts[ip].count : 0 - }) - state.value.warningMessage = action.payload.warningMessage - state.requestOut = false - state.value.messageLoading = false - state.value.msgType = action.payload.msgType - state.value.startDate = action.payload.startDate - state.value.endDate = action.payload.endDate - }) - .addCase(updateRowData.rejected, (state) => { - state.requestOut = false - state.value.messageLoading = false - }) - .addCase(updateBsmData.pending, (state) => { - state.loading = true - state.value.addBsmPoint = false - state.value.bsmDateError = - new Date(state.value.bsmEnd).getTime() - new Date(state.value.bsmStart).getTime() > 86400000 - }) - .addCase(updateBsmData.fulfilled, (state, action) => { - state.value.bsmData = action.payload.body - state.loading = false - state.value.bsmFilter = true - state.value.bsmFilterStep = 60 - state.value.bsmFilterOffset = 0 - }) - .addCase(updateBsmData.rejected, (state) => { - state.loading = false - }) - .addCase(getMapData.pending, (state) => { - state.loading = true - }) - .addCase(getMapData.fulfilled, (state, action) => { - state.loading = false - state.value.rsuMapData = action.payload.rsuMapData - state.value.mapDate = action.payload.mapDate - }) - .addCase(getMapData.rejected, (state) => { - state.loading = false - }) - }, -}) - -export const selectLoading = (state: RootState) => state.rsu.loading -export const selectRequestOut = (state: RootState) => state.rsu.requestOut - -export const selectSelectedRsu = (state: RootState) => state.rsu.value.selectedRsu -export const selectRsuManufacturer = (state: RootState) => state.rsu.value.selectedRsu?.properties?.manufacturer_name -export const selectRsuIpv4 = (state: RootState) => state.rsu.value.selectedRsu?.properties?.ipv4_address -export const selectRsuPrimaryRoute = (state: RootState) => state.rsu.value.selectedRsu?.properties?.primary_route -export const selectRsuData = (state: RootState) => state.rsu.value.rsuData -export const selectRsuOnlineStatus = (state: RootState) => state.rsu.value.rsuOnlineStatus -export const selectRsuCounts = (state: RootState) => state.rsu.value.rsuCounts -export const selectCountList = (state: RootState) => state.rsu.value.countList -export const selectCurrentSort = (state: RootState) => state.rsu.value.currentSort -export const selectStartDate = (state: RootState) => state.rsu.value.startDate -export const selectEndDate = (state: RootState) => state.rsu.value.endDate -export const selectMessageLoading = (state: RootState) => state.rsu.value.messageLoading -export const selectWarningMessage = (state: RootState) => state.rsu.value.warningMessage -export const selectMsgType = (state: RootState) => state.rsu.value.msgType -export const selectRsuMapData = (state: RootState) => state.rsu.value.rsuMapData -export const selectMapList = (state: RootState) => state.rsu.value.mapList -export const selectMapDate = (state: RootState) => state.rsu.value.mapDate -export const selectDisplayMap = (state: RootState) => state.rsu.value.displayMap -export const selectBsmStart = (state: RootState) => state.rsu.value.bsmStart -export const selectBsmEnd = (state: RootState) => state.rsu.value.bsmEnd -export const selectAddBsmPoint = (state: RootState) => state.rsu.value.addBsmPoint -export const selectBsmCoordinates = (state: RootState) => state.rsu.value.bsmCoordinates -export const selectBsmData = (state: RootState) => state.rsu.value.bsmData -export const selectBsmDateError = (state: RootState) => state.rsu.value.bsmDateError -export const selectBsmFilter = (state: RootState) => state.rsu.value.bsmFilter -export const selectBsmFilterStep = (state: RootState) => state.rsu.value.bsmFilterStep -export const selectBsmFilterOffset = (state: RootState) => state.rsu.value.bsmFilterOffset -export const selectIssScmsStatusData = (state: RootState) => state.rsu.value.issScmsStatusData -export const selectSsmDisplay = (state: RootState) => state.rsu.value.ssmDisplay -export const selectSrmSsmList = (state: RootState) => state.rsu.value.srmSsmList -export const selectSelectedSrm = (state: RootState) => state.rsu.value.selectedSrm -export const selectHeatMapData = (state: RootState) => state.rsu.value.heatMapData - -export const { - selectRsu, - toggleMapDisplay, - clearBsm, - toggleSsmSrmDisplay, - setSelectedSrm, - toggleBsmPointSelect, - updateBsmPoints, - updateBsmDate, - triggerBsmDateError, - changeMessageType, - setBsmFilter, - setBsmFilterStep, - setBsmFilterOffset, - setLoading, -} = rsuSlice.actions - -export default rsuSlice.reducer +import { AnyAction, createAsyncThunk, createSlice, ThunkDispatch } from '@reduxjs/toolkit' +import RsuApi from '../apis/rsu-api' +import { + ApiMsgRespWithCodes, + IssScmsStatus, + RsuCounts, + RsuInfo, + RsuMapInfo, + RsuMapInfoIpList, + RsuOnlineStatusRespMultiple, + RsuOnlineStatusRespSingle, + RsuProperties, + SsmSrmData, +} from '../apis/rsu-api-types' +import { RootState } from '../store' +import { selectToken, selectOrganizationName } from './userSlice' +import { SelectedSrm } from '../types/Srm' +import { CountsListElement } from '../types/Rsu' +import { MessageType, GeoMessageType } from '../types/MessageTypes' +const { DateTime } = require('luxon') +const currentDate = DateTime.local().setZone(DateTime.local().zoneName) + +const initialState = { + selectedRsu: null as RsuInfo['rsuList'][0], + rsuData: [] as RsuInfo['rsuList'], + rsuOnlineStatus: {} as RsuOnlineStatusRespMultiple, + rsuCounts: {} as RsuCounts, + countList: [] as CountsListElement[], + currentSort: '', + startDate: '', + endDate: '', + messageLoading: false, + warningMessage: false, + countsMsgType: 'BSM', + geoMsgType: 'BSM', + rsuMapData: {} as RsuMapInfo['geojson'], + mapList: [] as RsuMapInfoIpList, + mapDate: '' as RsuMapInfo['date'], + displayMap: false, + geoMsgStart: currentDate.minus({ days: 1 }).toString(), + geoMsgEnd: currentDate.toString(), + addGeoMsgPoint: false, + geoMsgCoordinates: [] as number[][], + geoMsgData: [] as Array>, + geoMsgDateError: false, + geoMsgFilter: false, + geoMsgFilterStep: 60, + geoMsgFilterOffset: 0, + issScmsStatusData: {} as IssScmsStatus, + ssmDisplay: false, + srmSsmList: [] as SsmSrmData, + selectedSrm: [] as SelectedSrm[], + heatMapData: { + type: 'FeatureCollection', + features: [], + } as GeoJSON.FeatureCollection, +} + +export const updateMessageType = + (messageType: MessageType) => async (dispatch: ThunkDispatch) => { + dispatch(changeCountsMsgType(messageType)) + dispatch(updateRowData({ message: messageType })) + } + +export const getRsuData = createAsyncThunk( + 'rsu/getRsuData', + async (_, { getState, dispatch }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + + await Promise.all([ + dispatch(_getRsuInfo()), + dispatch(_getRsuOnlineStatus(currentState.rsu.value.rsuOnlineStatus)), + dispatch(_getRsuCounts()), + dispatch( + _getRsuMapInfo({ + startDate: currentState.rsu.value.startDate, + endDate: currentState.rsu.value.endDate, + }) + ), + ]) + }, + { + condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, + } +) + +export const getRsuInfoOnly = createAsyncThunk('rsu/getRsuInfoOnly', async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + const rsuInfo = await RsuApi.getRsuInfo(token, organization) + const rsuData = rsuInfo.rsuList + return rsuData +}) + +export const getRsuLastOnline = createAsyncThunk('rsu/getRsuLastOnline', async (rsu_ip: string, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + const rsuLastOnline = await RsuApi.getRsuOnline(token, organization, '', { rsu_ip }) + return rsuLastOnline +}) + +export const _getRsuInfo = createAsyncThunk('rsu/_getRsuInfo', async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + const rsuInfo = await RsuApi.getRsuInfo(token, organization) + const rsuData = rsuInfo.rsuList + + return rsuData +}) + +export const _getRsuOnlineStatus = createAsyncThunk( + 'rsu/_getRsuOnlineStatus', + async (rsuOnlineStatusState: RsuOnlineStatusRespMultiple, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + const rsuOnlineStatus = (await RsuApi.getRsuOnline(token, organization)) ?? rsuOnlineStatusState + + return rsuOnlineStatus + } +) + +export const _getRsuCounts = createAsyncThunk('rsu/_getRsuCounts', async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + + const query_params = { + message: currentState.rsu.value.countsMsgType, + start: currentState.rsu.value.startDate, + end: currentState.rsu.value.endDate, + } + const rsuCounts = + (await RsuApi.getRsuCounts(token, organization, '', query_params)) ?? currentState.rsu.value.rsuCounts + const countList = Object.entries(rsuCounts).map(([key, value]) => { + return { + key: key, + rsu: key, + road: value.road, + count: value.count, + } + }) + + return { rsuCounts, countList } +}) + +export const _getRsuMapInfo = createAsyncThunk( + 'rsu/_getRsuMapInfo', + async ({ startDate, endDate }: { startDate: string; endDate: string }, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + let local_date = DateTime.local().zoneName + let localEndDate = endDate === '' ? local_date.toString() : endDate + let localStartDate = startDate === '' ? local_date.minus({ days: 1 }).toString() : startDate + + const rsuMapData = (await RsuApi.getRsuMapInfo(token, organization, '', { + ip_list: 'True', + })) as RsuMapInfoIpList + + return { + endDate: localEndDate, + startDate: localStartDate, + rsuMapData, + } + } +) + +export const getSsmSrmData = createAsyncThunk('rsu/getSsmSrmData', async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + return await RsuApi.getSsmSrmData(token) +}) + +export const getIssScmsStatus = createAsyncThunk( + 'rsu/getIssScmsStatus', + async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + + return await RsuApi.getIssScmsStatus(token, organization) + }, + { + condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, + } +) + +export const updateRowData = createAsyncThunk( + 'rsu/updateRowData', + async ( + data: { + message?: MessageType + start?: string + end?: string + }, + { getState } + ) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + + const countsMsgType = data.hasOwnProperty('message') ? data['message'] : currentState.rsu.value.countsMsgType + const startDate = data.hasOwnProperty('start') ? data['start'] : currentState.rsu.value.startDate + const endDate = data.hasOwnProperty('end') ? data['end'] : currentState.rsu.value.endDate + + const warningMessage = new Date(endDate).getTime() - new Date(startDate).getTime() > 86400000 + + const rsuCountsData = await RsuApi.getRsuCounts(token, organization, '', { + message: countsMsgType, + start: startDate, + end: endDate, + }) + + var countList = Object.entries(rsuCountsData).map(([key, value]) => { + return { + key: key, + rsu: key, + road: value.road, + count: value.count, + } + }) + + return { + countsMsgType, + startDate, + endDate, + warningMessage, + rsuCounts: rsuCountsData, + countList, + } + }, + { + condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, + } +) + +export const updateGeoMsgData = createAsyncThunk( + 'rsu/updateGeoMsgData', + async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + + try { + const geoMapData = await RsuApi.postGeoMsgData( + token, + JSON.stringify({ + msg_type: currentState.rsu.value.geoMsgType, + start: currentState.rsu.value.geoMsgStart, + end: currentState.rsu.value.geoMsgEnd, + geometry: currentState.rsu.value.geoMsgCoordinates, + }), + '' + ) + return geoMapData + } catch (err) { + console.error(err) + } + }, + { + // Will guard thunk from being executed + condition: (_, { getState }) => { + const { rsu } = getState() as RootState + console.log( + 'time', + rsu.value.geoMsgStart, + ' : ', + rsu.value.geoMsgEnd, + ' Coordinate length: ', + rsu.value.geoMsgCoordinates.length, + ' countsMsgType ', + rsu.value.countsMsgType + ) + const valid = + rsu.value.geoMsgStart !== '' && + rsu.value.geoMsgEnd !== '' && + rsu.value.geoMsgCoordinates.length > 2 && + rsu.value.countsMsgType !== '' + return valid + }, + } +) + +export const getMapData = createAsyncThunk( + 'rsu/getMapData', + async (_, { getState }) => { + const currentState = getState() as RootState + const token = selectToken(currentState) + const organization = selectOrganizationName(currentState) + const selectedRsu = selectSelectedRsu(currentState) + + const rsuMapData = (await RsuApi.getRsuMapInfo(token, organization, '', { + ip_address: selectedRsu.properties.ipv4_address, + })) as RsuMapInfo + return { + rsuMapData: rsuMapData.geojson, + mapDate: rsuMapData.date, + } + }, + { + condition: (_, { getState }) => selectToken(getState() as RootState) != undefined, + } +) + +export const rsuSlice = createSlice({ + name: 'rsu', + initialState: { + loading: false, + requestOut: false, + value: initialState, + }, + reducers: { + selectRsu: (state, action) => { + state.value.selectedRsu = action.payload + }, + toggleMapDisplay: (state) => { + state.value.displayMap = !state.value.displayMap + }, + clearGeoMsg: (state) => { + state.value.geoMsgCoordinates = [] + state.value.geoMsgData = [] + state.value.geoMsgDateError = false + }, + toggleSsmSrmDisplay: (state) => { + state.value.ssmDisplay = !state.value.ssmDisplay + }, + setSelectedSrm: (state, action) => { + state.value.selectedSrm = Object.keys(action.payload).length === 0 ? [] : [action.payload] + }, + toggleGeoMsgPointSelect: (state) => { + state.value.addGeoMsgPoint = !state.value.addGeoMsgPoint + }, + updateGeoMsgPoints: (state, action) => { + console.debug('updateGeoMsgPoints') + state.value.geoMsgCoordinates = action.payload + }, + updateGeoMsgDate: (state, action) => { + if (action.payload.type === 'start') state.value.geoMsgStart = action.payload.date + else state.value.geoMsgEnd = action.payload.date + }, + triggerGeoMsgDateError: (state) => { + state.value.geoMsgDateError = true + }, + changeCountsMsgType: (state, action) => { + state.value.countsMsgType = action.payload + }, + changeGeoMsgType: (state, action) => { + console.debug('changeGeoMsgType', action.payload) + state.value.geoMsgType = action.payload + }, + setGeoMsgFilter: (state, action) => { + state.value.geoMsgFilter = action.payload + }, + setGeoMsgFilterStep: (state, action) => { + state.value.geoMsgFilterStep = action.payload.value + }, + setGeoMsgFilterOffset: (state, action) => { + state.value.geoMsgFilterOffset = action.payload + }, + setLoading: (state, action) => { + state.loading = action.payload + }, + }, + extraReducers: (builder) => { + builder + .addCase(getRsuData.pending, (state) => { + state.loading = true + state.value.rsuData = [] + state.value.rsuOnlineStatus = {} + state.value.rsuCounts = {} + state.value.countList = [] + state.value.heatMapData = { + type: 'FeatureCollection', + features: [], + } + }) + .addCase(getRsuData.fulfilled, (state) => { + const heatMapFeatures: GeoJSON.Feature[] = [] + state.value.rsuData.forEach((rsu) => { + heatMapFeatures.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [rsu.geometry.coordinates[0], rsu.geometry.coordinates[1]], + }, + properties: { + ipv4_address: rsu.properties.ipv4_address, + count: + rsu.properties.ipv4_address in state.value.rsuCounts + ? state.value.rsuCounts[rsu.properties.ipv4_address].count + : 0, + }, + }) + }) + state.value.heatMapData.features = heatMapFeatures + state.loading = false + }) + .addCase(getRsuData.rejected, (state) => { + state.loading = false + }) + .addCase(getRsuInfoOnly.pending, (state) => { + state.loading = true + }) + .addCase(getRsuInfoOnly.fulfilled, (state) => { + state.loading = false + }) + .addCase(getRsuInfoOnly.rejected, (state) => { + state.loading = false + }) + .addCase(getRsuLastOnline.pending, (state) => { + state.loading = true + }) + .addCase(getRsuLastOnline.fulfilled, (state, action) => { + state.loading = false + const payload = action.payload as RsuOnlineStatusRespSingle + if (state.value.rsuOnlineStatus.hasOwnProperty(payload.ip)) { + ;(state.value.rsuOnlineStatus as RsuOnlineStatusRespMultiple)[payload.ip]['last_online'] = payload.last_online + } + }) + .addCase(getRsuLastOnline.rejected, (state) => { + state.loading = false + }) + .addCase(_getRsuInfo.fulfilled, (state, action) => { + state.value.rsuData = action.payload + }) + .addCase(_getRsuOnlineStatus.fulfilled, (state, action) => { + state.value.rsuOnlineStatus = action.payload as RsuOnlineStatusRespMultiple + }) + .addCase(_getRsuCounts.fulfilled, (state, action) => { + state.value.rsuCounts = action.payload.rsuCounts + state.value.countList = action.payload.countList + }) + .addCase(_getRsuMapInfo.fulfilled, (state, action) => { + state.value.startDate = action.payload.startDate + state.value.endDate = action.payload.endDate + state.value.mapList = action.payload.rsuMapData + }) + .addCase(getSsmSrmData.pending, (state) => { + state.loading = true + }) + .addCase(getSsmSrmData.rejected, (state) => { + state.loading = false + }) + .addCase(getSsmSrmData.fulfilled, (state, action) => { + state.value.srmSsmList = action.payload + }) + .addCase(getIssScmsStatus.fulfilled, (state, action) => { + state.value.issScmsStatusData = action.payload ?? state.value.issScmsStatusData + }) + .addCase(updateRowData.pending, (state) => { + state.requestOut = true + state.value.messageLoading = false + }) + .addCase(updateRowData.fulfilled, (state, action) => { + if (action.payload === null) return + state.value.rsuCounts = action.payload.rsuCounts + state.value.countList = action.payload.countList + state.value.heatMapData.features.forEach((feat, index) => { + const ip = feat.properties.ipv4_address as string + state.value.heatMapData.features[index].properties.count = + ip in action.payload.rsuCounts ? action.payload.rsuCounts[ip].count : 0 + }) + state.value.warningMessage = action.payload.warningMessage + state.requestOut = false + state.value.messageLoading = false + state.value.countsMsgType = action.payload.countsMsgType + state.value.startDate = action.payload.startDate + state.value.endDate = action.payload.endDate + }) + .addCase(updateRowData.rejected, (state) => { + state.requestOut = false + state.value.messageLoading = false + }) + .addCase(updateGeoMsgData.pending, (state) => { + console.debug('updateGeoMsgData pending') + state.loading = true + state.value.addGeoMsgPoint = false + // Removed 1 day limitation for new mongo deployment + // state.value.geoMsgDateError = + // new Date(state.value.geoMsgEnd).getTime() - new Date(state.value.geoMsgStart).getTime() > 86400000 + }) + .addCase(updateGeoMsgData.fulfilled, (state, action) => { + console.debug('updateGeoMsgData fulfilled') + state.value.geoMsgData = action.payload.body + state.loading = false + state.value.geoMsgFilter = true + state.value.geoMsgFilterStep = 60 + state.value.geoMsgFilterOffset = 0 + }) + .addCase(updateGeoMsgData.rejected, (state) => { + console.debug('updateGeoMsgData rejected') + state.loading = false + }) + .addCase(getMapData.pending, (state) => { + state.loading = true + }) + .addCase(getMapData.fulfilled, (state, action) => { + state.loading = false + state.value.rsuMapData = action.payload.rsuMapData + state.value.mapDate = action.payload.mapDate + }) + .addCase(getMapData.rejected, (state) => { + state.loading = false + }) + }, +}) + +export const selectLoading = (state: RootState) => state.rsu.loading +export const selectRequestOut = (state: RootState) => state.rsu.requestOut + +export const selectSelectedRsu = (state: RootState) => state.rsu.value.selectedRsu +export const selectRsuManufacturer = (state: RootState) => state.rsu.value.selectedRsu?.properties?.manufacturer_name +export const selectRsuIpv4 = (state: RootState) => state.rsu.value.selectedRsu?.properties?.ipv4_address +export const selectRsuPrimaryRoute = (state: RootState) => state.rsu.value.selectedRsu?.properties?.primary_route +export const selectRsuData = (state: RootState) => state.rsu.value.rsuData +export const selectRsuOnlineStatus = (state: RootState) => state.rsu.value.rsuOnlineStatus +export const selectRsuCounts = (state: RootState) => state.rsu.value.rsuCounts +export const selectCountList = (state: RootState) => state.rsu.value.countList +export const selectCurrentSort = (state: RootState) => state.rsu.value.currentSort +export const selectStartDate = (state: RootState) => state.rsu.value.startDate +export const selectEndDate = (state: RootState) => state.rsu.value.endDate +export const selectMessageLoading = (state: RootState) => state.rsu.value.messageLoading +export const selectWarningMessage = (state: RootState) => state.rsu.value.warningMessage +export const selectMsgType = (state: RootState) => state.rsu.value.countsMsgType +export const selectRsuMapData = (state: RootState) => state.rsu.value.rsuMapData +export const selectMapList = (state: RootState) => state.rsu.value.mapList +export const selectMapDate = (state: RootState) => state.rsu.value.mapDate +export const selectDisplayMap = (state: RootState) => state.rsu.value.displayMap +export const selectGeoMsgStart = (state: RootState) => state.rsu.value.geoMsgStart +export const selectGeoMsgEnd = (state: RootState) => state.rsu.value.geoMsgEnd +export const selectAddGeoMsgPoint = (state: RootState) => state.rsu.value.addGeoMsgPoint +export const selectGeoMsgCoordinates = (state: RootState) => state.rsu.value.geoMsgCoordinates +export const selectGeoMsgData = (state: RootState) => state.rsu.value.geoMsgData +export const selectGeoMsgDateError = (state: RootState) => state.rsu.value.geoMsgDateError +export const selectGeoMsgFilter = (state: RootState) => state.rsu.value.geoMsgFilter +export const selectGeoMsgFilterStep = (state: RootState) => state.rsu.value.geoMsgFilterStep +export const selectGeoMsgFilterOffset = (state: RootState) => state.rsu.value.geoMsgFilterOffset +export const selectIssScmsStatusData = (state: RootState) => state.rsu.value.issScmsStatusData +export const selectSsmDisplay = (state: RootState) => state.rsu.value.ssmDisplay +export const selectSrmSsmList = (state: RootState) => state.rsu.value.srmSsmList +export const selectSelectedSrm = (state: RootState) => state.rsu.value.selectedSrm +export const selectHeatMapData = (state: RootState) => state.rsu.value.heatMapData + +export const { + selectRsu, + toggleMapDisplay, + clearGeoMsg, + toggleSsmSrmDisplay, + setSelectedSrm, + toggleGeoMsgPointSelect, + updateGeoMsgPoints, + updateGeoMsgDate, + triggerGeoMsgDateError, + changeCountsMsgType, + changeGeoMsgType, + setGeoMsgFilter, + setGeoMsgFilterStep, + setGeoMsgFilterOffset, + setLoading, +} = rsuSlice.actions + +export default rsuSlice.reducer diff --git a/webapp/src/generalSlices/userSlice.test.ts b/webapp/src/generalSlices/userSlice.test.ts index 06627eb3..6670f7c2 100644 --- a/webapp/src/generalSlices/userSlice.test.ts +++ b/webapp/src/generalSlices/userSlice.test.ts @@ -43,6 +43,7 @@ describe('user reducer', () => { loginFailure: false, kcFailure: false, loginMessage: '', + routeNotFound: false, }, }) }) @@ -57,6 +58,7 @@ describe('async thunks', () => { loginFailure: undefined, kcFailure: null, loginMessage: '', + routeNotFound: false, }, } @@ -151,6 +153,7 @@ describe('reducers', () => { loginFailure: null, loginMessage: '', kcFailure: null, + routeNotFound: false, }, } diff --git a/webapp/src/generalSlices/userSlice.ts b/webapp/src/generalSlices/userSlice.ts index ed9173cd..dda091e0 100644 --- a/webapp/src/generalSlices/userSlice.ts +++ b/webapp/src/generalSlices/userSlice.ts @@ -54,6 +54,7 @@ export const userSlice = createSlice({ loginFailure: false, kcFailure: false, loginMessage: '', + routeNotFound: false, }, }, reducers: { @@ -81,6 +82,10 @@ export const userSlice = createSlice({ setLoginMessage: (state, action) => { state.value.loginMessage = action.payload }, + setRouteNotFound: (state, action) => { + console.log('setRouteNotFound: ', action.payload) + state.value.routeNotFound = action.payload + }, }, extraReducers: (builder) => { builder @@ -110,7 +115,8 @@ export const userSlice = createSlice({ }, }) -export const { logout, changeOrganization, setLoading, setLoginFailure, setKcFailure } = userSlice.actions +export const { logout, changeOrganization, setLoading, setLoginFailure, setKcFailure, setRouteNotFound } = + userSlice.actions export const selectAuthLoginData = (state: RootState) => state.user.value.authLoginData export const selectToken = (state: RootState) => state.user.value.authLoginData.token @@ -124,6 +130,7 @@ export const selectTokenExpiration = (state: RootState) => state.user.value.auth export const selectLoginFailure = (state: RootState) => state.user.value.loginFailure export const selectKcFailure = (state: RootState) => state.user.value.kcFailure export const selectLoginMessage = (state: RootState) => state.user.value.loginMessage +export const selectRouteNotFound = (state: RootState) => state.user.value.routeNotFound export const selectLoading = (state: RootState) => state.user.loading export const selectLoadingGlobal = (state: RootState) => { let loading = false diff --git a/webapp/src/managers.tsx b/webapp/src/managers.tsx index 6d4e50c5..6b9d22fe 100644 --- a/webapp/src/managers.tsx +++ b/webapp/src/managers.tsx @@ -45,7 +45,6 @@ const SecureStorageManager = { } }, setUserRole: (authData) => { - console.log('secureSetAuthData: ', authData['data']['organizations'][0]) return secureLocalStorage.setItem( AUTH_DATA_SECURE_STORAGE_KEY, JSON.stringify(authData['data']['organizations'][0]) diff --git a/webapp/src/pages/404.tsx b/webapp/src/pages/404.tsx new file mode 100644 index 00000000..9b5140a3 --- /dev/null +++ b/webapp/src/pages/404.tsx @@ -0,0 +1,82 @@ +import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' +import React, { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { Link } from 'react-router-dom' +import { RootState } from '../store' +import { setRouteNotFound } from '../generalSlices/userSlice' +import { useNavigate } from 'react-router-dom' +import { Button, Typography } from '@mui/material' + +type NotFoundProps = { + redirectRoute?: string + redirectRouteName?: string + description?: string + shouldRedirect?: boolean + offsetHeight?: number +} +export const NotFound = ({ + redirectRoute = '/dashboard', + redirectRouteName = 'Main Dashboard', + description = 'This route does not exist. Please return to the main dashboard.', + shouldRedirect = false, + offsetHeight = 135, +}: NotFoundProps) => { + const dispatch: ThunkDispatch = useDispatch() + const navigate = useNavigate() + + useEffect(() => { + if (shouldRedirect) { + dispatch(setRouteNotFound(true)) + } + }, []) + return ( +
    + + 404 - Page Not Found + +
    + + {description} + +
    + + {redirectRoute !== '/dashboard' ? ( + <> +
    + + + ) : ( + <> + )} +

    +
    + ) +} diff --git a/webapp/src/pages/Admin.test.tsx b/webapp/src/pages/Admin.test.tsx index 60351aab..38f23fcd 100644 --- a/webapp/src/pages/Admin.test.tsx +++ b/webapp/src/pages/Admin.test.tsx @@ -4,11 +4,14 @@ import Admin from './Admin' import { Provider } from 'react-redux' import { setupStore } from '../store' import { replaceChaoticIds } from '../utils/test-utils' +import { BrowserRouter } from 'react-router-dom' it('should take a snapshot', () => { const { container } = render( - + + + ) replaceChaoticIds(container) diff --git a/webapp/src/pages/Admin.tsx b/webapp/src/pages/Admin.tsx index b01d2ee6..ca625b60 100644 --- a/webapp/src/pages/Admin.tsx +++ b/webapp/src/pages/Admin.tsx @@ -1,6 +1,4 @@ -import React, { useEffect } from 'react' -import AdminFormManager from '../components/AdminFormManager' -import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' +import React, { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { updateTableData as updateRsuTableData } from '../features/adminRsuTab/adminRsuTabSlice' import { getAvailableUsers } from '../features/adminUserTab/adminUserTabSlice' @@ -8,9 +6,42 @@ import { getAvailableUsers } from '../features/adminUserTab/adminUserTabSlice' import '../features/adminRsuTab/Admin.css' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../store' +import { Box, Tab, Tabs, Typography } from '@mui/material' +import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom' +import AdminOrganizationTab from '../features/adminOrganizationTab/AdminOrganizationTab' +import AdminRsuTab from '../features/adminRsuTab/AdminRsuTab' +import AdminUserTab from '../features/adminUserTab/AdminUserTab' +import { NotFound } from './404' +import { SecureStorageManager } from '../managers' + +interface TabPanelProps { + children?: React.ReactNode +} + +function TabPanel(props: TabPanelProps) { + const { children, ...other } = props + + return ( +
    + + {children} + +
    + ) +} function Admin() { const dispatch: ThunkDispatch = useDispatch() + const location = useLocation() + + const getSelectedTab = () => location.pathname.split('/')[3] || 'rsus' + + const [value, setValue] = useState(getSelectedTab()) + + const handleChange = (_e, newValue) => { + console.log(value, newValue) + setValue(newValue) + } useEffect(() => { dispatch(updateRsuTableData()) @@ -18,38 +49,112 @@ function Admin() { }, [dispatch]) return ( -
    -

    CV Manager Admin Interface

    - - - -

    RSUs

    -
    - -

    Users

    -
    - -

    Organizations

    -
    -
    - - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    -
    -
    + <> + {SecureStorageManager.getUserRole() !== 'admin' ? ( +
    + +
    + ) : ( +
    +

    CV Manager Admin Interface

    + + + + + + + + + + + } /> + } /> + } /> + } /> + + } + /> + + + +
    + )} + ) } diff --git a/webapp/src/pages/Map.test.tsx b/webapp/src/pages/Map.test.tsx index d3d9112d..bc80d068 100644 --- a/webapp/src/pages/Map.test.tsx +++ b/webapp/src/pages/Map.test.tsx @@ -11,10 +11,10 @@ it('snapshot bsmCoordinates wzdx', () => { value: { rsuCounts: {}, mapList: [], - bsmData: [], - bsmStart: '2023-05-10T03:24:00', - bsmEnd: '2023-05-10T03:25:00', - bsmCoordinates: [ + geoMsgData: [], + geoMsgStart: '2023-05-10T03:24:00', + geoMsgEnd: '2023-05-10T03:25:00', + geoMsgCoordinates: [ [-104.9903, 39.7392], [-104.9904, 39.7393], [-104.9905, 39.7391], @@ -54,10 +54,10 @@ it('snapshot bsmCoordinates wzdx', () => { ) - fireEvent.click(screen.queryByText('RSU')) + fireEvent.click(screen.queryByText('RSU Viewer')) fireEvent.click(screen.queryByText('Heatmap')) - fireEvent.click(screen.queryByText('BSM Viewer')) - fireEvent.click(screen.queryByText('WZDx')) + fireEvent.click(screen.queryByText('V2X Msg Viewer')) + fireEvent.click(screen.queryByText('WZDx Viewer')) expect(replaceChaoticIds(container)).toMatchSnapshot() }) diff --git a/webapp/src/pages/Map.tsx b/webapp/src/pages/Map.tsx index 647e3dc8..7f52908f 100644 --- a/webapp/src/pages/Map.tsx +++ b/webapp/src/pages/Map.tsx @@ -11,6 +11,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker' import Slider from 'rc-slider' import Select from 'react-select' +import { DropdownList } from 'react-widgets' import { selectRsuOnlineStatus, selectMapList, @@ -22,15 +23,15 @@ import { selectRsuIpv4, selectDisplayMap, selectHeatMapData, - selectAddBsmPoint, - selectBsmStart, - selectBsmEnd, - selectBsmDateError, - selectBsmData, - selectBsmCoordinates, - selectBsmFilter, - selectBsmFilterStep, - selectBsmFilterOffset, + selectAddGeoMsgPoint, + selectGeoMsgStart, + selectGeoMsgEnd, + selectGeoMsgDateError, + selectGeoMsgData, + selectGeoMsgCoordinates, + selectGeoMsgFilter, + selectGeoMsgFilterStep, + selectGeoMsgFilterOffset, // actions selectRsu, @@ -38,14 +39,15 @@ import { getIssScmsStatus, getMapData, getRsuLastOnline, - toggleBsmPointSelect, - clearBsm, - updateBsmPoints, - updateBsmData, - updateBsmDate, - setBsmFilter, - setBsmFilterStep, - setBsmFilterOffset, + toggleGeoMsgPointSelect, + clearGeoMsg, + updateGeoMsgPoints, + updateGeoMsgData, + updateGeoMsgDate, + setGeoMsgFilter, + setGeoMsgFilterStep, + setGeoMsgFilterOffset, + changeGeoMsgType, } from '../generalSlices/rsuSlice' import { selectWzdxData, getWzdxData } from '../generalSlices/wzdxSlice' import { selectOrganizationName } from '../generalSlices/userSlice' @@ -75,11 +77,12 @@ import { } from '@mui/material' import 'rc-slider/assets/index.css' -import './css/BsmMap.css' +import './css/MsgMap.css' import './css/Map.css' import { WZDxFeature, WZDxWorkZoneFeed } from '../types/wzdx/WzdxWorkZoneFeed42' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../store' +import { MessageType, GeoMessageType } from '../types/MessageTypes' // @ts-ignore: workerClass does not exist in typed mapboxgl // eslint-disable-next-line import/no-webpack-loader-syntax @@ -99,7 +102,7 @@ function MapPage(props: MapPageProps) { const rsuCounts = useSelector(selectRsuCounts) const selectedRsu = useSelector(selectSelectedRsu) const mapList = useSelector(selectMapList) - const msgType = useSelector(selectMsgType) + const countsMsgType = useSelector(selectMsgType) const issScmsStatusData = useSelector(selectIssScmsStatusData) const rsuOnlineStatus = useSelector(selectRsuOnlineStatus) const rsuIpv4 = useSelector(selectRsuIpv4) @@ -109,16 +112,16 @@ function MapPage(props: MapPageProps) { const heatMapData = useSelector(selectHeatMapData) - const bsmData = useSelector(selectBsmData) - const bsmCoordinates = useSelector(selectBsmCoordinates) - const addBsmPoint = useSelector(selectAddBsmPoint) - const startBsmDate = useSelector(selectBsmStart) - const endBsmDate = useSelector(selectBsmEnd) - const bsmDateError = useSelector(selectBsmDateError) + const geoMsgData = useSelector(selectGeoMsgData) + const geoMsgCoordinates = useSelector(selectGeoMsgCoordinates) + const addGeoMsgPoint = useSelector(selectAddGeoMsgPoint) + const startGeoMsgDate = useSelector(selectGeoMsgStart) + const endGeoMsgDate = useSelector(selectGeoMsgEnd) + const msgViewerDateError = useSelector(selectGeoMsgDateError) - const filter = useSelector(selectBsmFilter) - const filterStep = useSelector(selectBsmFilterStep) - const filterOffset = useSelector(selectBsmFilterOffset) + const filter = useSelector(selectGeoMsgFilter) + const filterStep = useSelector(selectGeoMsgFilterStep) + const filterOffset = useSelector(selectGeoMsgFilterOffset) const wzdxData = useSelector(selectWzdxData) @@ -143,7 +146,7 @@ function MapPage(props: MapPageProps) { }) // BSM layer local state variables - const [bsmPolygonSource, setBsmPolygonSource] = useState>({ + const [geoMsgPolygonSource, setGeoMsgPolygonSource] = useState>({ type: 'Feature', geometry: { type: 'Polygon', @@ -151,21 +154,30 @@ function MapPage(props: MapPageProps) { }, properties: {}, }) - const [bsmPointSource, setBsmPointSource] = useState>({ + const [bsmPointSource, setMsgPointSource] = useState>({ type: 'FeatureCollection', features: [], }) - const [baseDate, setBaseDate] = useState(new Date(startBsmDate)) + const [baseDate, setBaseDate] = useState(new Date(startGeoMsgDate)) const [startDate, setStartDate] = useState(new Date(baseDate.getTime() + 60000 * filterOffset * filterStep)) const [endDate, setEndDate] = useState(new Date(startDate.getTime() + 60000 * filterStep)) const stepOptions = [ - { value: 1, label: '1 minute', options: [] as number[] }, - { value: 5, label: '5 minutes', options: [] as number[] }, - { value: 15, label: '15 minutes', options: [] as number[] }, - { value: 30, label: '30 minutes', options: [] as number[] }, - { value: 60, label: '60 minutes', options: [] as number[] }, + { value: 1, label: '1 minute' }, + { value: 5, label: '5 minutes' }, + { value: 15, label: '15 minutes' }, + { value: 30, label: '30 minutes' }, + { value: 60, label: '60 minutes' }, ] + const [selectedOption, setSelectedOption] = useState({ value: 60, label: '60 minutes' }) + + function stepValueToOption(val: number) { + for (var i = 0; i < stepOptions.length; i++) { + if (stepOptions[i].value === val) { + return stepOptions[i] + } + } + } // WZDx layer local state variables const [selectedWZDxMarkerIndex, setSelectedWZDxMarkerIndex] = useState(null) @@ -175,6 +187,13 @@ function MapPage(props: MapPageProps) { const [activeLayers, setActiveLayers] = useState(['rsu-layer']) + // Vendor filter local state variable + const [selectedVendor, setSelectedVendor] = useState('Select Vendor') + const vendorArray: string[] = ['Select Vendor', 'Commsignia', 'Yunex', 'Kapsch'] + const setVendor = (newVal) => { + setSelectedVendor(newVal) + } + // useEffects for Mapbox useEffect(() => { const listener = (e: KeyboardEvent) => { @@ -199,45 +218,52 @@ function MapPage(props: MapPageProps) { // useEffects for BSM layer useEffect(() => { - const localBaseDate = new Date(startBsmDate) + const localBaseDate = new Date(startGeoMsgDate) const localStartDate = new Date(localBaseDate.getTime() + 60000 * filterOffset * filterStep) const localEndDate = new Date(new Date(localStartDate).getTime() + 60000 * filterStep) setBaseDate(localBaseDate) setStartDate(localStartDate) setEndDate(localEndDate) - }, [startBsmDate, filterOffset, filterStep]) + }, [startGeoMsgDate, filterOffset, filterStep]) useEffect(() => { - if (!startBsmDate) { + if (!startGeoMsgDate) { dateChanged(new Date(), 'start') } - if (!endBsmDate) { + if (!endGeoMsgDate) { dateChanged(new Date(), 'end') } }, []) useEffect(() => { - if (activeLayers.includes('bsm-layer')) { - setBsmPolygonSource((prevPolygonSource) => { + if (activeLayers.includes('msg-viewer-layer')) { + setGeoMsgPolygonSource((prevPolygonSource) => { return { ...prevPolygonSource, geometry: { ...prevPolygonSource.geometry, - coordinates: [[...bsmCoordinates]], + coordinates: [[...geoMsgCoordinates]], }, } as GeoJSON.Feature }) const pointSourceFeatures = [] as Array> - if ((bsmData?.length ?? 0) > 0) { - for (const [, val] of Object.entries([...bsmData])) { - const bsmDate = new Date(val['properties']['time']) - if (bsmDate >= startDate && bsmDate <= endDate) { + if ((geoMsgData?.length ?? 0) > 0) { + const start_date = new Date(geoMsgData.slice(-1)[0]['properties']['time']) + const end_date = new Date(geoMsgData[0]['properties']['time']) + if (filter) { + // trim start / end dates to the first / last records + dateChanged(start_date, 'start') + dateChanged(end_date, 'end') + } + for (const [, val] of Object.entries([...geoMsgData])) { + const msgViewerDate = new Date(val['properties']['time']) + if (msgViewerDate >= startDate && msgViewerDate <= endDate) { pointSourceFeatures.push(val) } } } else { - bsmCoordinates.forEach((point: number[]) => { + geoMsgCoordinates.forEach((point: number[]) => { pointSourceFeatures.push({ type: 'Feature', geometry: { @@ -249,11 +275,13 @@ function MapPage(props: MapPageProps) { }) } - setBsmPointSource((prevPointSource) => { + console.debug('geoMsgData pointSourceFeatures: ', pointSourceFeatures) + + setMsgPointSource((prevPointSource) => { return { ...prevPointSource, features: pointSourceFeatures } }) } - }, [bsmCoordinates, bsmData, startDate, endDate, activeLayers]) + }, [geoMsgCoordinates, geoMsgData, startDate, endDate, activeLayers]) useEffect(() => { if (activeLayers.includes('rsu-layer')) { @@ -286,26 +314,27 @@ function MapPage(props: MapPageProps) { function dateChanged(e: Date, type: 'start' | 'end') { try { - let mst = DateTime.fromISO(e.toISOString()) - mst.setZone('America/Denver') - dispatch(updateBsmDate({ type, date: mst.toString() })) + let date = DateTime.fromISO(e.toISOString()) + date.setZone(DateTime.local().zoneName) + + dispatch(updateGeoMsgDate({ type, date: date.toString() })) } catch (err) { console.error('Encountered issue updating date: ', err.message) } } - const addBsmPointToCoordinates = (point: { lat: number; lng: number }) => { + const addGeoMsgPointToCoordinates = (point: { lat: number; lng: number }) => { const pointArray = [point.lng, point.lat] - if (bsmCoordinates.length > 1) { - if (bsmCoordinates[0] === bsmCoordinates.slice(-1)[0]) { - let tmp = [...bsmCoordinates] + if (geoMsgCoordinates.length > 1) { + if (geoMsgCoordinates[0] === geoMsgCoordinates.slice(-1)[0]) { + let tmp = [...geoMsgCoordinates] tmp.pop() - dispatch(updateBsmPoints([...tmp, pointArray, bsmCoordinates[0]])) + dispatch(updateGeoMsgPoints([...tmp, pointArray, geoMsgCoordinates[0]])) } else { - dispatch(updateBsmPoints([...bsmCoordinates, pointArray, bsmCoordinates[0]])) + dispatch(updateGeoMsgPoints([...geoMsgCoordinates, pointArray, geoMsgCoordinates[0]])) } } else { - dispatch(updateBsmPoints([...bsmCoordinates, pointArray])) + dispatch(updateGeoMsgPoints([...geoMsgCoordinates, pointArray])) } } @@ -324,14 +353,6 @@ function MapPage(props: MapPageProps) { } } - function defaultSlider(val: number) { - for (var i = 0; i < stepOptions.length; i++) { - if (stepOptions[i].value === val) { - return stepOptions[i].label - } - } - } - // useEffects for WZDx layers useEffect(() => { if (selectedWZDxMarkerIndex !== null) setSelectedWZDxMarker(wzdxMarkers[selectedWZDxMarkerIndex]) @@ -405,7 +426,7 @@ function MapPage(props: MapPageProps) { }} >
    openPopup(index)}> - Work Zone Icon + Work Zone Icon
    ) @@ -487,7 +508,7 @@ function MapPage(props: MapPageProps) { const layers: (LayerProps & { label: string })[] = [ { id: 'rsu-layer', - label: 'RSU', + label: 'RSU Viewer', type: 'symbol', }, { @@ -524,13 +545,13 @@ function MapPage(props: MapPageProps) { }, }, { - id: 'bsm-layer', - label: 'BSM Viewer', + id: 'msg-viewer-layer', + label: 'V2X Msg Viewer', type: 'symbol', }, { id: 'wzdx-layer', - label: 'WZDx', + label: 'WZDx Viewer', type: 'line', paint: { 'line-color': '#F29543', @@ -609,16 +630,21 @@ function MapPage(props: MapPageProps) { else if (event.target.value === 'none') handleNoneStatus() } - const handleButtonToggle = (event: React.SyntheticEvent, origin: 'config' | 'bsm') => { + const handleButtonToggle = (event: React.SyntheticEvent, origin: 'config' | 'msgViewer') => { if (origin === 'config') { dispatch(toggleConfigPointSelect()) - if (addBsmPoint) dispatch(toggleBsmPointSelect()) - } else if (origin === 'bsm') { - dispatch(toggleBsmPointSelect()) + if (addGeoMsgPoint) dispatch(toggleGeoMsgPointSelect()) + } else if (origin === 'msgViewer') { + dispatch(toggleGeoMsgPointSelect()) if (addConfigPoint) dispatch(toggleConfigPointSelect()) } } + const messageViewerTypes = EnvironmentVars.getMessageViewerTypes() + const messageTypeOptions = messageViewerTypes.map((type) => { + return { value: type, label: type } + }) + return (
    @@ -687,9 +713,10 @@ function MapPage(props: MapPageProps) { ) : null} + {activeLayers.includes('rsu-layer') ? ( +
    +

    Filter RSUs

    +

    Vendor

    + { + setVendor(value) + }} + /> +
    + ) : null}
    setViewState(evt.viewState)} onClick={(e) => { - if (addBsmPoint) { - addBsmPointToCoordinates(e.lngLat) + if (addGeoMsgPoint) { + addGeoMsgPointToCoordinates(e.lngLat) } if (addConfigPoint) { addConfigPointToCoordinates(e.lngLat) @@ -748,7 +791,8 @@ function MapPage(props: MapPageProps) { )} {rsuData?.map( (rsu) => - activeLayers.includes('rsu-layer') && [ + activeLayers.includes('rsu-layer') && + (selectedVendor === 'Select Vendor' || rsu['properties']['manufacturer_name'] === selectedVendor) && [ )} - {activeLayers.includes('bsm-layer') && ( + {activeLayers.includes('msg-viewer-layer') && (
    - {bsmCoordinates.length > 2 ? ( - + {geoMsgCoordinates.length > 2 ? ( + @@ -876,7 +920,7 @@ function MapPage(props: MapPageProps) {
    )}

    - {msgType} Counts: {selectedRsuCount} + {countsMsgType} Counts: {selectedRsuCount}

    @@ -884,8 +928,8 @@ function MapPage(props: MapPageProps) { - {activeLayers.includes('bsm-layer') && - (filter ? ( + {activeLayers.includes('msg-viewer-layer') && + (filter && geoMsgData.length > 0 ? (

    @@ -896,24 +940,34 @@ function MapPage(props: MapPageProps) { { - dispatch(setBsmFilterOffset(e)) + dispatch(setGeoMsgFilterOffset(e)) }} /> + {/*

    */}
    o.label === countsMsgType)} + placeholder="Select Message Type" + className="selectContainer" + onChange={(value) => dispatch(changeGeoMsgType(value.value))} + /> +
    { - dateChanged(e.toDate(), 'start') + if (e !== null) { + dateChanged(e.toDate(), 'start') + } }} - renderInput={(params) => } + renderInput={(params) => ( + + )} />
    @@ -950,13 +1024,21 @@ function MapPage(props: MapPageProps) { { - dateChanged(e.toDate(), 'end') + if (e !== null) { + dateChanged(e.toDate(), 'end') + } }} - renderInput={(params) => } + renderInput={(params) => ( + + )} />
    @@ -964,17 +1046,12 @@ function MapPage(props: MapPageProps) {
    - {bsmDateError ? ( -
    - Date ranges longer than 24 hours are not supported due to their large data sets -
    - ) : null}
    ))}
    @@ -1098,6 +1175,7 @@ const theme = createTheme({ }) const dateTimeOptions: Intl.DateTimeFormatOptions = { + month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', diff --git a/webapp/src/pages/RsuMapView.test.tsx b/webapp/src/pages/RsuMapView.test.tsx deleted file mode 100644 index 9473211e..00000000 --- a/webapp/src/pages/RsuMapView.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import { render } from '@testing-library/react' -import RsuMapView from './RsuMapView' -import { Provider } from 'react-redux' -import { setupStore } from '../store' -import { replaceChaoticIds } from '../utils/test-utils' - -it('should take a snapshot', () => { - const initialState = { - rsu: { - value: { - selectedRsu: { - properties: { - ipv4_address: '1.1.1.1', - }, - geometry: { coordinates: [0, 1] }, - }, - selectedSrm: [{ long: 1, lat: 1 }], - srmSsmList: [ - { ip: '1.1.1.1', type: 'srmTx' }, - { ip: '1.1.1.1', type: 'other' }, - ], - rsuMapData: { - features: [ - { - properties: { - ingressPath: 'true', - egressPath: 'true', - }, - }, - ], - }, - }, - }, - } - const { container } = render( - - - - ) - - expect(replaceChaoticIds(container)).toMatchSnapshot() -}) diff --git a/webapp/src/pages/RsuMapView.tsx b/webapp/src/pages/RsuMapView.tsx deleted file mode 100644 index ad1f67fa..00000000 --- a/webapp/src/pages/RsuMapView.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import React, { useEffect, useState } from 'react' -import Map, { Source, Layer, LineLayer, CircleLayer } from 'react-map-gl' -import { Container } from 'reactstrap' -import EnvironmentVars from '../EnvironmentVars' -import './css/RsuMapView.css' -import SsmSrmItem from '../components/SsmSrmItem' -import { useSelector, useDispatch } from 'react-redux' -import { - selectRsuMapData, - selectSelectedRsu, - selectSelectedSrm, - selectMapDate, - selectSsmDisplay, - selectRsuIpv4, - selectSrmSsmList, - - // actions - toggleMapDisplay, - toggleSsmSrmDisplay, - getSsmSrmData, - setSelectedSrm, -} from '../generalSlices/rsuSlice' -import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' -import { RootState } from '../store' - -interface RsuMapViewProps { - auth: boolean -} - -function RsuMapView(props: RsuMapViewProps) { - const dispatch: ThunkDispatch = useDispatch() - - const rsuMapData = useSelector(selectRsuMapData) - const selectedRsu = useSelector(selectSelectedRsu) - const selectedSrm = useSelector(selectSelectedSrm) - const mapDate = useSelector(selectMapDate) - const ssmDisplay = useSelector(selectSsmDisplay) - const rsuIpv4 = useSelector(selectRsuIpv4) - const srmSsmList = useSelector(selectSrmSsmList) - - const [srmCount, setSrmCount] = useState(0) - const [ssmCount, setSsmCount] = useState(0) - const [msgList, setMsgList] = useState([]) - const [egressData, setEgressData] = useState>({ - type: 'FeatureCollection' as 'FeatureCollection', - features: [], - }) - const [ingressData, setIngressData] = useState>({ - type: 'FeatureCollection' as 'FeatureCollection', - features: [], - }) - - useEffect(() => { - dispatch(getSsmSrmData()) - }, [dispatch]) - - useEffect(() => { - let localSrmCount = 0 - let localSsmCount = 0 - let localMsgList = [] - console.error('srmSsmList', srmSsmList) - for (const elem of srmSsmList) { - if (elem.ip === rsuIpv4) { - localMsgList.push(elem) - if (elem.type === 'srmTx') { - localSrmCount += 1 - } else { - localSsmCount += 1 - } - } - } - setSrmCount(localSrmCount) - setSsmCount(localSsmCount) - setMsgList(localMsgList) - }, [srmSsmList, rsuIpv4]) - - useEffect(() => { - const ingressDataFeatures = [] as Array> - const egressDataFeatures = [] as Array> - - Object.entries(rsuMapData?.['features'] ?? []).map((feature) => { - if (feature[1].properties.ingressPath === 'true') { - ingressDataFeatures.push(feature[1]) - } - return null - }) - Object.entries(rsuMapData?.['features'] ?? []).map((feature) => { - if (feature[1].properties.egressPath === 'true') { - egressDataFeatures.push(feature[1]) - } - return null - }) - - setIngressData((prevIngressData) => { - return { ...prevIngressData, features: ingressDataFeatures } - }) - setEgressData((prevEgressData) => { - return { ...prevEgressData, features: egressDataFeatures } - }) - }, [rsuMapData]) - - const srmData: GeoJSON.FeatureCollection = { - type: 'FeatureCollection' as 'FeatureCollection', - features: [], - } - - if (selectedSrm.length > 0) { - srmData.features.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [selectedSrm[0].long, selectedSrm[0].lat], - }, - properties: {}, - }) - } - - const ingressLayer: LineLayer = { - id: 'ingressLayer', - type: 'line', - minzoom: 14, - source: 'ingressData', - layout: { - 'line-join': 'round', - 'line-cap': 'round', - }, - paint: { - 'line-color': 'rgb(50,205,50)', - 'line-width': 3, - }, - } - - const egressLayer: LineLayer = { - id: 'egressLayer', - type: 'line', - minzoom: 14, - source: 'egressData', - layout: { - 'line-join': 'round', - 'line-cap': 'round', - }, - paint: { - 'line-color': 'rgb(203, 4, 4)', - 'line-width': 3, - }, - } - - const srmLayer: CircleLayer = { - id: 'srmMarker', - type: 'circle', - source: 'srmData', - minzoom: 12, - paint: { - 'circle-radius': 8, - 'circle-color': 'rgb(14, 32, 82)', - }, - } - - const [viewState, setViewState] = useState({ - latitude: selectedRsu.geometry.coordinates[1], - longitude: selectedRsu.geometry.coordinates[0], - zoom: 17, - }) - - return ( -
    - - { - setViewState(evt.viewState) - }} - > - - - - - - - - - - - - -
    MAP data from {mapDate}
    - {ssmDisplay ? ( -
    - -

    SSM / SRM Data For {rsuIpv4}

    -
    -

    Time

    -

    Request Id

    -

    Role

    -

    Status

    -

    Display

    -
    - {msgList.map((index) => ( - - ))} -

    Total Counts

    -
    -

    SSM: {ssmCount}

    -

    SRM: {srmCount}

    -
    -
    - ) : ( - - )} -
    -
    -

    Reference

    -
    -
    -
    -

    - Ingress Lane

    -
    -
    -
    -

    - Egress Lane

    -
    -
    -
    -

    - SSM (table)

    -
    -
    -
    -

    - SRM (table)

    -
    -
    -
    -

    - SRM (map)

    -
    -
    -
    - ) -} - -export default RsuMapView diff --git a/webapp/src/pages/__snapshots__/Admin.test.tsx.snap b/webapp/src/pages/__snapshots__/Admin.test.tsx.snap index 9cde05db..a7dcd62d 100644 --- a/webapp/src/pages/__snapshots__/Admin.test.tsx.snap +++ b/webapp/src/pages/__snapshots__/Admin.test.tsx.snap @@ -5,718 +5,34 @@ exports[`should take a snapshot 1`] = `
    -

    - CV Manager Admin Interface -

    -
      - - - -
    -
    +
    +

    -
    -
    -
    -
    -

    - CV Manager RSUs - - -

    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - - ​ - - -
    - -
    - -
    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - -
    - - - - - - -
    - - Milepost - - - -
    -
    -
    - - IP Address - - - -
    -
    -
    - - Primary Route - - - -
    -
    -
    - - RSU Model - - - -
    -
    -
    - - Serial Number - - - -
    -
    - - Actions - -
    - No records to display -
    -
    -
    -
    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    +
    + +

    diff --git a/webapp/src/pages/__snapshots__/Map.test.tsx.snap b/webapp/src/pages/__snapshots__/Map.test.tsx.snap index bc0283dd..44ce67cc 100644 --- a/webapp/src/pages/__snapshots__/Map.test.tsx.snap +++ b/webapp/src/pages/__snapshots__/Map.test.tsx.snap @@ -26,7 +26,7 @@ exports[`snapshot bsmCoordinates wzdx 1`] = ` class="legend-input" type="checkbox" /> - RSU + RSU Viewer
    - BSM Viewer + V2X Msg Viewer
    - WZDx + WZDx Viewer

    @@ -98,6 +98,82 @@ exports[`snapshot bsmCoordinates wzdx 1`] = ` Clear
    +
    +
    + + +
    +
    +
    + Select Message Type +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    @@ -109,11 +185,13 @@ exports[`snapshot bsmCoordinates wzdx 1`] = ` data-shrink="true" for="mui-1" id="mui-1-label" + style="color: black;" > Select start date
    Select end date
    - RSU + RSU Viewer
    - BSM Viewer + V2X Msg Viewer
    - WZDx + WZDx Viewer
    @@ -353,6 +433,74 @@ exports[`snapshot bsmData clicked 1`] = ` SCMS Status
    +
    +

    + Filter RSUs +

    +

    + Vendor +

    + +
    -
    -
    -
    -
    - -
    - MAP data from -
    - -
    -
    -

    - Reference -

    -
    -
    -
    -

    - - Ingress Lane -

    -
    -
    -
    -

    - - Egress Lane -

    -
    -
    -
    -

    - - SSM (table) -

    -
    -
    -
    -

    - - SRM (table) -

    -
    -
    -
    -

    - - SRM (map) -

    -
    -
    -
    -
    -`; diff --git a/webapp/src/pages/css/Map.css b/webapp/src/pages/css/Map.css index e7b1b05a..f763176c 100644 --- a/webapp/src/pages/css/Map.css +++ b/webapp/src/pages/css/Map.css @@ -8,7 +8,7 @@ background: #0e2052; margin: 10px; border-radius: 15px; - height: 160px; + height: fit-content; } .legend-header { @@ -31,11 +31,14 @@ font-family: Arial, Helvetica, sans-serif; font-size: 18px; color: white; + margin-top: 2px; } .legend-input { + width: 16px; + height: 16px; margin-right: 10px; - accent-color: #d16d15; + accent-color: #b55e12; } .legend-grid { @@ -50,6 +53,44 @@ margin-top: 10px; } +.vendor-filter-div { + z-index: 90; + background: #0e2052; + border-radius: 15px; + margin-top: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-evenly; + width: fit-content; + height: fit-content; + padding: 5px 20px; + margin-left: 10px; + font-family: Arial, Helvetica, sans-serif; + color: #ffffff; +} + +.vendor-filter-div h2 { + margin-bottom: 10px; +} + +.vendor-filter-div h4 { + font-weight: normal; + margin-bottom: 4px; +} + +.form-dropdown { + width: 100%; +} + +.rw-dropdown-list-value { + font-family: Arial, Helvetica, sans-serif; +} + +.rw-list-option { + font-family: Arial, Helvetica, sans-serif; +} + .rsu-status-label { display: block; font-family: Arial, Helvetica, sans-serif; @@ -63,7 +104,9 @@ .rsu-status-input { margin-right: 10px; - accent-color: #d16d15; + accent-color: #b55e12; + width: 16px; + height: 16px; } .map-button { @@ -89,3 +132,9 @@ .contained-button { width: 234.65px; } + +.mapboxgl-popup-close-button { + font-size: 24px; + top: 0; + right: 4px; +} diff --git a/webapp/src/pages/css/BsmMap.css b/webapp/src/pages/css/MsgMap.css similarity index 98% rename from webapp/src/pages/css/BsmMap.css rename to webapp/src/pages/css/MsgMap.css index 56f08b89..073131f7 100644 --- a/webapp/src/pages/css/BsmMap.css +++ b/webapp/src/pages/css/MsgMap.css @@ -36,7 +36,7 @@ width: fit-content; padding: 5px; font-size: 1em; - background: #d16d15; + background: #b55e12; border-radius: 10px; border-color: white; cursor: pointer; @@ -90,7 +90,7 @@ width: fit-content; padding: 5px; font-size: 1em; - background: #d16d15; + background: #b55e12; border-radius: 10px; border-color: white; cursor: pointer; diff --git a/webapp/src/styles/index.ts b/webapp/src/styles/index.ts index d5a41cc0..70486feb 100644 --- a/webapp/src/styles/index.ts +++ b/webapp/src/styles/index.ts @@ -34,9 +34,9 @@ export const theme = createTheme({ }, text: { primary: '#ffffff', - secondary: '#ffffff', - disabled: '#ffffff', - hint: '#ffffff', + secondary: '#d16d15', + disabled: '#000000', + hint: '#0e2052', }, divider: '#333', background: { @@ -54,8 +54,134 @@ export const theme = createTheme({ }, }, MuiTextField: {}, + MuiInputLabel: { + styleOverrides: { + // This is the global theme styling for Form.Label + root: { + color: 'white', // Set the color to white + }, + }, + }, }, input: { color: '#11ff00', }, }) + +export const tableTheme = createTheme({ + palette: { + common: { + black: '#000000', + white: '#ffffff', + }, + primary: { + main: '#ffffff', + light: '#0e2052', + contrastTextColor: '#0e2052', + }, + secondary: { + main: '#333333', + light: '#0e2052', + contrastTextColor: '#0e2052', + }, + text: { + primary: '#ffffff', + secondary: '#ffffff', + disabled: '#ffffff', + hint: '#ffffff', + }, + divider: '#333', + background: { + paper: '#333', + default: '#1c1d1f', + }, + }, + components: { + MuiPaper: { + styleOverrides: { + root: { + border: '1.5px solid #ffffff', + }, + }, + }, + MuiIcon: { + styleOverrides: { + root: { + color: '#333333', + backgroundColor: '#dadde5', + borderRadius: '50%', + padding: '2px', + }, + }, + }, + MuiInputAdornment: { + styleOverrides: { + positionStart: { + color: '#333333', + }, + positionEnd: { + color: '#333333', + }, + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + backgroundColor: '#dadde5', + color: '#333333', + }, + }, + }, + MuiButtonBase: { + styleOverrides: { + root: { + color: '#ffffff', + }, + }, + }, + MuiCheckbox: { + styleOverrides: { + root: { + color: '#ffffff', + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + color: '#ffffff', + }, + }, + }, + MuiToolbar: { + styleOverrides: { + root: { + color: '#333333', + backgroundColor: '#dadde5', + }, + }, + }, + MuiIconButton: { + styleOverrides: { + colorInherit: { + color: '#333333', + }, + }, + }, + MuiTableCell: { + styleOverrides: { + footer: { + backgroundColor: '#dadde5', + borderBottom: 'None', + }, + }, + }, + MuiTablePagination: { + styleOverrides: { + root: { + color: '#333333', + }, + }, + }, + }, +}) diff --git a/webapp/src/types/MessageTypes.d.ts b/webapp/src/types/MessageTypes.d.ts index 9c41b2cb..b20762e4 100644 --- a/webapp/src/types/MessageTypes.d.ts +++ b/webapp/src/types/MessageTypes.d.ts @@ -1 +1,2 @@ -export type MessageType = 'BSM' | 'SSM' | 'SPAT' | 'SRM' | 'MAP' +export type MessageType = 'BSM' | 'PSM' | 'SSM' | 'SPAT' | 'SRM' | 'MAP' +export type GeoMessageType = 'BSM' | 'PSM' diff --git a/webapp/yarn.lock b/webapp/yarn.lock new file mode 100644 index 00000000..4581822a --- /dev/null +++ b/webapp/yarn.lock @@ -0,0 +1,13247 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@adobe/css-tools@^4.0.1": + version "4.3.2" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz" + integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.6" + resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.8.3": + version "7.23.5" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.11.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.16.0", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.4.0-0", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.0": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz" + integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.10" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.10" + "@babel/types" "^7.18.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/eslint-parser@^7.16.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz" + integrity sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw== + dependencies: + "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.1" + +"@babel/generator@^7.18.10", "@babel/generator@^7.23.6", "@babel/generator@^7.7.2": + 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" + +"@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5": + 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" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz" + integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6": + 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/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" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.23.6", "@babel/helper-create-class-features-plugin@^7.23.7": + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz" + integrity sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g== + 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.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz" + integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.3.1" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-define-polyfill-provider@^0.4.4": + version "0.4.4" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz" + integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA== + 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" + +"@babel/helper-define-polyfill-provider@^0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz" + integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q== + 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" + +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20": + 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== + +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0", "@babel/helper-function-name@^7.23.0": + 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/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.18.6", "@babel/helper-hoist-variables@^7.22.5": + 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/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0": + version "7.23.0" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.18.9", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.18.6", "@babel/helper-optimise-call-expression@^7.22.5": + 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/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.22.20": + version "7.22.20" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + 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/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.6": + 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/types" "^7.22.5" + +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + +"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + +"@babel/helper-wrap-function@^7.18.9": + version "7.20.5" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz" + integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + +"@babel/helpers@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz" + integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6": + version "7.23.6" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz" + integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + 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.18.6.tgz" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz" + integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-proposal-optional-chaining" "^7.20.7" + +"@babel/plugin-proposal-async-generator-functions@^7.20.1": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz" + integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz" + integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-decorators@^7.16.4": + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.7.tgz" + integrity sha512-b1s5JyeMvqj7d9m9KhJNHKc18gEJiSyVzVX3bwbiPalQBQpuvfPh6lA9F7Kk/dWH0TIiXRpB9yicwijY6buPng== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.23.7" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-decorators" "^7.23.3" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz" + integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.20.2": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz" + integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + 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-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + 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== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": + 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" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + 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.14.5" + +"@babel/plugin-syntax-decorators@^7.23.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.23.3.tgz" + integrity sha512-cf7Niq4/+/juY67E0PbgH0TDhLQ5J7zS8C/Q5FFx+DWyrRa9sUQdTXkjqKu8zGvuqr7vw1muKiukseihU+PJDA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + 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-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + 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-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.14.5", "@babel/plugin-syntax-flow@^7.23.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz" + integrity sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-assertions@^7.20.0": + version "7.20.0" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz" + integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-import-meta@^7.8.3": + 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.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + 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" + +"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.23.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz" + integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + 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.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + 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.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": + 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-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + 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" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + 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.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + 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-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + 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.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": + 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.14.5" + +"@babel/plugin-syntax-typescript@^7.23.3", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz" + integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz" + integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz" + integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.20.2": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz" + integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-classes@^7.20.2": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz" + integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz" + integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/template" "^7.20.7" + +"@babel/plugin-transform-destructuring@^7.20.2": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz" + integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-flow-strip-types@^7.16.0": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz" + integrity sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-flow" "^7.23.3" + +"@babel/plugin-transform-for-of@^7.18.8": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz" + integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.19.6": + version "7.20.11" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz" + integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== + dependencies: + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-modules-commonjs@^7.19.6", "@babel/plugin-transform-modules-commonjs@^7.23.3": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz" + integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.19.6": + version "7.20.11" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz" + integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-identifier" "^7.19.1" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": + version "7.20.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz" + integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz" + integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-constant-elements@^7.12.1": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.23.3.tgz" + integrity sha512-zP0QKq/p6O42OL94udMgSfKXyse4RyJ0JqbQ34zDAONWjyrEsghYEyTSK5FIpmXmCpB55SHokL1cRRKHv8L2Qw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz" + integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz" + integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.18.6" + +"@babel/plugin-transform-react-jsx@^7.14.9", "@babel/plugin-transform-react-jsx@^7.18.6": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz" + integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.21.0" + +"@babel/plugin-transform-react-pure-annotations@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz" + integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.20.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz" + integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + regenerator-transform "^0.15.1" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-runtime@^7.16.4": + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz" + integrity sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.7" + babel-plugin-polyfill-corejs3 "^0.8.7" + babel-plugin-polyfill-regenerator "^0.5.4" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.19.0": + version "7.20.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz" + integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typescript@^7.23.3": + version "7.23.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz" + integrity sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.23.6" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-typescript" "^7.23.3" + +"@babel/plugin-transform-unicode-escapes@^7.18.10": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz" + integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.2": + version "7.20.2" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz" + integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.20.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.20.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@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.20.0" + "@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-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.20.2" + "@babel/plugin-transform-classes" "^7.20.2" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.20.2" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.19.6" + "@babel/plugin-transform-modules-commonjs" "^7.19.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.6" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.20.2" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz" + integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-transform-react-display-name" "^7.18.6" + "@babel/plugin-transform-react-jsx" "^7.18.6" + "@babel/plugin-transform-react-jsx-development" "^7.18.6" + "@babel/plugin-transform-react-pure-annotations" "^7.18.6" + +"@babel/preset-typescript@^7.16.0": + version "7.23.3" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz" + integrity sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/plugin-transform-modules-commonjs" "^7.23.3" + "@babel/plugin-transform-typescript" "^7.23.3" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.0", "@babel/runtime@^7.19.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0", "@babel/runtime@^7.9.2": + version "7.23.8" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.3.3": + version "7.22.15" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9", "@babel/traverse@^7.20.5", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@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.22.6" + "@babel/parser" "^7.23.6" + "@babel/types" "^7.23.6" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.23.6" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@csstools/normalize.css@*": + version "12.1.1" + resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz" + integrity sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ== + +"@csstools/postcss-cascade-layers@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz" + integrity sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA== + dependencies: + "@csstools/selector-specificity" "^2.0.2" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-color-function@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz" + integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-font-format-keywords@^1.0.1": + 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== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-hwb-function@^1.0.2": + 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== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-ic-unit@^1.0.1": + 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== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-is-pseudo-class@^2.0.7": + 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== + dependencies: + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-nested-calc@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz" + integrity sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^1.0.1": + 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== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^1.1.1": + 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== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": + 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== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-stepped-value-functions@^1.0.1": + 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== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-text-decoration-shorthand@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz" + integrity sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz" + integrity sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-unset-value@^1.0.2": + 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== + +"@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": + version "2.2.0" + resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" + integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== + +"@date-io/core@^2.15.0", "@date-io/core@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/core/-/core-2.16.0.tgz" + integrity sha512-DYmSzkr+jToahwWrsiRA2/pzMEtz9Bq1euJwoOuYwuwIYXnZFtHajY2E6a1VNVDc9jP8YUXK1BvnZH9mmT19Zg== + +"@date-io/date-fns@^2.15.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.16.0.tgz" + integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/date-fns@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.16.0.tgz" + integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/dayjs@^2.15.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.16.0.tgz" + integrity sha512-y5qKyX2j/HG3zMvIxTobYZRGnd1FUW2olZLS0vTj7bEkBQkjd2RO7/FEwDY03Z1geVGlXKnzIATEVBVaGzV4Iw== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/luxon@^2.15.0": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.16.1.tgz" + integrity sha512-aeYp5K9PSHV28946pC+9UKUi/xMMYoaGelrpDibZSgHu2VWHXrr7zWLEr+pMPThSs5vt8Ei365PO+84pCm37WQ== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/moment@^2.15.0": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/moment/-/moment-2.16.1.tgz" + integrity sha512-JkxldQxUqZBfZtsaCcCMkm/dmytdyq5pS1RxshCQ4fHhsvP5A7gSqPD22QbVXMcJydi3d3v1Y8BQdUKEuGACZQ== + dependencies: + "@date-io/core" "^2.16.0" + +"@emotion/babel-plugin@^11.10.5": + version "11.10.5" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz" + integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.17.12" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/serialize" "^1.1.1" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.1.3" + +"@emotion/cache@^11.10.5", "@emotion/cache@^11.4.0": + version "11.10.5" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz" + integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== + dependencies: + "@emotion/memoize" "^0.8.0" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + stylis "4.1.3" + +"@emotion/core@^11.0.0": + version "11.0.0" + resolved "https://registry.npmjs.org/@emotion/core/-/core-11.0.0.tgz" + integrity sha512-w4sE3AmHmyG6RDKf6mIbtHpgJUSJ2uGvPQb8VXFL7hFjMPibE8IiehG8cMX3Ztm4svfCQV6KqusQbeIOkurBcA== + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/hash@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz" + integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== + +"@emotion/is-prop-valid@^1.1.0", "@emotion/is-prop-valid@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz" + integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== + dependencies: + "@emotion/memoize" "^0.8.0" + +"@emotion/memoize@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz" + integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== + +"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.1.4", "@emotion/react@^11.10.4", "@emotion/react@^11.10.5", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.8.1", "@emotion/react@^11.9.0": + version "11.10.5" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz" + integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/cache" "^11.10.5" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz" + integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA== + dependencies: + "@emotion/hash" "^0.9.0" + "@emotion/memoize" "^0.8.0" + "@emotion/unitless" "^0.8.0" + "@emotion/utils" "^1.2.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz" + integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== + +"@emotion/styled@^11.10.4", "@emotion/styled@^11.10.5", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1": + version "11.10.5" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz" + integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.10.5" + "@emotion/is-prop-valid" "^1.2.0" + "@emotion/serialize" "^1.1.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" + "@emotion/utils" "^1.2.0" + +"@emotion/stylis@^0.8.4": + version "0.8.5" + resolved "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@^0.7.4": + version "0.7.5" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/unitless@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz" + integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz" + integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== + +"@emotion/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz" + integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== + +"@emotion/weak-memoize@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz" + integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.10.0" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^1.3.2": + version "1.4.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" + integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@hello-pangea/dnd@^16.0.0": + version "16.2.0" + resolved "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.2.0.tgz" + integrity sha512-inACvMcvvLr34CG0P6+G/3bprVKhwswxjcsFUSJ+fpOGjhvDj9caiA9X3clby0lgJ6/ILIJjyedHZYECB7GAgA== + dependencies: + "@babel/runtime" "^7.19.4" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.0.4" + redux "^4.2.0" + use-memo-one "^1.1.3" + +"@hookform/error-message@^2.0.1": + version "2.0.1" + resolved "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz" + integrity sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg== + +"@humanwhocodes/config-array@^0.10.4": + version "0.10.7" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz" + integrity sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/gitignore-to-minimatch@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz" + integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^26.6.2" + jest-util "^26.6.2" + slash "^3.0.0" + +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + +"@jest/console@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz" + integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + slash "^3.0.0" + +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" + micromatch "^4.0.2" + p-each-series "^2.1.0" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.8.1" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" + micromatch "^4.0.4" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== + dependencies: + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== + dependencies: + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + +"@jest/expect-utils@^29.5.0": + version "29.5.0" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz" + integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== + dependencies: + jest-get-type "^29.4.3" + +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== + dependencies: + "@jest/types" "^26.6.2" + "@sinonjs/fake-timers" "^6.0.1" + "@types/node" "*" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== + dependencies: + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" + "@types/node" "*" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" + +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" + +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.4" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^4.0.3" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.0.2" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^7.0.0" + optionalDependencies: + node-notifier "^8.0.0" + +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^8.1.0" + +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.9" + source-map "^0.6.0" + +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== + dependencies: + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-result@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz" + integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== + dependencies: + "@jest/console" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== + dependencies: + "@jest/test-result" "^26.6.2" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== + dependencies: + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" + +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^26.6.2" + babel-plugin-istanbul "^6.0.0" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-regex-util "^26.0.0" + jest-util "^26.6.2" + micromatch "^4.0.2" + pirates "^4.0.1" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + +"@jest/transform@^29.5.0": + version "29.5.0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz" + integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== + dependencies: + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@mapbox/geojson-rewind@^0.5.2": + version "0.5.2" + resolved "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz" + integrity sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA== + dependencies: + get-stream "^6.0.1" + minimist "^1.2.6" + +"@mapbox/jsonlint-lines-primitives@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz" + integrity sha1-zlblOfg1UrWNENZy6k1vya3HsjQ= + +"@mapbox/mapbox-gl-supported@^2.0.1": + version "2.0.1" + resolved "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz" + integrity sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ== + +"@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0", "@mapbox/point-geometry@0.1.0": + version "0.1.0" + resolved "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz" + integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI= + +"@mapbox/tiny-sdf@^2.0.6": + version "2.0.6" + resolved "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz" + integrity sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA== + +"@mapbox/unitbezier@^0.0.1": + version "0.0.1" + resolved "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz" + integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw== + +"@mapbox/vector-tile@^1.3.1": + version "1.3.1" + resolved "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz" + integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw== + dependencies: + "@mapbox/point-geometry" "~0.1.0" + +"@mapbox/whoots-js@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz" + integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== + +"@material-table/core@^6.1.6": + version "6.1.6" + resolved "https://registry.npmjs.org/@material-table/core/-/core-6.1.6.tgz" + integrity sha512-fbxDj+kqCsdU0MBhJai/5RBgLCnV7R4WboZjYUtOzX5TQcGBCYk/BAae67rwG++0IUSdmKENVawPMNbV0qWROw== + dependencies: + "@babel/runtime" "^7.19.0" + "@date-io/core" "^2.16.0" + "@date-io/date-fns" "^2.16.0" + "@emotion/core" "^11.0.0" + "@emotion/react" "^11.10.4" + "@emotion/styled" "^11.10.4" + "@hello-pangea/dnd" "^16.0.0" + "@mui/icons-material" ">=5.10.6" + "@mui/material" ">=5.10.7" + "@mui/x-date-pickers" "^5.0.3" + classnames "^2.3.2" + date-fns "^2.29.3" + debounce "^1.2.1" + deep-eql "^4.1.1" + deepmerge "^4.2.2" + prop-types "^15.8.1" + react-double-scrollbar "0.0.15" + uuid "^9.0.0" + zustand "^4.1.1" + +"@material-ui/core@^4.12.3": + version "4.12.3" + resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz" + integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/styles" "^4.11.4" + "@material-ui/system" "^4.12.1" + "@material-ui/types" "5.1.0" + "@material-ui/utils" "^4.11.2" + "@types/react-transition-group" "^4.2.0" + clsx "^1.0.4" + hoist-non-react-statics "^3.3.2" + popper.js "1.16.1-lts" + prop-types "^15.7.2" + react-is "^16.8.0 || ^17.0.0" + react-transition-group "^4.4.0" + +"@material-ui/styles@^4.11.4": + version "4.11.5" + resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz" + integrity sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA== + dependencies: + "@babel/runtime" "^7.4.4" + "@emotion/hash" "^0.8.0" + "@material-ui/types" "5.1.0" + "@material-ui/utils" "^4.11.3" + clsx "^1.0.4" + csstype "^2.5.2" + hoist-non-react-statics "^3.3.2" + jss "^10.5.1" + jss-plugin-camel-case "^10.5.1" + jss-plugin-default-unit "^10.5.1" + jss-plugin-global "^10.5.1" + jss-plugin-nested "^10.5.1" + jss-plugin-props-sort "^10.5.1" + jss-plugin-rule-value-function "^10.5.1" + jss-plugin-vendor-prefixer "^10.5.1" + prop-types "^15.7.2" + +"@material-ui/system@^4.12.1": + version "4.12.2" + resolved "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz" + integrity sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/utils" "^4.11.3" + csstype "^2.5.2" + prop-types "^15.7.2" + +"@material-ui/types@5.1.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz" + integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== + +"@material-ui/utils@^4.11.2", "@material-ui/utils@^4.11.3": + version "4.11.3" + resolved "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz" + integrity sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg== + dependencies: + "@babel/runtime" "^7.4.4" + prop-types "^15.7.2" + react-is "^16.8.0 || ^17.0.0" + +"@mui/base@5.0.0-alpha.119": + version "5.0.0-alpha.119" + resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.119.tgz" + integrity sha512-XA5zhlYfXi67u613eIF0xRmktkatx6ERy3h+PwrMN5IcWFbgiL1guz8VpdXON+GWb8+G7B8t5oqTFIaCqaSAeA== + dependencies: + "@babel/runtime" "^7.21.0" + "@emotion/is-prop-valid" "^1.2.0" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.11" + "@popperjs/core" "^2.11.6" + clsx "^1.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/core-downloads-tracker@^5.11.12": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.12.tgz" + integrity sha512-LHh8HZQ5nPVcW5QnyLwkAZ40txc/S2bzKMQ3bTO+5mjuwAJ2AzQrjZINLVy1geY7ei1pHXVqO1hcWHg/QdT44w== + +"@mui/icons-material@^5.11.9", "@mui/icons-material@>=5.10.6": + version "5.11.11" + resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz" + integrity sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw== + dependencies: + "@babel/runtime" "^7.21.0" + +"@mui/material@^5.0.0", "@mui/material@^5.11.9", "@mui/material@^5.4.1", "@mui/material@>=5.10.7": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/material/-/material-5.11.12.tgz" + integrity sha512-M6BiIeJjySeEzWeiFJQ9pIjJy6mx5mHPWeMT99wjQdAmA2GxCQhE9A0fh6jQP4jMmYzxhOIhjsGcp0vSdpseXg== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/base" "5.0.0-alpha.119" + "@mui/core-downloads-tracker" "^5.11.12" + "@mui/system" "^5.11.12" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.12" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + react-is "^18.2.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.11.12": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.12.tgz" + integrity sha512-hnJ0svNI1TPeWZ18E6DvES8PB4NyMLwal6EyXf69rTrYqT6wZPLjB+HiCYfSOCqU/fwArhupSqIIkQpDs8CkAw== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/utils" "^5.11.12" + prop-types "^15.8.1" + +"@mui/styled-engine-sc@^5.11.9": + version "5.11.11" + resolved "https://registry.npmjs.org/@mui/styled-engine-sc/-/styled-engine-sc-5.11.11.tgz" + integrity sha512-6+HsfcKHlhjQklDoEup7Itl+Xgn+BCsqEpIdIIhlxED4YlOZ38xghxIKrx78XFZznTorbhAspUgDDKIaB5vDMg== + dependencies: + "@babel/runtime" "^7.21.0" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.11.11": + version "5.11.11" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz" + integrity sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg== + dependencies: + "@babel/runtime" "^7.21.0" + "@emotion/cache" "^11.10.5" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/system@^5.11.12", "@mui/system@^5.4.1", "@mui/system@>=5.10.7": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.11.12.tgz" + integrity sha512-sYjsXkiwKpZDC3aS6O/6KTjji0jGINLQcrD5EJ5NTkIDiLf19I4HJhnufgKqlTWNfoDBlRohuTf3TzfM06c4ug== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/private-theming" "^5.11.12" + "@mui/styled-engine" "^5.11.11" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.12" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/types@^7.2.3": + version "7.2.3" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz" + integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw== + +"@mui/utils@^5.10.3", "@mui/utils@^5.11.11", "@mui/utils@^5.11.12": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.11.12.tgz" + integrity sha512-5vH9B/v8pzkpEPO2HvGM54ToXV6cFdAn8UrvdN8TMEEwpn/ycW0jLiyBcgUlPsQ+xha7hqXCPQYHaYFDIcwaiw== + dependencies: + "@babel/runtime" "^7.21.0" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@^5.0.15", "@mui/x-date-pickers@^5.0.3": + version "5.0.20" + resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.20.tgz" + integrity sha512-ERukSeHIoNLbI1C2XRhF9wRhqfsr+Q4B1SAw2ZlU7CWgcG8UBOxgqRKDEOVAIoSWL+DWT6GRuQjOKvj6UXZceA== + dependencies: + "@babel/runtime" "^7.18.9" + "@date-io/core" "^2.15.0" + "@date-io/date-fns" "^2.15.0" + "@date-io/dayjs" "^2.15.0" + "@date-io/luxon" "^2.15.0" + "@date-io/moment" "^2.15.0" + "@mui/utils" "^5.10.3" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + prop-types "^15.7.2" + react-transition-group "^4.4.5" + rifm "^0.12.1" + +"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": + version "5.1.1-v1" + resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" + integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== + dependencies: + eslint-scope "5.1.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pmmmwh/react-refresh-webpack-plugin@^0.5.3": + version "0.5.11" + resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz" + integrity sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ== + dependencies: + ansi-html-community "^0.0.8" + common-path-prefix "^3.0.0" + core-js-pure "^3.23.3" + error-stack-parser "^2.0.6" + find-up "^5.0.0" + html-entities "^2.1.0" + loader-utils "^2.0.4" + schema-utils "^3.0.0" + source-map "^0.7.3" + +"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.5", "@popperjs/core@^2.11.6", "@popperjs/core@^2.6.0": + version "2.11.6" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + +"@react-aria/ssr@^3.2.0": + version "3.4.1" + resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.4.1.tgz" + integrity sha512-NmhoilMDyIfQiOSdQgxpVH2tC2u85Y0mVijtBNbI9kcDYLEiW/r6vKYVKtkyU+C4qobXhGMPfZ70PTc0lysSVA== + dependencies: + "@swc/helpers" "^0.4.14" + +"@react-keycloak/core@^3.2.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@react-keycloak/core/-/core-3.2.0.tgz" + integrity sha512-1yzU7gQzs+6E1v6hGqxy0Q+kpMHg9sEcke2yxZR29WoU8KNE8E50xS6UbI8N7rWsgyYw8r9W1cUPCOF48MYjzw== + dependencies: + react-fast-compare "^3.2.0" + +"@react-keycloak/web@3.4.0": + version "3.4.0" + resolved "https://registry.npmjs.org/@react-keycloak/web/-/web-3.4.0.tgz" + integrity sha512-yKKSCyqBtn7dt+VckYOW1IM5NW999pPkxDZOXqJ6dfXPXstYhOQCkTZqh8l7UL14PkpsoaHDh7hSJH8whah01g== + dependencies: + "@babel/runtime" "^7.9.0" + "@react-keycloak/core" "^3.2.0" + hoist-non-react-statics "^3.3.2" + +"@react-oauth/google@^0.2.8": + version "0.2.8" + resolved "https://registry.npmjs.org/@react-oauth/google/-/google-0.2.8.tgz" + integrity sha512-W3sRcU6kSZMGUOk10Vy5kPZPzvsi7+UpM2MxnT6fMVp+whDMKCVope5R01gwRydK9OI+0rozAARCD2NgrbkV7w== + +"@reduxjs/toolkit@^1.9.1": + version "1.9.3" + resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz" + integrity sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg== + dependencies: + immer "^9.0.16" + redux "^4.2.0" + redux-thunk "^2.4.2" + reselect "^4.1.7" + +"@restart/hooks@^0.4.5", "@restart/hooks@^0.4.6", "@restart/hooks@^0.4.7": + version "0.4.7" + resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz" + integrity sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A== + dependencies: + dequal "^2.0.2" + +"@restart/ui@^1.4.1": + version "1.4.1" + resolved "https://registry.npmjs.org/@restart/ui/-/ui-1.4.1.tgz" + integrity sha512-J7wFOx2DcmkBqCqiZgDsggLO7faiNh4Nv1/v80FmbRgP+MYpwaVDKKXLC69DA4+ejgNIsBP5ORtC74EZqO1j8A== + dependencies: + "@babel/runtime" "^7.18.3" + "@popperjs/core" "^2.11.5" + "@react-aria/ssr" "^3.2.0" + "@restart/hooks" "^0.4.7" + "@types/warning" "^3.0.0" + dequal "^2.0.2" + dom-helpers "^5.2.0" + uncontrollable "^7.2.1" + warning "^4.0.3" + +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^11.2.1": + version "11.2.1" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" + integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rushstack/eslint-patch@^1.1.0": + version "1.7.0" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz" + integrity sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg== + +"@sinclair/typebox@^0.24.1": + version "0.24.51" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz" + integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== + +"@sinclair/typebox@^0.25.16": + version "0.25.24" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== + +"@sinonjs/commons@^1.7.0": + version "1.8.6" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz" + integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz" + integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== + +"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz" + integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz" + integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz" + integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== + +"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz" + integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== + +"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz" + integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== + +"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz" + integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== + +"@svgr/babel-plugin-transform-svg-component@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz" + integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== + +"@svgr/babel-preset@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz" + integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" + "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" + "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" + "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" + "@svgr/babel-plugin-transform-svg-component" "^5.5.0" + +"@svgr/core@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz" + integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== + dependencies: + "@svgr/plugin-jsx" "^5.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.0" + +"@svgr/hast-util-to-babel-ast@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz" + integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== + dependencies: + "@babel/types" "^7.12.6" + +"@svgr/plugin-jsx@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz" + integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== + dependencies: + "@babel/core" "^7.12.3" + "@svgr/babel-preset" "^5.5.0" + "@svgr/hast-util-to-babel-ast" "^5.5.0" + svg-parser "^2.0.2" + +"@svgr/plugin-svgo@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz" + integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== + dependencies: + cosmiconfig "^7.0.0" + deepmerge "^4.2.2" + svgo "^1.2.2" + +"@svgr/webpack@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz" + integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== + dependencies: + "@babel/core" "^7.12.3" + "@babel/plugin-transform-react-constant-elements" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@babel/preset-react" "^7.12.5" + "@svgr/core" "^5.5.0" + "@svgr/plugin-jsx" "^5.5.0" + "@svgr/plugin-svgo" "^5.5.0" + loader-utils "^2.0.0" + +"@swc/helpers@^0.4.14": + version "0.4.14" + resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz" + integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== + dependencies: + tslib "^2.4.0" + +"@syncfusion/ej2-base@~19.4.52": + version "19.4.52" + resolved "https://registry.npmjs.org/@syncfusion/ej2-base/-/ej2-base-19.4.52.tgz" + integrity sha512-BMJGjscgl5CGpWQW1y1osv+rcKBPYgPBpSfz8FA0XcqhL6wNjGomALmxHznk7QGWC/4bdUhx6AKsGHutMGm/PA== + dependencies: + "@syncfusion/ej2-icons" "~19.4.52" + +"@syncfusion/ej2-buttons@~19.4.52", "@syncfusion/ej2-buttons@~19.4.53", "@syncfusion/ej2-buttons@~19.4.55": + version "19.4.55" + resolved "https://registry.npmjs.org/@syncfusion/ej2-buttons/-/ej2-buttons-19.4.55.tgz" + integrity sha512-Ebvh5GfvOEQaB51EI6a9Cfrmkfmu9gbPeRmLPkXZPEokUSxIqyCP216Nm8NlOeEK3EbPxrOy/L1X9m1nOBM40Q== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + +"@syncfusion/ej2-calendars@19.4.56": + version "19.4.56" + resolved "https://registry.npmjs.org/@syncfusion/ej2-calendars/-/ej2-calendars-19.4.56.tgz" + integrity sha512-QeBsu2OgJF4ZIRjP4H5U27Ge5nTAYC77r4wo4udb7Z8qTQzuhH/J0b6Jj3g3ptxGUUjzLDOL7OpONRnsXzC3JA== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-buttons" "~19.4.55" + "@syncfusion/ej2-inputs" "~19.4.52" + "@syncfusion/ej2-lists" "~19.4.55" + "@syncfusion/ej2-popups" "~19.4.53" + +"@syncfusion/ej2-data@~19.4.54": + version "19.4.54" + resolved "https://registry.npmjs.org/@syncfusion/ej2-data/-/ej2-data-19.4.54.tgz" + integrity sha512-btJDjKepcaVGtNqVoblhYFhx4sAW1ju+Nq1CBPn4kO0AtRPBQ3Gi47uQgsstkxNMDrv+UG1hNi7PhHpPNOtwUQ== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + +"@syncfusion/ej2-icons@~19.4.52": + version "19.4.52" + resolved "https://registry.npmjs.org/@syncfusion/ej2-icons/-/ej2-icons-19.4.52.tgz" + integrity sha512-hkEvuVA/soEdqFwFSGbj2Z9u6Ajb+4HZNYIKtw6aCwjn/FE5vFuGMFBdkLEX9BRkUl/e/uvyP6/TKBM/nfkY8A== + +"@syncfusion/ej2-inputs@~19.4.52": + version "19.4.52" + resolved "https://registry.npmjs.org/@syncfusion/ej2-inputs/-/ej2-inputs-19.4.52.tgz" + integrity sha512-141wT1W8DbXM50A1RVHOtruCyPncF6CDOU8RdhtFx9ydbXvbSXH+2i+vKr8UK23qC07z8+YB8/wV7ZAQ5vI0Bw== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-buttons" "~19.4.52" + "@syncfusion/ej2-popups" "~19.4.52" + "@syncfusion/ej2-splitbuttons" "~19.4.52" + +"@syncfusion/ej2-lists@~19.4.55": + version "19.4.55" + resolved "https://registry.npmjs.org/@syncfusion/ej2-lists/-/ej2-lists-19.4.55.tgz" + integrity sha512-EGAcCc2mFOt99oOuT+wXSPEiyfiyHoBSJnAW5pvdCU1mI7FYcArCPVNLY5ynCgngQ7Ih0XxFPkuzEC0qmwlokA== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-buttons" "~19.4.55" + "@syncfusion/ej2-data" "~19.4.54" + +"@syncfusion/ej2-popups@~19.4.52", "@syncfusion/ej2-popups@~19.4.53": + version "19.4.53" + resolved "https://registry.npmjs.org/@syncfusion/ej2-popups/-/ej2-popups-19.4.53.tgz" + integrity sha512-B0Uen6IFsmPR3Ws8NIIszJ2ECpWrH7VFCqcGL1DQnp9tm6+iEafv84ziffVoC4nGj2mHaHl7tNSoZTnOo/TcgQ== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-buttons" "~19.4.53" + +"@syncfusion/ej2-react-base@~19.4.56": + version "19.4.56" + resolved "https://registry.npmjs.org/@syncfusion/ej2-react-base/-/ej2-react-base-19.4.56.tgz" + integrity sha512-mi1OKYgGfOe2MKcnMBuioYbdJea4IXCbFvUxG1WEZI5n3t6CxKH4ZWum7IgBO1kcTkaCdhs8Aj0ACFXUFmOwOQ== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + +"@syncfusion/ej2-react-calendars@^19.4.56": + version "19.4.56" + resolved "https://registry.npmjs.org/@syncfusion/ej2-react-calendars/-/ej2-react-calendars-19.4.56.tgz" + integrity sha512-o5bNc+hct5Jj/8CwvflYxUExddP0gkXpNsXNw3iGu2GCTGEnZ/Xb6OVXv8ammrCcB/Ck8vEliNTOKAEqdMLslA== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-calendars" "19.4.56" + "@syncfusion/ej2-react-base" "~19.4.56" + +"@syncfusion/ej2-splitbuttons@~19.4.52": + version "19.4.52" + resolved "https://registry.npmjs.org/@syncfusion/ej2-splitbuttons/-/ej2-splitbuttons-19.4.52.tgz" + integrity sha512-EmUh/3gwW04nCF/0UUfS6w3nl6Sy62ysPFqzwFTu9972DHr4R5W7t147PMJnFRa9Q0i/ud5a9/jERU7quaxBDw== + dependencies: + "@syncfusion/ej2-base" "~19.4.52" + "@syncfusion/ej2-popups" "~19.4.52" + +"@testing-library/dom@^8.0.0": + version "8.20.0" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz" + integrity sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^5.16.5": + version "5.16.5" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz" + integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA== + dependencies: + "@adobe/css-tools" "^4.0.1" + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^12.1.2": + version "12.1.5" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "<18.0.0" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7", "@types/babel__core@^7.1.9": + version "7.20.0" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz" + integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.18.3" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz" + integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + dependencies: + "@babel/types" "^7.3.0" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/classnames@^2.3.1": + version "2.3.1" + resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz" + integrity sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A== + dependencies: + classnames "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1": + version "8.21.1" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz" + integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.41" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz" + integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/geojson@*": + version "7946.0.10" + resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + +"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": + version "4.1.6" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz" + integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + dependencies: + "@types/node" "*" + +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.14" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz" + integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@*", "@types/jest@^29.5.8": + version "29.5.8" + resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz" + integrity sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mapbox-gl@^2.6.0": + version "2.7.10" + resolved "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz" + integrity sha512-nMVEcu9bAcenvx6oPWubQSPevsekByjOfKjlkr+8P91vawtkxTnopDoXXq1Qn/f4cg3zt0Z2W9DVsVsKRNXJTw== + dependencies: + "@types/geojson" "*" + +"@types/mime@*", "@types/mime@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@^20.6.2": + version "20.6.3" + resolved "https://registry.npmjs.org/@types/node/-/node-20.6.3.tgz" + integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prettier@^2.0.0", "@types/prettier@^2.1.5": + version "2.7.2" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== + +"@types/prop-types@*", "@types/prop-types@^15.7.5": + version "15.7.5" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/q@^1.5.1": + version "1.5.8" + resolved "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz" + integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== + +"@types/qs@*": + version "6.9.11" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz" + integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-dom@^16.8 || ^17.0 || ^18.0", "@types/react-dom@^18.2.7": + version "18.2.7" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz" + integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== + dependencies: + "@types/react" "*" + +"@types/react-dom@<18.0.0": + version "17.0.20" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.20.tgz" + integrity sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA== + dependencies: + "@types/react" "^17" + +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + +"@types/react-tabs@^5.0.5": + version "5.0.5" + resolved "https://registry.npmjs.org/@types/react-tabs/-/react-tabs-5.0.5.tgz" + integrity sha512-3CZTmjR7nNrZnYbnxp/DtK5e82mhM22dN47aYObmYLcp9fC1XjIEF0mLzGKFl1fR6/R8X7DyGh9hO6lON6LVkQ== + dependencies: + react-tabs "*" + +"@types/react-transition-group@^4.2.0", "@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.4", "@types/react-transition-group@^4.4.5": + version "4.4.5" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@^16.8.6 || ^17.0.0", "@types/react@^17", "@types/react@^17.0.0", "@types/react@^17.0.0 || ^18.0.0", "@types/react@>=16.14.8", "@types/react@>=16.9.11": + version "17.0.65" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.65.tgz" + integrity sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/semver@^7.3.12": + version "7.5.6" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.5" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz" + integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.5" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz" + integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ== + dependencies: + "@types/jest" "*" + +"@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + +"@types/warning@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" + integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA== + +"@types/ws@^8.5.5": + version "8.5.10" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^15.0.0": + version "15.0.15" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz" + integrity sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg== + dependencies: + "@types/yargs-parser" "*" + +"@types/yargs@^16.0.0": + version "16.0.5" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz" + integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ== + dependencies: + "@types/yargs-parser" "*" + +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^4.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@^5.5.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz" + integrity sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw== + dependencies: + "@typescript-eslint/utils" "5.62.0" + +"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.5.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== + dependencies: + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== + dependencies: + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@^5.58.0", "@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.3, abab@^2.0.5: + version "2.0.6" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +address@^1.0.1, address@^1.1.2: + version "1.2.2" + resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^8.6.0, ajv@>=8: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^8.8.2, ajv@^8.9.0: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + 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" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^5.0.0, aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.7" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== + +array.prototype.findlastindex@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.reduce@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz" + integrity sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +array.prototype.tosorted@^1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz" + integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + +asap@~2.0.3, asap@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + +async@^3.2.3: + version "3.2.5" + resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + +asynciterator.prototype@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz" + integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== + dependencies: + has-symbols "^1.0.3" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^10.4.13: + version "10.4.17" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz" + integrity sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg== + dependencies: + browserslist "^4.22.2" + caniuse-lite "^1.0.30001578" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== + +axios@^1.6.5: + version "1.6.5" + resolved "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz" + integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" + +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + +babel-jest@^27.4.2, babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== + dependencies: + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz" + integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q== + dependencies: + "@jest/transform" "^29.5.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-loader@^8.2.3: + version "8.3.0" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-istanbul@^6.0.0, babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +babel-plugin-named-asset-import@^0.3.8: + version "0.3.8" + resolved "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz" + integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== + +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs2@^0.4.7: + version "0.4.8" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz" + integrity sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.5.0" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-corejs3@^0.8.7: + version "0.8.7" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz" + integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.4" + core-js-compat "^3.33.1" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + +babel-plugin-polyfill-regenerator@^0.5.4: + version "0.5.5" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz" + integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.5.0" + +"babel-plugin-styled-components@>= 1.12.0": + version "2.0.7" + resolved "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz" + integrity sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-module-imports" "^7.16.0" + babel-plugin-syntax-jsx "^6.18.0" + lodash "^4.17.11" + picomatch "^2.3.0" + +babel-plugin-syntax-export-extensions@^6.8.0: + version "6.13.0" + resolved "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz" + integrity sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg== + +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz" + integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== + +babel-plugin-transform-export-extensions@^6.22.0: + version "6.22.0" + resolved "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz" + integrity sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg== + dependencies: + babel-plugin-syntax-export-extensions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-remove-prop-types@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@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-top-level-await" "^7.8.3" + +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== + dependencies: + babel-plugin-jest-hoist "^27.5.1" + babel-preset-current-node-syntax "^1.0.0" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + +babel-preset-react-app@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz" + integrity sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg== + dependencies: + "@babel/core" "^7.16.0" + "@babel/plugin-proposal-class-properties" "^7.16.0" + "@babel/plugin-proposal-decorators" "^7.16.4" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.0" + "@babel/plugin-proposal-numeric-separator" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.0" + "@babel/plugin-proposal-private-methods" "^7.16.0" + "@babel/plugin-transform-flow-strip-types" "^7.16.0" + "@babel/plugin-transform-react-display-name" "^7.16.0" + "@babel/plugin-transform-runtime" "^7.16.4" + "@babel/preset-env" "^7.16.4" + "@babel/preset-react" "^7.16.0" + "@babel/preset-typescript" "^7.16.0" + "@babel/runtime" "^7.16.3" + babel-plugin-macros "^3.1.0" + babel-plugin-transform-react-remove-prop-types "^0.4.24" + +babel-runtime@^6.22.0: + version "6.26.0" + resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" + integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.npmjs.org/base/-/base-0.11.2.tgz" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +bfj@^7.0.2: + version "7.1.0" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz" + integrity sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw== + dependencies: + bluebird "^3.7.2" + check-types "^11.2.3" + hoopy "^0.1.4" + jsonpath "^1.1.1" + tryer "^1.0.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + 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" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.22.2, "browserslist@>= 4", "browserslist@>= 4.21.0", browserslist@>=4: + version "4.22.2" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz" + integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== + dependencies: + caniuse-lite "^1.0.30001565" + electron-to-chromium "^1.4.601" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.1.0: + version "3.3.0" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0, camelcase@^6.2.0, camelcase@^6.2.1: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001578: + version "1.0.30001579" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz" + integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + +chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0: + 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" + +chalk@^4.0.2: + 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" + +chalk@^4.1.0: + 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" + +chalk@^4.1.2: + 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" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +char-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz" + integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== + +check-types@^11.2.3: + version "11.2.3" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz" + integrity sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== + +chokidar@^3.4.2, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@*, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.3.1, classnames@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + +clean-css@^5.2.2: + version "5.3.3" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== + dependencies: + source-map "~0.6.0" + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clsx@^1.0.4, clsx@^1.1.0, clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz" + integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + 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" + +color-name@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +colord@^2.9.1: + version "2.9.3" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confusing-browser-globals@^1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^2.0.0: + 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== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + +core-js-compat@^3.25.1, core-js-compat@^3.33.1: + version "3.35.0" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz" + integrity sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw== + dependencies: + browserslist "^4.22.2" + +core-js-pure@^3.23.3: + version "3.35.0" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.0.tgz" + integrity sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew== + +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + +core-js@^3.19.2: + version "3.35.0" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz" + integrity sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-fetch@^3.0.4, cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-js@^4.1.1: + version "4.2.0" + resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-blank-pseudo@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz" + integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== + dependencies: + postcss-selector-parser "^6.0.9" + +css-box-model@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== + +css-has-pseudo@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz" + integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== + dependencies: + postcss-selector-parser "^6.0.9" + +css-loader@^6.5.1: + version "6.9.0" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.9.0.tgz" + integrity sha512-3I5Nu4ytWlHvOP6zItjiHlefBNtrH+oehq8tnQa2kO305qpVyx9XNIT1CXIj5bgCJs7qICBCkgCYxQLKPANoLA== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.31" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.3" + postcss-modules-scope "^3.1.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + +css-minimizer-webpack-plugin@^3.2.0: + version "3.4.1" + resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz" + integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== + dependencies: + cssnano "^5.0.6" + jest-worker "^27.0.2" + postcss "^8.3.5" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-prefers-color-scheme@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz" + integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-vendor@^2.0.8: + version "2.0.8" + resolved "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== + dependencies: + "@babel/runtime" "^7.8.3" + is-in-browser "^1.0.2" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + +csscolorparser@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz" + integrity sha1-s085HupNqPPpgjHizNjfnAQfFxs= + +cssdb@^7.1.0: + version "7.10.0" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.10.0.tgz" + integrity sha512-yGZ5tmA57gWh/uvdQBHs45wwFY0IBh3ypABk5sEubPBPSzXzkNgsWReqx7gdx6uhC+QoFBe+V8JwBB9/hQ6cIA== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz" + integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg== + +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.1" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.0.6: + version "5.1.15" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== + dependencies: + cssnano-preset-default "^5.2.14" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.0.2, csso@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csstype@^2.5.2: + version "2.6.17" + resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz" + integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== + +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +csstype@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +date-arithmetic@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz" + integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg== + +date-fns@^2.0.0, date-fns@^2.25.0: + version "2.28.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz" + integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + +dayjs@^1.10.7, dayjs@^1.11.7, dayjs@^1.8.17: + version "1.11.7" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^2.3.3: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^2.6.0: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decimal.js@^10.2.1: + version "10.4.3" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-eql@^4.1.1: + version "4.1.3" + resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz" + integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz" + integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dequal@^2.0.2, dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +detect-port-alt@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domelementtype@1: + version "1.3.1" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.5.2: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +earcut@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^3.1.6: + version "3.1.9" + resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.4.601: + version "1.4.637" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.637.tgz" + integrity sha512-G7j3UCOukFtxVO1vWrPQUoDk3kL70mtvjc/DC/k2o7lE0wAdq+Vwp1ipagOow+BH0uVztFysLWbkM/RTIrbK3w== + +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== + +emittery@^0.7.1: + version "0.7.2" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz" + integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== + +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encoding@^0.1.0: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +env-cmd@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz" + integrity sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA== + dependencies: + commander "^4.0.0" + cross-spawn "^7.0.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-abstract@^1.17.2, es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-iterator-helpers@^1.0.12, es-iterator-helpers@^1.0.15: + version "1.0.15" + resolved "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz" + integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== + dependencies: + asynciterator.prototype "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.1" + es-abstract "^1.22.1" + es-set-tostringtag "^2.0.1" + function-bind "^1.1.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + iterator.prototype "^1.1.2" + safe-array-concat "^1.0.1" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-set-tostringtag@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz" + integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== + dependencies: + get-intrinsic "^1.2.2" + has-tostringtag "^1.0.0" + hasown "^2.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-react-app@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz" + integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== + dependencies: + "@babel/core" "^7.16.0" + "@babel/eslint-parser" "^7.16.3" + "@rushstack/eslint-patch" "^1.1.0" + "@typescript-eslint/eslint-plugin" "^5.5.0" + "@typescript-eslint/parser" "^5.5.0" + babel-preset-react-app "^10.0.1" + confusing-browser-globals "^1.0.11" + eslint-plugin-flowtype "^8.0.3" + eslint-plugin-import "^2.25.3" + eslint-plugin-jest "^25.3.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.27.1" + eslint-plugin-react-hooks "^4.3.0" + eslint-plugin-testing-library "^5.0.1" + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-flowtype@^8.0.3: + version "8.0.3" + resolved "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz" + integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== + dependencies: + lodash "^4.17.21" + string-natural-compare "^3.0.1" + +eslint-plugin-import@^2.25.3: + version "2.29.1" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jest@^25.3.0: + version "25.7.0" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz" + integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.8.0" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + dependencies: + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + +eslint-plugin-react-hooks@^4.3.0: + version "4.6.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.27.1: + version "7.33.2" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz" + integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== + dependencies: + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.12" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" + prop-types "^15.8.1" + resolve "^2.0.0-next.4" + semver "^6.3.1" + string.prototype.matchall "^4.0.8" + +eslint-plugin-testing-library@^5.0.1: + version "5.11.1" + resolved "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz" + integrity sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw== + dependencies: + "@typescript-eslint/utils" "^5.58.0" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.2.2" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-webpack-plugin@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz" + integrity sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w== + dependencies: + "@types/eslint" "^7.29.0 || ^8.4.1" + jest-worker "^28.0.2" + micromatch "^4.0.5" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + +eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", "eslint@^7.5.0 || ^8.0.0", eslint@^8.0.0, eslint@^8.1.0, eslint@^8.3.0, "eslint@>= 6", eslint@>=5, eslint@8.23.1: + version "8.23.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz" + integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg== + dependencies: + "@eslint/eslintrc" "^1.3.2" + "@humanwhocodes/config-array" "^0.10.4" + "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" + "@humanwhocodes/module-importer" "^1.0.1" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.1" + globals "^13.15.0" + globby "^11.1.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.6.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== + +esquery@^1.4.0: + version "1.5.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +exec-sh@^0.3.2: + version "0.3.6" + resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz" + integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz" + integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== + dependencies: + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + +expect@^29.0.0: + version "29.5.0" + resolved "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz" + integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== + dependencies: + "@jest/expect-utils" "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + +express@^4.17.3: + version "4.18.2" + resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.16.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz" + integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fbjs-css-vars@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== + +fbjs@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz" + integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== + dependencies: + cross-fetch "^3.1.5" + fbjs-css-vars "^1.0.0" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.30" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +filesize@^8.0.6: + version "8.0.7" + resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" + integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz" + integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + +follow-redirects@^1.0.0, follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fork-ts-checker-webpack-plugin@^6.5.0: + version "6.5.3" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz" + integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@types/json-schema" "^7.0.5" + chalk "^4.1.0" + chokidar "^3.4.2" + cosmiconfig "^6.0.0" + deepmerge "^4.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + memfs "^3.1.2" + minimatch "^3.0.4" + schema-utils "2.7.0" + semver "^7.3.2" + tapable "^1.0.0" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz" + integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.5" + resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +geojson-vt@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz" + integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0, get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + +gl-matrix@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz" + integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.3.10: + version "10.3.10" + resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.15.0: + version "13.24.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.0.4, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +grid-index@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz" + integrity sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz" + integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== + +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +harmony-reflect@^1.4.6: + version "1.6.2" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" + integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz" + integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz" + integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz" + integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +history@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/history/-/history-5.3.0.tgz" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-entities@^2.1.0, html-entities@^2.3.2: + version "2.4.0" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz" + integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-webpack-plugin@^5.5.0: + version "5.6.0" + resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz" + integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +hyphenate-style-name@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +idb@^7.0.1: + version "7.1.1" + resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" + integrity sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA== + dependencies: + harmony-reflect "^1.4.6" + +ieee754@^1.1.12: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + +immer@^9.0.16, immer@^9.0.7, immer@>=9.0: + version "9.0.17" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.17.tgz" + integrity sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg== + +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz" + integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" + integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz" + integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== + +is-map@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz" + integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + +is-root@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-set@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isarray@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" + integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.1" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.0.2, istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.8.5: + version "10.8.7" + resolved "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-canvas-mock@^2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz" + integrity sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A== + dependencies: + cssfontparser "^1.2.1" + moo-color "^1.0.2" + +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + dependencies: + "@jest/types" "^26.6.2" + execa "^4.0.0" + throat "^5.0.0" + +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== + dependencies: + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" + +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== + dependencies: + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + is-ci "^2.0.0" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" + prompts "^2.0.1" + yargs "^15.4.1" + +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== + dependencies: + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + prompts "^2.0.1" + yargs "^16.2.0" + +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" + chalk "^4.0.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" + jest-get-type "^26.3.0" + jest-jasmine2 "^26.6.3" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + micromatch "^4.0.2" + pretty-format "^26.6.2" + +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== + dependencies: + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-diff@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz" + integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-docblock@^26.0.0: + version "26.0.0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz" + integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== + dependencies: + detect-newline "^3.0.0" + +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== + dependencies: + detect-newline "^3.0.0" + +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + jest-get-type "^26.3.0" + jest-util "^26.6.2" + pretty-format "^26.6.2" + +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + jsdom "^16.4.0" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jsdom "^16.6.0" + +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== + dependencies: + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + jest-mock "^26.6.2" + jest-util "^26.6.2" + +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== + dependencies: + "@jest/types" "^26.6.2" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + jest-regex-util "^26.0.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + micromatch "^4.0.2" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" + +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== + dependencies: + "@jest/types" "^27.5.1" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +jest-haste-map@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz" + integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA== + dependencies: + "@jest/types" "^29.5.0" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + jest-worker "^29.5.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^26.6.2" + is-generator-fn "^2.0.0" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" + throat "^5.0.0" + +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + throat "^6.0.1" + +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== + dependencies: + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== + dependencies: + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + +jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-matcher-utils@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz" + integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.5.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz" + integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.5.0" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== + +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== + +jest-regex-util@^28.0.0: + version "28.0.2" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz" + integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== + dependencies: + "@jest/types" "^26.6.2" + jest-regex-util "^26.0.0" + jest-snapshot "^26.6.2" + +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== + dependencies: + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" + +jest-resolve@*, jest-resolve@^27.4.2, jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== + dependencies: + "@jest/types" "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-pnp-resolver "^1.2.2" + jest-util "^27.5.1" + jest-validate "^27.5.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== + dependencies: + "@jest/types" "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-pnp-resolver "^1.2.2" + jest-util "^26.6.2" + read-pkg-up "^7.0.1" + resolve "^1.18.1" + slash "^3.0.0" + +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.7.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-docblock "^26.0.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" + source-map-support "^0.5.6" + throat "^5.0.0" + +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.8.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + source-map-support "^0.5.6" + throat "^6.0.1" + +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + cjs-module-lexer "^0.6.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-regex-util "^26.0.0" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + slash "^3.0.0" + strip-bom "^4.0.0" + yargs "^15.4.1" + +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.9" + +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" + +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== + dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.0.0" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + natural-compare "^1.4.0" + pretty-format "^27.5.1" + semver "^7.3.2" + +jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + +jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" + camelcase "^6.0.0" + chalk "^4.0.0" + jest-get-type "^26.3.0" + leven "^3.1.0" + pretty-format "^26.6.2" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== + dependencies: + "@jest/types" "^27.5.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^27.5.1" + leven "^3.1.0" + pretty-format "^27.5.1" + +jest-watch-typeahead@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz" + integrity sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw== + dependencies: + ansi-escapes "^4.3.1" + chalk "^4.0.0" + jest-regex-util "^28.0.0" + jest-watcher "^28.0.0" + slash "^4.0.0" + string-length "^5.0.1" + strip-ansi "^7.0.1" + +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== + dependencies: + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^26.6.2" + string-length "^4.0.1" + +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== + dependencies: + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.5.1" + string-length "^4.0.1" + +jest-watcher@^28.0.0: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz" + integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== + dependencies: + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.10.2" + jest-util "^28.1.3" + string-length "^4.0.1" + +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^28.0.2: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" + integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz" + integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== + dependencies: + "@types/node" "*" + jest-util "^29.5.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^26.6.0: + version "26.6.3" + resolved "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== + dependencies: + "@jest/core" "^26.6.3" + import-local "^3.0.2" + jest-cli "^26.6.3" + +"jest@^27.0.0 || ^28.0.0", jest@^27.4.3: + version "27.5.1" + resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== + dependencies: + "@jest/core" "^27.5.1" + import-local "^3.0.2" + jest-cli "^27.5.1" + +jiti@^1.19.1: + version "1.21.0" + resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +js-sdsl@^4.1.4: + version "4.4.2" + resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz" + integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== + +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdom@^16.4.0, jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.0, json5@^2.2.1: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + +jsonpointer@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + +jss-plugin-camel-case@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz" + integrity sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.10.0" + +jss-plugin-default-unit@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz" + integrity sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.10.0" + +jss-plugin-global@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz" + integrity sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.10.0" + +jss-plugin-nested@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz" + integrity sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.10.0" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz" + integrity sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.10.0" + +jss-plugin-rule-value-function@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz" + integrity sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.10.0" + tiny-warning "^1.0.2" + +jss-plugin-vendor-prefixer@^10.5.1: + version "10.10.0" + resolved "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz" + integrity sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.8" + jss "10.10.0" + +jss@^10.5.1, jss@10.10.0: + version "10.10.0" + resolved "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz" + integrity sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^3.0.2" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +kdbush@^4.0.1, kdbush@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz" + integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA== + +keycloak-js@>=9.0.2, keycloak-js@21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/keycloak-js/-/keycloak-js-21.1.1.tgz" + integrity sha512-Viyhf0SOpu2jM/A33vpigSCFLo8l4yg8lqzaGyxXoZ3nGO9lo68B2LwJBDtgpzqDUh8DK//yCOzdWuR2CT4keA== + dependencies: + base64-js "^1.5.1" + js-sha256 "^0.9.0" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^3.0.2, kind-of@^3.0.3: + version "3.2.2" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +klona@^2.0.4, klona@^2.0.5: + version "2.0.6" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== + +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + +launch-editor@^2.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz" + integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lilconfig@^2.0.3, lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0, loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^3.2.0: + version "3.2.1" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + 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" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^5.1.1: + 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" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +"lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + +"luxon@^1.21.3 || ^2.x || ^3.x", "luxon@^1.28.0 || ^2.0.0 || ^3.0.0", luxon@^2.3.2: + version "2.5.2" + resolved "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz" + integrity sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA== + +lz-string@^1.4.4: + version "1.5.0" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: + 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" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz" + integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== + dependencies: + object-visit "^1.0.0" + +mapbox-gl@*, mapbox-gl@^2.9.1: + version "2.15.0" + resolved "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz" + integrity sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A== + dependencies: + "@mapbox/geojson-rewind" "^0.5.2" + "@mapbox/jsonlint-lines-primitives" "^2.0.2" + "@mapbox/mapbox-gl-supported" "^2.0.1" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/tiny-sdf" "^2.0.6" + "@mapbox/unitbezier" "^0.0.1" + "@mapbox/vector-tile" "^1.3.1" + "@mapbox/whoots-js" "^3.1.0" + csscolorparser "~1.0.3" + earcut "^2.2.4" + geojson-vt "^3.2.1" + gl-matrix "^3.4.3" + grid-index "^1.1.0" + kdbush "^4.0.1" + murmurhash-js "^1.0.0" + pbf "^3.2.1" + potpack "^2.0.0" + quickselect "^2.0.0" + rw "^1.3.3" + supercluster "^8.0.0" + tinyqueue "^2.0.3" + vt-pbf "^3.1.3" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.1.2, memfs@^3.4.3: + 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" + +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +"mime-db@>= 1.43.0 < 2", mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + 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: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +mini-css-extract-plugin@^2.4.5: + version "2.7.7" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.7.tgz" + integrity sha512-+0n11YGyRavUR3IlaOzJ0/4Il1avMvJ1VJfhWfCn24ITQXhRr1gghbhhrda6tgtNcpZaWKdSuwKq20Jb7fnlyw== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + 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" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@~0.5.1: + 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" + +moo-color@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz" + integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ== + dependencies: + color-name "^1.1.4" + +ms@^2.1.1, ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + 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" + +murmurhash-js@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz" + integrity sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E= + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-notifier@^8.0.0, "node-notifier@^8.0.1 || ^9.0.0 || ^10.0.0": + version "8.0.2" + resolved "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz" + integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" + integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +npm-run-path@^4.0.0, npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nwsapi@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz" + integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz" + integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.1, object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz" + integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.6, object.entries@^1.1.7: + version "1.1.7" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.fromentries@^2.0.6, object.fromentries@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.7" + resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz" + integrity sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g== + dependencies: + array.prototype.reduce "^1.0.6" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + safe-array-concat "^1.0.0" + +object.groupby@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + +object.hasown@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz" + integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== + dependencies: + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0, object.values@^1.1.6, object.values@^1.1.7: + version "1.1.7" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9, open@^8.4.0: + version "8.4.2" + resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-each-series@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz" + integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbf@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz" + integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1, pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +popper.js@1.16.1-lts: + version "1.16.1-lts" + resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz" + integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz" + integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== + +postcss-attribute-case-insensitive@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz" + integrity sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-browser-comments@^4: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz" + integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^4.2.4: + version "4.2.4" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz" + integrity sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-hex-alpha@^8.0.4: + version "8.0.4" + resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz" + integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz" + integrity sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-custom-media@^8.0.2: + version "8.0.2" + resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz" + integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-properties@^12.1.10: + version "12.1.11" + resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz" + integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz" + integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-dir-pseudo-class@^6.0.5: + version "6.0.5" + resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz" + integrity sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-double-position-gradients@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz" + integrity sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-env-function@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz" + integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-flexbugs-fixes@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz" + integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== + +postcss-focus-visible@^6.0.4: + version "6.0.4" + resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz" + integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-focus-within@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz" + integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^3.0.5: + version "3.0.5" + resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz" + integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== + +postcss-image-set-function@^4.0.7: + version "4.0.7" + resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz" + integrity sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-lab-function@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz" + integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-loader@^6.2.1: + version "6.2.1" + resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz" + integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.5" + semver "^7.3.5" + +postcss-logical@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz" + integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== + +postcss-media-minmax@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" + integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== + +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.1" + +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== + dependencies: + browserslist "^4.21.4" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.3: + version "4.0.4" + resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz" + integrity sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz" + integrity sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + +postcss-nesting@^10.2.0: + version "10.2.0" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz" + integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== + dependencies: + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz" + integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== + dependencies: + "@csstools/normalize.css" "*" + postcss-browser-comments "^4" + sanitize.css "*" + +postcss-opacity-percentage@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz" + integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz" + integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^7.0.5: + version "7.0.5" + resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz" + integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^7.0.1: + version "7.8.3" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz" + integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== + dependencies: + "@csstools/postcss-cascade-layers" "^1.1.1" + "@csstools/postcss-color-function" "^1.1.1" + "@csstools/postcss-font-format-keywords" "^1.0.1" + "@csstools/postcss-hwb-function" "^1.0.2" + "@csstools/postcss-ic-unit" "^1.0.1" + "@csstools/postcss-is-pseudo-class" "^2.0.7" + "@csstools/postcss-nested-calc" "^1.0.0" + "@csstools/postcss-normalize-display-values" "^1.0.1" + "@csstools/postcss-oklab-function" "^1.1.1" + "@csstools/postcss-progressive-custom-properties" "^1.3.0" + "@csstools/postcss-stepped-value-functions" "^1.0.1" + "@csstools/postcss-text-decoration-shorthand" "^1.0.0" + "@csstools/postcss-trigonometric-functions" "^1.0.2" + "@csstools/postcss-unset-value" "^1.0.2" + autoprefixer "^10.4.13" + browserslist "^4.21.4" + css-blank-pseudo "^3.0.3" + css-has-pseudo "^3.0.4" + css-prefers-color-scheme "^6.0.3" + cssdb "^7.1.0" + postcss-attribute-case-insensitive "^5.0.2" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^4.2.4" + postcss-color-hex-alpha "^8.0.4" + postcss-color-rebeccapurple "^7.1.1" + postcss-custom-media "^8.0.2" + postcss-custom-properties "^12.1.10" + postcss-custom-selectors "^6.0.3" + postcss-dir-pseudo-class "^6.0.5" + postcss-double-position-gradients "^3.1.2" + postcss-env-function "^4.0.6" + postcss-focus-visible "^6.0.4" + postcss-focus-within "^5.0.4" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^3.0.5" + postcss-image-set-function "^4.0.7" + postcss-initial "^4.0.1" + postcss-lab-function "^4.2.1" + postcss-logical "^5.0.4" + postcss-media-minmax "^5.0.0" + postcss-nesting "^10.2.0" + postcss-opacity-percentage "^1.1.2" + postcss-overflow-shorthand "^3.0.4" + postcss-page-break "^3.0.4" + postcss-place "^7.0.5" + postcss-pseudo-class-any-link "^7.1.6" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-pseudo-class-any-link@^7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz" + integrity sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz" + integrity sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.0.15" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz" + integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +"postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.0, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.1.4, postcss@^8.2, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3, postcss@^8.3.5, postcss@^8.4, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.31, postcss@^8.4.4, postcss@^8.4.6, "postcss@>= 8", postcss@>=8, postcss@>=8.0.9: + version "8.4.33" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^7.0.35: + version "7.0.39" + resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +potpack@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz" + integrity sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: + version "5.6.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + +pretty-format@^27.0.2, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.0.0: + version "29.5.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz" + integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== + dependencies: + "@jest/schemas" "^29.4.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.5.0: + version "29.5.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz" + integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== + dependencies: + "@jest/schemas" "^29.4.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise-polyfill@^8.1.3: + version "8.3.0" + resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz" + integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg== + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +promise@^8.1.0: + version "8.3.0" + resolved "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + +prompts@^2.0.1, prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types-extra@^1.1.0, prop-types-extra@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +protocol-buffers-schema@^3.3.1: + version "3.5.1" + resolved "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz" + integrity sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +raf-schd@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc-slider@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/rc-slider/-/rc-slider-10.1.0.tgz" + integrity sha512-nhC8V0+lNj4gGKZix2QAfcj/EP3NvCtFhNJPFMvXUdn7pe8bSa2vXNSxQVN5b9veVSic4Xeqgd/7KamX3gqznA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.18.1" + shallowequal "^1.1.0" + +rc-util@^5.18.1: + version "5.24.4" + resolved "https://registry.npmjs.org/rc-util/-/rc-util-5.24.4.tgz" + integrity sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^16.12.0" + shallowequal "^1.1.0" + +react-app-polyfill@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz" + integrity sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w== + dependencies: + core-js "^3.19.2" + object-assign "^4.1.1" + promise "^8.1.0" + raf "^3.4.1" + regenerator-runtime "^0.13.9" + whatwg-fetch "^3.6.2" + +react-bootstrap@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.0.tgz" + integrity sha512-Jcrn6aUuRVBeSB6dzKODKZU1TONOdhAxu0IDm4Sv74SJUm98dMdhSotF2SNvFEADANoR+stV+7TK6SNX1wWu5w== + dependencies: + "@babel/runtime" "^7.17.2" + "@restart/hooks" "^0.4.6" + "@restart/ui" "^1.4.1" + "@types/react-transition-group" "^4.4.4" + classnames "^2.3.1" + dom-helpers "^5.2.1" + invariant "^2.2.4" + prop-types "^15.8.1" + prop-types-extra "^1.1.0" + react-transition-group "^4.4.2" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-confirm-alert@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/react-confirm-alert/-/react-confirm-alert-2.8.0.tgz" + integrity sha512-qvNjJWuWUpTh+q4NecUjCMIWLNDl8IwW6JRIky5pzoiFBXsLWSA2Z1VsaDsQedwgyxEpKnMEJFETkDogBpv/kA== + +react-dev-utils@^12.0.0: + version "12.0.1" + resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz" + integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== + dependencies: + "@babel/code-frame" "^7.16.0" + address "^1.1.2" + browserslist "^4.18.1" + chalk "^4.1.2" + cross-spawn "^7.0.3" + detect-port-alt "^1.1.6" + escape-string-regexp "^4.0.0" + filesize "^8.0.6" + find-up "^5.0.0" + fork-ts-checker-webpack-plugin "^6.5.0" + global-modules "^2.0.0" + globby "^11.0.4" + gzip-size "^6.0.0" + immer "^9.0.7" + is-root "^2.1.0" + loader-utils "^3.2.0" + open "^8.4.0" + pkg-up "^3.1.0" + prompts "^2.4.2" + react-error-overlay "^6.0.11" + recursive-readdir "^2.2.2" + shell-quote "^1.7.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +"react-dom@^16.0.0 || ^17.0.0", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8.0 || ^17 || ^18", "react-dom@^16.8.0 || ^17.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.5 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", react-dom@^17.0.2, "react-dom@^17.0.2 || ^18.0.0", react-dom@<18.0.0, "react-dom@>= 16.8.0", react-dom@>=0.16.0, react-dom@>=16.0.0, react-dom@>=16.14.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0, react-dom@>=16.9.0: + version "17.0.2" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-double-scrollbar@0.0.15: + version "0.0.15" + resolved "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz" + integrity sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg== + +react-error-overlay@^6.0.11: + version "6.0.11" + resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== + +react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: + version "3.2.1" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz" + integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg== + +react-hook-form@^7.0.0, react-hook-form@^7.41.5: + version "7.41.5" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.41.5.tgz" + integrity sha512-DAKjSJ7X9f16oQrP3TW2/eD9N6HOgrmIahP4LOdFphEWVfGZ2LulFd6f6AQ/YS/0cx/5oc4j8a1PXxuaurWp/Q== + +react-icons@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz" + integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== + +react-is@^16.12.0, "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, "react-is@^16.8.0 || ^17.0.0", "react-is@>= 16.8.0": + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-map-gl@^7.0.21: + version "7.0.23" + resolved "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.0.23.tgz" + integrity sha512-874jEtdS/fB2R4jSJKud9va0H0GlxhtiSFuUMATiniQ7A2lQnZLkZIPEWwIPkMmNZDXNlTAkxWEdSHzsqADVAw== + dependencies: + "@types/mapbox-gl" "^2.6.0" + +react-popper@^2.2.4: + version "2.3.0" + resolved "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz" + integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + +"react-redux@^7.2.1 || ^8.0.2", react-redux@^8.0.4, react-redux@^8.0.5: + version "8.0.5" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz" + integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + +react-refresh@^0.11.0, "react-refresh@>=0.10.0 <1.0.0": + version "0.11.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" + integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== + +react-router-dom@^6.2.2: + version "6.2.2" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.2.tgz" + integrity sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ== + dependencies: + history "^5.2.0" + react-router "6.2.2" + +react-router@6.2.2: + version "6.2.2" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.2.2.tgz" + integrity sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ== + dependencies: + history "^5.2.0" + +react-scripts@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz" + integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== + dependencies: + "@babel/core" "^7.16.0" + "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" + "@svgr/webpack" "^5.5.0" + babel-jest "^27.4.2" + babel-loader "^8.2.3" + babel-plugin-named-asset-import "^0.3.8" + babel-preset-react-app "^10.0.1" + bfj "^7.0.2" + browserslist "^4.18.1" + camelcase "^6.2.1" + case-sensitive-paths-webpack-plugin "^2.4.0" + css-loader "^6.5.1" + css-minimizer-webpack-plugin "^3.2.0" + dotenv "^10.0.0" + dotenv-expand "^5.1.0" + eslint "^8.3.0" + eslint-config-react-app "^7.0.0" + eslint-webpack-plugin "^3.1.1" + file-loader "^6.2.0" + fs-extra "^10.0.0" + html-webpack-plugin "^5.5.0" + identity-obj-proxy "^3.0.0" + jest "^27.4.3" + jest-resolve "^27.4.2" + jest-watch-typeahead "^1.0.0" + mini-css-extract-plugin "^2.4.5" + postcss "^8.4.4" + postcss-flexbugs-fixes "^5.0.2" + postcss-loader "^6.2.1" + postcss-normalize "^10.0.1" + postcss-preset-env "^7.0.1" + prompts "^2.4.2" + react-app-polyfill "^3.0.0" + react-dev-utils "^12.0.0" + react-refresh "^0.11.0" + resolve "^1.20.0" + resolve-url-loader "^4.0.0" + sass-loader "^12.3.0" + semver "^7.3.5" + source-map-loader "^3.0.0" + style-loader "^3.3.1" + tailwindcss "^3.0.2" + terser-webpack-plugin "^5.2.5" + webpack "^5.64.4" + webpack-dev-server "^4.6.0" + webpack-manifest-plugin "^4.0.2" + workbox-webpack-plugin "^6.4.1" + optionalDependencies: + fsevents "^2.3.2" + +react-secure-storage@^1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/react-secure-storage/-/react-secure-storage-1.3.2.tgz" + integrity sha512-pNCyksbLXWIYRS9vCzERXdIMErtx9Ik70TPtLKivcq44+zYybbxA72wpp5ivghK9Xe0gRku2w/7zBy/9n+RtKA== + dependencies: + crypto-js "^4.1.1" + murmurhash-js "^1.0.0" + +react-select@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/react-select/-/react-select-5.3.2.tgz" + integrity sha512-W6Irh7U6Ha7p5uQQ2ZnemoCQ8mcfgOtHfw3wuMzG6FAu0P+CYicgofSLOq97BhjMx8jS+h+wwWdCBeVVZ9VqlQ== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^5.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + +react-shallow-renderer@^16.13.1: + version "16.15.0" + resolved "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +react-spinners@^0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/react-spinners/-/react-spinners-0.11.0.tgz" + integrity sha512-rDZc0ABWn/M1OryboGsWVmIPg8uYWl0L35jPUhr40+Yg+syVPjeHwvnB7XWaRpaKus3M0cG9BiJA+ZB0dAwWyw== + dependencies: + "@emotion/react" "^11.1.4" + +react-table@^7.8.0: + version "7.8.0" + resolved "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz" + integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== + +react-tabs@*, react-tabs@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/react-tabs/-/react-tabs-4.2.1.tgz" + integrity sha512-nQcEN3KrAsSry6f9Jz2oyMQsnh+sLEy31YjlskL/mnI3KU/c7BeyD1VzHZmmcJ15UEFu12pYOXYkdTzZ0uyIbw== + dependencies: + clsx "^1.1.0" + prop-types "^15.5.0" + +react-test-renderer@^17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.2" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.2" + +react-transition-group@^4.3.0, react-transition-group@^4.4.0, react-transition-group@^4.4.2, react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-widgets@5.8.4: + version "5.8.4" + resolved "https://registry.npmjs.org/react-widgets/-/react-widgets-5.8.4.tgz" + integrity sha512-WcA/K+eVKAW+vyeQKdRqo2gmnLqHbNSDDKQ84j/wyhbautCRrGbjWAmKb4+tI3OzUgCAAEJDZ75azAY2WoKWYQ== + dependencies: + "@restart/hooks" "^0.4.5" + "@types/classnames" "^2.3.1" + "@types/react-transition-group" "^4.4.4" + classnames "^2.3.1" + date-arithmetic "^4.0.1" + dom-helpers "^5.2.1" + prop-types-extra "^1.1.1" + react-transition-group "^4.4.2" + tiny-warning "^1.0.3" + uncontrollable "^7.2.1" + +react@*, "react@^16.0.0 || ^17.0.0", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17 || ^18", "react@^16.8.0 || ^17.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0-0 || ^18.0.0", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", "react@^16.8.3 || ^17.0.0-0 || ^18.0.0", "react@^16.8.5 || ^17.0.0 || ^18.0.0", "react@^16.9.0 || ^17.0.0 || ^18", "react@^17.0.0 || ^18.0.0", react@^17.0.2, "react@^17.0.2 || ^18.0.0", react@<18.0.0, "react@>= 0.14.7", "react@>= 16", "react@>= 16.8.0", react@>=0.14.0, react@>=0.16.0, react@>=15.0.0, react@>=16, react@>=16.0.0, react@>=16.14.0, react@>=16.3.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.9.0, react@17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +reactstrap@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.2.tgz" + integrity sha512-4KroiGOdqZLAnMGzHjpErW3G7bLB+QbKzzMLIDXydPIV0y74lpdL7WtXHkLWAGInd97WCPNx4+R0NQDPyzIfhw== + dependencies: + "@babel/runtime" "^7.12.5" + "@popperjs/core" "^2.6.0" + classnames "^2.2.3" + prop-types "^15.5.8" + react-popper "^2.2.4" + react-transition-group "^4.4.2" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +redux-thunk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" + integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== + +redux@^4, redux@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + +reflect.getprototypeof@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz" + integrity sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regenerator-runtime@^0.13.9: + version "0.13.11" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regex-parser@^2.2.11: + version "2.3.0" + resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz" + integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg== + +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.1.tgz" + integrity sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +reselect@^4.1.7: + version "4.1.7" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz" + integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + +resolve-url-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" + integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== + dependencies: + adjust-sourcemap-loader "^4.0.0" + convert-source-map "^1.7.0" + loader-utils "^2.0.0" + postcss "^7.0.35" + source-map "0.6.1" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + +resolve.exports@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== + +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.4: + version "2.0.0-next.5" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rifm@^0.12.1: + version "0.12.1" + resolved "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz" + integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup-plugin-terser@^7.0.0: + version "7.0.2" + resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.0.0, rollup@^2.43.1: + version "2.79.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rw@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= + +safe-array-concat@^1.0.0, safe-array-concat@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz" + integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== + dependencies: + call-bind "^1.0.5" + get-intrinsic "^1.2.2" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz" + integrity sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ== + dependencies: + call-bind "^1.0.5" + get-intrinsic "^1.2.2" + is-regex "^1.1.4" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" + integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sanitize.css@*: + version "13.0.0" + resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz" + integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== + +sass-loader@^12.3.0: + version "12.6.0" + resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz" + integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^6.1.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^6.1.2: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +"semver@2 || 3 || 4 || 5": + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.1.1: + version "1.2.0" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz" + integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== + dependencies: + define-data-property "^1.1.1" + function-bind "^1.1.2" + get-intrinsic "^1.2.2" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + +set-function-name@^2.0.0, set-function-name@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3, shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-list-map@^2.0.0, source-list-map@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz" + integrity sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg== + dependencies: + abab "^2.0.5" + iconv-lite "^0.6.3" + source-map-js "^1.0.1" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.6, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.4" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.13" + resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz" + integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^2.0.2, stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz" + integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-length@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz" + integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== + dependencies: + char-regex "^2.0.0" + strip-ansi "^7.0.1" + +string-natural-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" + integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^3.3.1: + version "3.3.4" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz" + integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== + +styled-components@^5.3.1, styled-components@^5.3.6, "styled-components@>= 2": + version "5.3.8" + resolved "https://registry.npmjs.org/styled-components/-/styled-components-5.3.8.tgz" + integrity sha512-6jQrlvaJQ16uWVVO0rBfApaTPItkqaG32l3746enNZzpMDxMvzmHzj8rHUg39bvVtom0Y8o8ZzWuchEXKGjVsg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1.12.0" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== + dependencies: + browserslist "^4.21.4" + postcss-selector-parser "^6.0.4" + +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supercluster@^8.0.0: + version "8.0.1" + resolved "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz" + integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ== + dependencies: + kdbush "^4.0.2" + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + 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" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.2: + version "2.0.4" + resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^1.2.2: + version "1.3.2" + resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tailwindcss@^3.0.2: + version "3.4.1" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz" + integrity sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.19.1" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: + version "5.3.6" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz" + integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.14" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.14.1" + +terser@^5.0.0, terser@^5.10.0, terser@^5.14.1: + version "5.16.5" + resolved "https://registry.npmjs.org/terser/-/terser-5.16.5.tgz" + integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + +throat@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz" + integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tiny-invariant@^1.0.6: + version "1.3.1" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tiny-warning@^1.0.2, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz" + integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz" + integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.0.0: + version "4.1.3" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.3, tslib@^2.4.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-detect@^4.0.0, type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3, "type-fest@>=0.17.0 <5.0.0": + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +"typescript@^3.2.1 || ^4", typescript@^4.9.4, "typescript@>= 2.7", "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=3.8: + version "4.9.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +ua-parser-js@^0.7.30: + version "0.7.34" + resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz" + integrity sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@~1.0.0, unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz" + integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +use-memo-one@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + +use-sync-external-store@^1.0.0, use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + +v8-to-istanbul@^7.0.0: + version "7.1.2" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz" + integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vt-pbf@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz" + integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== + dependencies: + "@mapbox/point-geometry" "0.1.0" + "@mapbox/vector-tile" "^1.3.1" + pbf "^3.2.1" + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7, walker@^1.0.8, walker@~1.0.5: + version "1.0.8" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +warning@^4.0.0, warning@^4.0.2, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.6.0, "webpack-dev-server@3.x || 4.x": + version "4.15.1" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz" + integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.13.0" + +webpack-manifest-plugin@^4.0.2: + version "4.1.1" + resolved "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz" + integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== + dependencies: + tapable "^2.0.0" + webpack-sources "^2.2.0" + +webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^2.2.0: + version "2.3.1" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz" + integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== + dependencies: + source-list-map "^2.0.1" + source-map "^0.6.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", "webpack@^4.4.0 || ^5.9.0", "webpack@^4.44.2 || ^5.47.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.64.4, "webpack@>= 4", webpack@>=2, "webpack@>=4.43.0 <6.0.0": + version "5.76.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz" + integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@^0.7.4, websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@^3.4.1, whatwg-fetch@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" + integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + +which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: + version "1.1.13" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +workbox-background-sync@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz" + integrity sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw== + dependencies: + idb "^7.0.1" + workbox-core "6.6.0" + +workbox-broadcast-update@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz" + integrity sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q== + dependencies: + workbox-core "6.6.0" + +workbox-build@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz" + integrity sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.11.1" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^11.2.1" + "@rollup/plugin-replace" "^2.4.1" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + rollup-plugin-terser "^7.0.0" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "6.6.0" + workbox-broadcast-update "6.6.0" + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-google-analytics "6.6.0" + workbox-navigation-preload "6.6.0" + workbox-precaching "6.6.0" + workbox-range-requests "6.6.0" + workbox-recipes "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + workbox-streams "6.6.0" + workbox-sw "6.6.0" + workbox-window "6.6.0" + +workbox-cacheable-response@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz" + integrity sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw== + dependencies: + workbox-core "6.6.0" + +workbox-core@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz" + integrity sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ== + +workbox-expiration@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz" + integrity sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw== + dependencies: + idb "^7.0.1" + workbox-core "6.6.0" + +workbox-google-analytics@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz" + integrity sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q== + dependencies: + workbox-background-sync "6.6.0" + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + +workbox-navigation-preload@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz" + integrity sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q== + dependencies: + workbox-core "6.6.0" + +workbox-precaching@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz" + integrity sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw== + dependencies: + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + +workbox-range-requests@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz" + integrity sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw== + dependencies: + workbox-core "6.6.0" + +workbox-recipes@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz" + integrity sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A== + dependencies: + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-precaching "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + +workbox-routing@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz" + integrity sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw== + dependencies: + workbox-core "6.6.0" + +workbox-strategies@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz" + integrity sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ== + dependencies: + workbox-core "6.6.0" + +workbox-streams@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz" + integrity sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg== + dependencies: + workbox-core "6.6.0" + workbox-routing "6.6.0" + +workbox-sw@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz" + integrity sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ== + +workbox-webpack-plugin@^6.4.1: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz" + integrity sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A== + dependencies: + fast-json-stable-stringify "^2.1.0" + pretty-bytes "^5.4.1" + upath "^1.2.0" + webpack-sources "^1.4.3" + workbox-build "6.6.0" + +workbox-window@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz" + integrity sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "6.6.0" + +worker-loader@^3.0.8: + version "3.0.8" + resolved "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz" + integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^7.4.6: + version "7.5.9" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + +ws@^8.13.0: + version "8.16.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.1.1, zustand@4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.1.tgz" + integrity sha512-EVyo/eLlOTcJm/X5M00rwtbYFXwRVTaRteSvhtbTZUCQFJkNfIyHPiJ6Ke68MSWzcKHpPzvqNH4gC2ZS/sbNqw== + dependencies: + use-sync-external-store "1.2.0"