diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2593d1ad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7f815fb3..7c9c0dae 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,189 +1,39 @@ -name: Build and Push Docker Images +name: CI on: push: - branches: [ main, master ] + branches: + - main + - master pull_request: - branches: [ main, master ] - workflow_dispatch: - inputs: - specific_service: - description: 'Specific service to build (leave empty for all)' - required: false - default: '' - version: - description: 'Version tag (default: generated from date)' - required: false - default: '' jobs: - build: + test: runs-on: ubuntu-latest - + strategy: + matrix: + service: + - Back-End/task_user + - Back-End/Notifications + - Back-End/pongProject + - Back-End/login + - Back-End/chat steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Needed for detecting changes - - - name: Generate version if not provided - id: version - run: | - if [ -z "${{ github.event.inputs.version }}" ]; then - echo "version=$(date +'%Y.%m.%d')-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - else - echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - # Check for changes in Chat Service - - name: Check Chat changes - id: chat-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'chat' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Back-End/chat/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Chat Service if changed - - name: Build and push Chat Service - if: ${{ steps.chat-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Back-End/chat - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/chat:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/chat:${{ steps.version.outputs.version }} - - # Check for changes in Login Service - - name: Check Login changes - id: login-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'login' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Back-End/login/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Login Service if changed - - name: Build and push Login Service - if: ${{ steps.login-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Back-End/login - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/login-server:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/login-server:${{ steps.version.outputs.version }} - - # Check for changes in Frontend (assuming it's at root level) - - name: Check Frontend changes - id: frontend-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'frontend' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Frontend/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Frontend if changed - - name: Build and push Frontend - if: ${{ steps.frontend-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Frontend - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/frontend:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/frontend:${{ steps.version.outputs.version }} - - # Check for changes in Pong Service - - name: Check Pong changes - id: pong-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'pong' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Back-End/pongProject/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Pong Service if changed - - name: Build and push Pong Service - if: ${{ steps.pong-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Back-End/pongProject - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/pong-server:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/pong-server:${{ steps.version.outputs.version }} - - # Check for changes in Notifications Service - - name: Check Notifications changes - id: notifications-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'notifications' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Back-End/Notifications/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Notifications Service if changed - - name: Build and push Notifications Service - if: ${{ steps.notifications-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Back-End/Notifications - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/notifications-server:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/notifications-server:${{ steps.version.outputs.version }} - - # Check for changes in Task User Service - - name: Check Task User changes - id: task-user-changes - if: ${{ github.event.inputs.specific_service == '' || github.event.inputs.specific_service == 'task-user' }} - run: | - if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q "^Back-End/task_user/"; then - echo "changed=true" >> $GITHUB_OUTPUT - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - - # Build Task User Service if changed - - name: Build and push Task User Service - if: ${{ steps.task-user-changes.outputs.changed == 'true' }} - uses: docker/build-push-action@v4 - with: - context: ./Back-End/task_user - push: true - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/task-user-server:latest - ${{ secrets.DOCKER_HUB_USERNAME }}/task-user-server:${{ steps.version.outputs.version }} \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + cd ${{ matrix.service }} + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests + run: | + cd ${{ matrix.service }} + python manage.py test || echo "TESTS_FAILED=1" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index b599a2be..123511d9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,7 @@ temp/ *__pycache__/ *.pyc .vscode/settings.json -*secret.yaml *secrets.yaml -*secret.yml -*secret.yml .venv venv* *venv/ @@ -69,9 +66,27 @@ secrets.yaml fleet-server.crt fleet-server.key Manifests/secrets.yaml - skaffold Manifests/skaffold Manifests/skaffolds Manifests/skaffold -Manifests/skaffolds \ No newline at end of file +Manifests/skaffolds +helm-charts/my-umbrella/values.yaml + +*values*.yaml +values.yaml +v*.yaml +helm-charts/my-umbrella/values.yaml +*/values.yaml +helm-charts/my-umbrella/charts/chat-service-chart/values.yaml +helm-charts/my-umbrella/charts/login-service-chart/values.yaml +helm-charts/my-umbrella/charts/login-service-chart/values.yaml +helm-charts/my-umbrella/charts/chat-service-chart/values.yaml +helm-charts/my-umbrella/charts/cert-manager-chart-0.1.0.tgz +helm-charts/my-umbrella/charts/chat-service-chart-0.1.0.tgz +helm-charts/my-umbrella/charts/front-end-chart-0.1.0.tgz +helm-charts/my-umbrella/charts/login-service-chart-0.1.0.tgz +helm-charts/my-umbrella/charts/pong-service-chart-0.1.0.tgz +helm-charts/my-umbrella/charts/front-end-chart/values.yaml +helm-charts/my-umbrella/charts/cert-manager-chart/values.yaml +*.tgz \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8d39d9e9..6857f779 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "Docker Compose Up", "type": "shell", - "command": "docker compose up", + "command": "docker compose up --build", "options": { "cwd": "${workspaceFolder}/Back-End/Dockers" }, diff --git a/Back-End/Dockers/celery_notification/Dockerfile b/Back-End/Dockers/celery_notification/Dockerfile deleted file mode 100644 index 3d78564b..00000000 --- a/Back-End/Dockers/celery_notification/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:latest - -WORKDIR /app -COPY ./requirements.txt /app/requirements.txt - -RUN pip3 install --upgrade pip -RUN pip3 install -r requirements.txt - -ENV CELERY_BROKER_URL redis://redis_notification:6379/0 -ENV CELERY_RESULT_BACKEND redis://redis_notification:6379/0 - -RUN sleep 20 - -CMD ["celery", "-A", "notification", "worker", "--loglevel=info"] \ No newline at end of file diff --git a/Back-End/Dockers/celery_notification/requirements.txt b/Back-End/Dockers/celery_notification/requirements.txt deleted file mode 100644 index bb05c56f..00000000 --- a/Back-End/Dockers/celery_notification/requirements.txt +++ /dev/null @@ -1,317 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements.in -# -aiohappyeyeballs==2.4.3 - # via - # -r requirements.in - # aiohttp -aiohttp==3.11.8 - # via -r requirements.in -aiosignal==1.3.1 - # via - # -r requirements.in - # aiohttp -amqp==5.2.0 - # via - # -r requirements.in - # kombu -asgiref==3.8.1 - # via - # -r requirements.in - # channels - # channels-redis - # daphne - # django - # django-cors-headers -async-timeout==4.0.3 - # via - # -r requirements.in - # redis -attrs==24.2.0 - # via - # -r requirements.in - # aiohttp - # service-identity - # twisted -autobahn==24.4.2 - # via - # -r requirements.in - # daphne -automat==24.8.1 - # via - # -r requirements.in - # twisted -billiard==4.2.1 - # via - # -r requirements.in - # celery -build==1.2.2.post1 - # via - # -r requirements.in - # pip-tools -celery==5.4.0 - # via -r requirements.in -certifi==2024.8.30 - # via - # -r requirements.in - # requests -cffi==1.17.1 - # via - # -r requirements.in - # cryptography -channels==4.1.0 - # via - # -r requirements.in - # channels-redis -channels-redis==4.2.0 - # via -r requirements.in -charset-normalizer==3.3.2 - # via - # -r requirements.in - # requests -click==8.1.7 - # via - # -r requirements.in - # celery - # click-didyoumean - # click-plugins - # click-repl - # pip-tools -click-didyoumean==0.3.1 - # via - # -r requirements.in - # celery -click-plugins==1.1.1 - # via - # -r requirements.in - # celery -click-repl==0.3.0 - # via - # -r requirements.in - # celery -constantly==23.10.4 - # via - # -r requirements.in - # twisted -cryptography==43.0.1 - # via - # -r requirements.in - # autobahn - # jwcrypto - # pyopenssl - # service-identity -daphne==4.1.2 - # via -r requirements.in -django==5.1.3 - # via - # -r requirements.in - # channels - # django-cors-headers - # django-filter - # django-oauth-toolkit - # django-redis - # djangorestframework -django-cors-headers==4.4.0 - # via -r requirements.in -django-filter==24.3 - # via -r requirements.in -django-loki==0.1.4 - # via -r requirements.in -django-oauth-toolkit==3.0.1 - # via -r requirements.in -django-prometheus==2.3.1 - # via -r requirements.in -django-redis==5.4.0 - # via -r requirements.in -djangorestframework==3.15.2 - # via -r requirements.in -frozenlist==1.5.0 - # via - # -r requirements.in - # aiohttp - # aiosignal -grafana==0.0.1 - # via -r requirements.in -hyperlink==21.0.0 - # via - # -r requirements.in - # autobahn - # twisted -idna==3.10 - # via - # -r requirements.in - # hyperlink - # requests - # twisted - # yarl -incremental==24.7.2 - # via - # -r requirements.in - # twisted -joblib==1.4.2 - # via - # -r requirements.in - # scikit-learn -jwcrypto==1.5.6 - # via - # -r requirements.in - # django-oauth-toolkit -kombu==5.4.2 - # via - # -r requirements.in - # celery -msgpack==1.1.0 - # via - # -r requirements.in - # channels-redis -multidict==6.1.0 - # via - # -r requirements.in - # aiohttp - # yarl -numpy==2.1.2 - # via - # -r requirements.in - # scikit-learn - # scipy -oauthlib==3.2.2 - # via - # -r requirements.in - # django-oauth-toolkit -packaging==24.2 - # via - # -r requirements.in - # build -pillow==11.0.0 - # via -r requirements.in -pip-tools==7.4.1 - # via -r requirements.in -prometheus-client==0.21.0 - # via - # -r requirements.in - # django-prometheus -prompt-toolkit==3.0.48 - # via - # -r requirements.in - # click-repl -propcache==0.2.0 - # via - # -r requirements.in - # aiohttp - # yarl -psycopg==3.2.3 - # via -r requirements.in -pyasn1==0.6.1 - # via - # -r requirements.in - # pyasn1-modules - # service-identity -pyasn1-modules==0.4.1 - # via - # -r requirements.in - # service-identity -pycparser==2.22 - # via - # -r requirements.in - # cffi -pyopenssl==24.2.1 - # via - # -r requirements.in - # twisted -pyproject-hooks==1.2.0 - # via - # -r requirements.in - # build - # pip-tools -python-dateutil==2.9.0.post0 - # via - # -r requirements.in - # celery -pytz==2024.2 - # via - # -r requirements.in - # django-loki -redis==5.1.1 - # via - # -r requirements.in - # channels-redis - # django-redis -requests==2.32.3 - # via - # -r requirements.in - # django-loki - # django-oauth-toolkit -scikit-learn==1.5.2 - # via -r requirements.in -scipy==1.14.1 - # via - # -r requirements.in - # scikit-learn -service-identity==24.1.0 - # via - # -r requirements.in - # twisted -six==1.16.0 - # via - # -r requirements.in - # python-dateutil -sqlparse==0.5.1 - # via - # -r requirements.in - # django -style==1.1.0 - # via - # -r requirements.in - # update -threadpoolctl==3.5.0 - # via - # -r requirements.in - # scikit-learn -twisted[tls]==24.7.0 - # via - # -r requirements.in - # daphne -txaio==23.1.1 - # via - # -r requirements.in - # autobahn -typing-extensions==4.12.2 - # via - # -r requirements.in - # jwcrypto - # psycopg - # twisted -tzdata==2024.2 - # via - # -r requirements.in - # celery - # kombu -update==0.0.1 - # via -r requirements.in -urllib3==2.2.3 - # via - # -r requirements.in - # requests -vine==5.1.0 - # via - # -r requirements.in - # amqp - # celery - # kombu -wcwidth==0.2.13 - # via - # -r requirements.in - # prompt-toolkit -wheel==0.45.1 - # via pip-tools -yarl==1.18.0 - # via - # -r requirements.in - # aiohttp -zope-interface==7.1.0 - # via - # -r requirements.in - # twisted diff --git a/Back-End/Dockers/celery_notification/sss.txt b/Back-End/Dockers/celery_notification/sss.txt deleted file mode 100644 index 651f1bd9..00000000 --- a/Back-End/Dockers/celery_notification/sss.txt +++ /dev/null @@ -1,313 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements.in -# -aiohappyeyeballs==2.4.3 - # via - # -r requirements.in - # aiohttp -aiohttp==3.11.8 - # via -r requirements.in -aiosignal==1.3.1 - # via - # -r requirements.in - # aiohttp -amqp==5.2.0 - # via - # -r requirements.in - # kombu -asgiref==3.8.1 - # via - # -r requirements.in - # channels - # channels-redis - # daphne - # django - # django-cors-headers -async-timeout==4.0.3 - # via - # -r requirements.in - # redis -attrs==24.2.0 - # via - # -r requirements.in - # aiohttp - # service-identity - # twisted -autobahn==24.4.2 - # via - # -r requirements.in - # daphne -automat==24.8.1 - # via - # -r requirements.in - # twisted -billiard==4.2.1 - # via - # -r requirements.in - # celery -build==1.2.2.post1 - # via - # -r requirements.in - # pip-tools -celery==5.4.0 - # via -r requirements.in -certifi==2024.8.30 - # via - # -r requirements.in - # requests -cffi==1.17.1 - # via - # -r requirements.in - # cryptography -channels==4.1.0 - # via - # -r requirements.in - # channels-redis -channels-redis==4.2.0 - # via -r requirements.in -charset-normalizer==3.3.2 - # via - # -r requirements.in - # requests -click==8.1.7 - # via - # -r requirements.in - # celery - # click-didyoumean - # click-plugins - # click-repl - # pip-tools -click-didyoumean==0.3.1 - # via - # -r requirements.in - # celery -click-plugins==1.1.1 - # via - # -r requirements.in - # celery -click-repl==0.3.0 - # via - # -r requirements.in - # celery -constantly==23.10.4 - # via - # -r requirements.in - # twisted -cryptography==43.0.1 - # via - # -r requirements.in - # autobahn - # jwcrypto - # pyopenssl - # service-identity -daphne==4.1.2 - # via -r requirements.in -django==5.1.3 - # via - # -r requirements.in - # channels - # django-cors-headers - # django-filter - # django-oauth-toolkit - # django-redis - # djangorestframework -django-cors-headers==4.4.0 - # via -r requirements.in -django-filter==24.3 - # via -r requirements.in -django-loki==0.1.4 - # via -r requirements.in -django-oauth-toolkit==3.0.1 - # via -r requirements.in -django-prometheus==2.3.1 - # via -r requirements.in -django-redis==5.4.0 - # via -r requirements.in -djangorestframework==3.15.2 - # via -r requirements.in -frozenlist==1.5.0 - # via - # -r requirements.in - # aiohttp - # aiosignal -grafana==0.0.1 - # via -r requirements.in -hyperlink==21.0.0 - # via - # -r requirements.in - # autobahn - # twisted -idna==3.10 - # via - # -r requirements.in - # hyperlink - # requests - # twisted - # yarl -incremental==24.7.2 - # via - # -r requirements.in - # twisted -joblib==1.4.2 - # via - # -r requirements.in - # scikit-learn -jwcrypto==1.5.6 - # via - # -r requirements.in - # django-oauth-toolkit -kombu==5.4.2 - # via - # -r requirements.in - # celery -msgpack==1.1.0 - # via - # -r requirements.in - # channels-redis -multidict==6.1.0 - # via - # -r requirements.in - # aiohttp - # yarl -oauthlib==3.2.2 - # via - # -r requirements.in - # django-oauth-toolkit -packaging==24.2 - # via - # -r requirements.in - # build -pillow==11.0.0 - # via -r requirements.in -pip-tools==7.4.1 - # via -r requirements.in -prometheus-client==0.21.0 - # via - # -r requirements.in - # django-prometheus -prompt-toolkit==3.0.48 - # via - # -r requirements.in - # click-repl -propcache==0.2.0 - # via - # -r requirements.in - # aiohttp - # yarl -pyasn1==0.6.1 - # via - # -r requirements.in - # pyasn1-modules - # service-identity -pyasn1-modules==0.4.1 - # via - # -r requirements.in - # service-identity -pycparser==2.22 - # via - # -r requirements.in - # cffi -pyopenssl==24.2.1 - # via - # -r requirements.in - # twisted -pyproject-hooks==1.2.0 - # via - # -r requirements.in - # build - # pip-tools -python-dateutil==2.9.0.post0 - # via - # -r requirements.in - # celery -pytz==2024.2 - # via - # -r requirements.in - # django-loki -redis==5.1.1 - # via - # -r requirements.in - # channels-redis - # django-redis -requests==2.32.3 - # via - # -r requirements.in - # django-loki - # django-oauth-toolkit -scikit-learn==1.5.2 - # via -r requirements.in -scipy==1.14.1 - # via - # -r requirements.in - # scikit-learn -service-identity==24.1.0 - # via - # -r requirements.in - # twisted -six==1.16.0 - # via - # -r requirements.in - # python-dateutil -sqlparse==0.5.1 - # via - # -r requirements.in - # django -style==1.1.0 - # via - # -r requirements.in - # update -threadpoolctl==3.5.0 - # via - # -r requirements.in - # scikit-learn -twisted[tls]==24.7.0 - # via - # -r requirements.in - # daphne -txaio==23.1.1 - # via - # -r requirements.in - # autobahn -typing-extensions==4.12.2 - # via - # -r requirements.in - # jwcrypto - # twisted -tzdata==2024.2 - # via - # -r requirements.in - # celery - # kombu -update==0.0.1 - # via -r requirements.in -urllib3==2.2.3 - # via - # -r requirements.in - # requests -vine==5.1.0 - # via - # -r requirements.in - # amqp - # celery - # kombu -wcwidth==0.2.13 - # via - # -r requirements.in - # prompt-toolkit -wheel==0.45.1 - # via pip-tools -yarl==1.18.0 - # via - # -r requirements.in - # aiohttp -zope-interface==7.1.0 - # via - # -r requirements.in - # twisted - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/Back-End/Dockers/chat_db/Dockerfile b/Back-End/Dockers/chat_db/Dockerfile deleted file mode 100644 index d94141a1..00000000 --- a/Back-End/Dockers/chat_db/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM postgres:latest - -# ENV POSTGRES_USER=pasquale -# ENV POSTGRES_DB=chat_db -# ENV POSTGRES_PASSWORD=123 - -EXPOSE 5432 - -COPY ./init.sql /docker-entrypoint-initdb.d/ diff --git a/Back-End/Dockers/chat_db/init.sql b/Back-End/Dockers/chat_db/init.sql deleted file mode 100644 index af4d7601..00000000 --- a/Back-End/Dockers/chat_db/init.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Create a test table -CREATE TABLE test_table ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL -); - --- Insert some test data -INSERT INTO test_table (name) VALUES ('Alice'), ('Bob'), ('Charlie'); - --- Query the test data -SELECT * FROM test_table; \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/Dockerfile b/Back-End/Dockers/chatservice/Dockerfile deleted file mode 100644 index 4141990d..00000000 --- a/Back-End/Dockers/chatservice/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.14-rc-bookworm - -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev - -COPY ./chat /chat -COPY ./requirements.txt /chat/requirements.txt -WORKDIR /chat - -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -EXPOSE 8000 - -COPY ./entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/chat/__init__.py b/Back-End/Dockers/chatservice/chat/chat/__init__.py deleted file mode 100644 index a1b8d37e..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -# This will make sure the app is always imported when -# Django starts so that shared_task will use this app. -from chat.celery import app as celery_app - -__all__ = ('celery_app',) \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/chat/asgi.py b/Back-End/Dockers/chatservice/chat/chat/asgi.py deleted file mode 100644 index e105998f..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/asgi.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -from django.core.asgi import get_asgi_application -from channels.routing import ProtocolTypeRouter, URLRouter -from channels.security.websocket import AllowedHostsOriginValidator -from django.urls import path -from my_chat.middleware import TokenAuthMiddlewareStack -from my_chat import consumers -from .routing import websocket_urlpatterns - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') - -print("ASGI APPLICATION LOADED") - -application = ProtocolTypeRouter({ - "http": get_asgi_application(), - "websocket": AllowedHostsOriginValidator( - TokenAuthMiddlewareStack( - URLRouter(websocket_urlpatterns) - ) - ), -}) - -#path('ws/chat//', consumers.ChatConsumer.as_asgi()), - -# application = ProtocolTypeRouter({ -# "http": get_asgi_application(), -# "websocket": -# URLRouter([ -# websocket_urlpatterns -# ]), -# }) diff --git a/Back-End/Dockers/chatservice/chat/chat/celery.py b/Back-End/Dockers/chatservice/chat/chat/celery.py deleted file mode 100644 index 02f00fd7..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/celery.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import absolute_import, unicode_literals -import os -from celery import Celery - -# set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') - -app = Celery('chat') - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -app.config_from_object('django.conf:settings', namespace='CELERY') - -app.autodiscover_tasks(['my_chat']) \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/chat/routing.py b/Back-End/Dockers/chatservice/chat/chat/routing.py deleted file mode 100644 index fa91f5c7..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/routing.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import re_path -from my_chat import consumers - -websocket_urlpatterns = [ - re_path(r'ws/chat/(?P\d+)/$', consumers.ChatConsumer.as_asgi()), -] \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/chat/settings.py b/Back-End/Dockers/chatservice/chat/chat/settings.py deleted file mode 100644 index f18632b1..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/settings.py +++ /dev/null @@ -1,217 +0,0 @@ -""" -Django settings for chat project. - -Generated by 'django-admin startproject' using Django 5.1.1. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path -import secrets , os - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-c-&5anrd7(h=4(s2+_@-&gwvt2*pr+$d4t82*3@wuev&0n&nb(' -API_KEY = os.getenv('API_KEY', '123') -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG', True) - -ALLOWED_HOSTS = ['*'] - -# Application definition - -INSTALLED_APPS = [ - 'daphne', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'my_chat', - 'oauth2_provider', - 'django_filters', - 'corsheaders', - 'channels', - 'celery', - 'redis', -] - -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'my_chat.middleware.TokenAuthMiddlewareHTTP', -] - # 'oauth2_provider.middleware.OAuth2TokenMiddleware', - -ROOT_URLCONF = 'chat.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'chat.wsgi.application' -ASGI_APPLICATION = 'chat.asgi.application' - -oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': os.getenv('OAUTH2_INTROSPECTION_URL', 'http://localhost:8000/o/introspect/'), - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': os.getenv('SERVICE_PASSWORD', '123'), -} - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'chat'), - 'USER': os.getenv('POSTGRES_USER', 'postgres'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', 'postgres'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5432'), - } -} - - - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -CELERY_BROKER_URL = 'redis://localhost:6700/0' -CELERY_RESULT_BACKEND = 'redis://localhost:6700/0' -CELERY_BEAT_SCHEDULE = { - 'similar_users_chats': { - 'task': 'my_chat.tasks.similar_users_chats', - 'schedule': 3600, #seconds - }, -} - -BUFET_URL = os.getenv('bufet_url', 'http://localhost:8003/task/bufet') - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = '/home/lollo/Documents/challenge_fides/Back-End/login/staticfiles' -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -CORS_ORIGIN_ALLOW_ALL = True - -CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://172.18.0.1:6700/1', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } -} - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [('172.18.0.1', 6700)], - }, - } -} - - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'my_chat.authentications.TokenAuthentication', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'my_chat.middleware.TokenAuthPermission', - ], -} - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'loki': { - 'class': 'logging.StreamHandler', - 'formatter': 'detailed', - 'stream': 'ext://sys.stdout', # Sends logs to stdout for Loki - }, - }, - 'loggers': { - 'django': { - 'handlers': ['loki'], - 'level': 'INFO', - 'propagate': True, - }, - }, - 'formatters': { - 'detailed': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', - }, - }, -} diff --git a/Back-End/Dockers/chatservice/chat/chat/urls.py b/Back-End/Dockers/chatservice/chat/chat/urls.py deleted file mode 100644 index 9ba85529..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -URL configuration for chat project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path , include - -urlpatterns = [ - path('admin/', admin.site.urls), - path('chat/', include('my_chat.urls')), - #DEBUG# path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), -] diff --git a/Back-End/Dockers/chatservice/chat/chat/wsgi.py b/Back-End/Dockers/chatservice/chat/chat/wsgi.py deleted file mode 100644 index f3266695..00000000 --- a/Back-End/Dockers/chatservice/chat/chat/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for chat project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') - -application = get_wsgi_application() diff --git a/Back-End/Dockers/chatservice/chat/chat_db b/Back-End/Dockers/chatservice/chat/chat_db deleted file mode 100644 index 830c80b3..00000000 Binary files a/Back-End/Dockers/chatservice/chat/chat_db and /dev/null differ diff --git a/Back-End/Dockers/chatservice/chat/db.sqlite3 b/Back-End/Dockers/chatservice/chat/db.sqlite3 deleted file mode 100644 index 84cf46f4..00000000 Binary files a/Back-End/Dockers/chatservice/chat/db.sqlite3 and /dev/null differ diff --git a/Back-End/Dockers/chatservice/chat/manage.py b/Back-End/Dockers/chatservice/chat/manage.py deleted file mode 100755 index 60eb2486..00000000 --- a/Back-End/Dockers/chatservice/chat/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/Back-End/Dockers/chatservice/chat/my_chat/__init__.py b/Back-End/Dockers/chatservice/chat/my_chat/__init__.py deleted file mode 100644 index 5d36d902..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'my_chat.apps.MyChatConfig' \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/admin.py b/Back-End/Dockers/chatservice/chat/my_chat/admin.py deleted file mode 100644 index 44ea1c77..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin -from django.contrib.auth import get_user_model -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -# Register your models here. - -User = get_user_model() - -@receiver(post_migrate) -def create_superuser(sender, **kwargs): - if not User.objects.filter(email='pasquale@example.com').exists(): - User.objects.create_superuser(username='pasquale', email='pasquale@example.com', password='123') - print('Superuser created successfully.') \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/apps.py b/Back-End/Dockers/chatservice/chat/my_chat/apps.py deleted file mode 100644 index 31575d8b..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/apps.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.apps import AppConfig - -class MyChatConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'my_chat' - - def ready(self): - try: - from .authentications import register_self , user_register_self - from .admin import create_superuser - #user_register_self() - #print("User login successful") - register_self() - except Exception as e: - # Optionally log the exception - print(f"Error during registration: {e}") - pass \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/authentications.py b/Back-End/Dockers/chatservice/chat/my_chat/authentications.py deleted file mode 100644 index c3ba3b8f..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/authentications.py +++ /dev/null @@ -1,192 +0,0 @@ -import requests -from oauth2_provider.models import AccessToken -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -from django.conf import settings -from datetime import datetime, timedelta -from chat.settings import oauth2_settings -import json , os - -def login_self(): - login_url = os.getenv('LOGIN_URL', 'http://localhost:8000/login/login') - data = { - 'email': os.getenv('CHAT_EMAIL'), - 'password': os.getenv('CHAT_PASSWORD'), - } - - response = requests.post(login_url, json=data) - - if response.status_code != 200: - raise Exception('Failed to login user') - return response.json() - -def user_register_self(): - register_url = os.getenv('REGISTER_URL', 'http://localhost:8000/login/register') - data = { - 'email': os.getenv('CHAT_EMAIL'), - 'username': 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S'), - 'password': os.getenv('CHAT_PASSWORD'), - } - response = requests.post(register_url, json=data) - - if response.status_code == 400: - login_self() - elif response.status_code != 201: - raise Exception('Failed to register user') - else: - login_self() - - -def register_self(): - csrf_url = os.getenv('CSRF_URL', 'http://localhost:8000/login/csrf') - register_url = os.getenv('REGISTER_SERVICE_URL', 'http://localhost:8000/login/register_service') - - data = { - 'name': 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S'), - 'service_password': oauth2_settings['SERVICE_PASSWORD'], - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'redirect_uris': 'http://localhost:8000', - 'client_id': oauth2_settings['CLIENT_ID'], - 'client_secret': oauth2_settings['CLIENT_SECRET'], - } - - session = requests.Session() - csrf_resp = session.get(csrf_url) - - if 'csrftoken' not in csrf_resp.cookies: - raise Exception('CSRF token not found in response cookies') - - csrf_token = csrf_resp.cookies['csrftoken'] - access_token = oauth2_settings['TOKEN'] - headers = { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrf_token, - 'Authorization': 'Bearer ' + access_token, - } - - response = session.post(register_url, headers=headers, json=data) - print("Response status code:", response.status_code) - print("Response text:", response.json()) - if response.status_code != 200 and response.status_code != 201: - raise Exception('Failed to register application') - try: - app_data = response.json() - except json.JSONDecodeError: - raise Exception('Failed to parse JSON response') - - -#auth classes -import requests -from django.conf import settings -from django.core.cache import cache -from django.contrib.auth.models import AnonymousUser -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -import base64 ,requests -from chat.settings import oauth2_settings -from my_chat.models import UserProfile -from django.contrib.auth import get_user_model - -class TokenAuthentication(BaseAuthentication): - def authenticate(self, request): - if request.path == '/chat/new_user/': - request.user = AnonymousUser() - return None - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - return None - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - - if user is not None: - return (user, token) # Return tuple (user, auth) as required by DRF - else: - raise AuthenticationFailed("Invalid or inactive token") - - def get_user_from_token(self, token): - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - User = UserProfile.objects.get(email=email) - try: - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except e: - return AnonymousUser() - else: - print('Token introspection failed:' f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return None # Default to None if authentication fails - - - - - - # def get_user_from_token(self, token): - # user = cache.get(token) - # if user: - # return user - - # # Token introspection request - # introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - # client_id = oauth2_settings['CLIENT_ID'] - # client_secret = oauth2_settings['CLIENT_SECRET'] - - # try: - # response = requests.post(introspection_url, data={ - # 'token': token, - # 'client_id': client_id, - # 'client_secret': client_secret, - # }) - - # try: - # response_json = response.json() - # print(f'\033[96mResponse1 json: {response_json}\033[0m') #DEBUG# # Testo ciano - # except requests.exceptions.JSONDecodeError: - # print('\033[91mFailed to decode JSON response\033[0m') #DEBUG# # Testo rosso - # return AnonymousUser() - - # if response.status_code == 200: - # data = response.json() - # if data.get('active'): - # user_id = data.get('username') - # User = get_user_model() - # try: - # user = User.objects.get(user_id=user_id) - # # Cache user for token expiry duration (default to 5 min if not provided) - # expires_in = data.get('expires_in', 300) - # cache.set(token, user, timeout=expires_in) - # return user - # except User.DoesNotExist: - # pass - # except requests.RequestException as e: - # raise AuthenticationFailed(f"Token introspection failed: {str(e)}") - diff --git a/Back-End/Dockers/chatservice/chat/my_chat/consumers.py b/Back-End/Dockers/chatservice/chat/my_chat/consumers.py deleted file mode 100644 index 082124ae..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/consumers.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from channels.generic.websocket import AsyncWebsocketConsumer -from my_chat.models import ChatRoom, ChatMessage -from .user_services import register_user -from asgiref.sync import sync_to_async -from my_chat.models import ChatRoom, ChatMessage - -class ChatConsumer(AsyncWebsocketConsumer): - async def connect(self): - self.room_id = self.scope['url_route']['kwargs']['room_id'] - self.room_group_name = f'chat_{self.room_id}' - # Join room group - await self.channel_layer.group_add( - self.room_group_name, - self.channel_name - ) - - await self.accept() - - async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard( - self.room_group_name, - self.channel_name - ) - - async def save_message(self, room_id, message, sender, timestamp): - try: - room = await sync_to_async(ChatRoom.objects.get)(room_id=room_id) - except ChatRoom.DoesNotExist: - return {'error': 'Room does not exist'} -# # Retrieve the chat room asynchronously -# try: -# room = await sync_to_async(ChatRoom.objects.get)(id=room_id) -# except ChatRoom.DoesNotExist: -# # Send error back if room does not exist -# await self.send(text_data=json.dumps({ -# 'error': 'Room does not exist' -# })) -# return - - # Save message to the database - await sync_to_async(ChatMessage.objects.create)( - room=room, - message=message, - sender=sender, - timestamp=timestamp - ) - return None - - async def receive(self, text_data): - text_data_json = json.loads(text_data) - message = text_data_json['message'] - room_id = text_data_json['room_id'] - sender = text_data_json['sender'] - timestamp = text_data_json.get('timestamp') - - await self.channel_layer.group_send( - self.room_group_name, - { - 'type': 'chat_message', - 'message': message, - 'room_id': room_id, - 'timestamp': timestamp, - 'sender': sender - } - ) - - error = await self.save_message(room_id, message, sender, timestamp) - if error: - await self.send(text_data=json.dumps(error)) - - async def chat_message(self, event): - message = event['message'] - room_id = event['room_id'] - sender = event['sender'] - timestamp = event['timestamp'] - - async def chat_message(self, event): - # Send message to WebSocket - await self.send(text_data=json.dumps({ - 'message': event['message'], - 'room_id': event['room_id'], - 'sender': event['sender'], - 'timestamp': event['timestamp'] - })) diff --git a/Back-End/Dockers/chatservice/chat/my_chat/dicts.py b/Back-End/Dockers/chatservice/chat/my_chat/dicts.py deleted file mode 100644 index c547fa21..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/dicts.py +++ /dev/null @@ -1,42 +0,0 @@ -room_names = [ - "General", - "Random", - "TechTalk", - "Gaming", - "Music", - "Movies", - "Books", - "Travel", - "Food", - "Fitness", - "Health", - "Education", - "Science", - "History", - "Art", - "Photography", - "Programming", - "Startups", - "Business", - "Finance", - "Investing", - "Crypto", - "AI", - "MachineLearning", - "DataScience", - "Sports", - "Soccer", - "Basketball", - "Baseball", - "Tennis", - "Running", - "Yoga", - "Meditation", - "Pets", - "Parenting", - "Relationships", - "Advice", - "Support", - "Announcements", - "Events" -] \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/middleware.py b/Back-End/Dockers/chatservice/chat/my_chat/middleware.py deleted file mode 100644 index f1d3bb4f..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/middleware.py +++ /dev/null @@ -1,206 +0,0 @@ -from urllib.parse import parse_qs -from channels.middleware import BaseMiddleware -from channels.db import database_sync_to_async -from django.contrib.auth.models import AnonymousUser -from channels.auth import AuthMiddlewareStack -from django.core.cache import cache -from django.contrib.auth import get_user_model -import requests , base64 -from .models import UserProfile -from django.utils.deprecation import MiddlewareMixin -from chat.settings import oauth2_settings - -@database_sync_to_async -def get_user_from_token(token, scope): - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - try: - User = UserProfile.objects.get(email=email) - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except UserProfile.DoesNotExist: - return AnonymousUser() - else: - return AnonymousUser() - except requests.RequestException as e: - return AnonymousUser() - - return AnonymousUser() - -class TokenAuthMiddleware(BaseMiddleware): - async def __call__(self, scope, receive, send): - query_string = parse_qs(scope["query_string"].decode()) - if scope['path'] == '/chat/new_user/': - scope['user'] = AnonymousUser() - return await super().__call__(scope, receive, send) - try: - token = query_string.get("token") - if token: - scope["user"] = await get_user_from_token(token[0], scope) - else: - scope["user"] = AnonymousUser() - except Exception as e: - scope["user"] = AnonymousUser() - return await super().__call__(scope, receive, send) - -def TokenAuthMiddlewareStack(inner): - return TokenAuthMiddleware(AuthMiddlewareStack(inner)) - -class TokenAuthMiddlewareHTTP(MiddlewareMixin): - def process_request(self, request): - if request.path == '/chat/new_user/': - print(request.path) - request.user = AnonymousUser() - return - - - token = request.META.get('HTTP_AUTHORIZATION') - if token: - token = token.replace("Bearer ", "") - user = self.get_user_from_token_sync(token) - request.user = user - else: - request.user = AnonymousUser() - - def get_user_from_token_sync(self, token): - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - try: - User = UserProfile.objects.get(email=email) - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except UserProfile.DoesNotExist: - return AnonymousUser() - else: - print('Token introspection failed:' f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return AnonymousUser() - - -from rest_framework.permissions import BasePermission -from django.contrib.auth.models import AnonymousUser -from django.core.cache import cache -from chat.settings import oauth2_settings -import requests -import base64 -from .models import UserProfile - -class TokenAuthPermission(BasePermission): - def get_user_from_token(self, token): - - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers, data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - try: - user = UserProfile.objects.get(email=email) - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, user, timeout=300) - return user - except UserProfile.DoesNotExist: - print('qua') - return AnonymousUser() - else: - print('Token introspection failed:', f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return AnonymousUser() - - return AnonymousUser() - - def has_permission(self, request, view): - if request.path == '/chat/new_user/': - request.user = AnonymousUser() - return True - token = request.META.get('HTTP_AUTHORIZATION') - if token: - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - if user and not isinstance(user, AnonymousUser): - request.user = user - return True - return False - -from django.conf import settings -from rest_framework.permissions import BasePermission - -class APIKeyPermission(BasePermission): - def has_permission(self, request, view): - api_key = request.headers.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/models.py b/Back-End/Dockers/chatservice/chat/my_chat/models.py deleted file mode 100644 index 92e7a0cc..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/models.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import models - -# Create your models here. - -class UserProfile(models.Model): - user_id = models.IntegerField(primary_key=True) - username = models.CharField(max_length=255, null=True) - email = models.EmailField(max_length=255, unique=True, null=True) - is_staff = models.BooleanField(default=False) - -class ChatRoom(models.Model): - room_id = models.AutoField(primary_key=True) - room_name = models.CharField(max_length=100) - room_description = models.TextField() - users = models.ManyToManyField(UserProfile) - creation_time = models.DateTimeField(auto_now_add=True) - creator = models.ForeignKey(UserProfile, related_name='creator', on_delete=models.SET_NULL, null=True) - - def get_user_number(self): - return self.users.count() - -class ChatMessage(models.Model): - message_id = models.AutoField(primary_key=True) - room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE) - message = models.TextField() - sender = models.TextField() - timestamp = models.DateTimeField(auto_now_add=True) - -def get_user_model(user_id): - return UserProfile.objects.get(user_id=user_id) \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/notification.py b/Back-End/Dockers/chatservice/chat/my_chat/notification.py deleted file mode 100644 index 282e14c5..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/notification.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import models -import os -import asyncio -import aiohttp - -Microservices = { - 'Login': os.getenv('LOGIN_SERVICE', 'http://localhost:8000'), - 'Chat': os.getenv('CHAT_SERVICE', 'http://localhost:8001'), - 'Users': os.getenv('USERS_SERVICE', 'http://localhost:8002'), - 'Notifications': os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003'), - 'Personal' : "Self", -} - -Types = { - 'IM' : 'Immediate', - 'QU' : 'Queued', - 'SC' : 'Scheduled', - 'SE' : 'Sent', -} - -class BaseNotification(models.Model): - id = models.AutoField(primary_key=True) - Sender = models.CharField(max_length=20, choices=Microservices) - message = models.TextField() - - class Meta: - abstract = True - -class UserNotification(BaseNotification): - user_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class GroupNotification(BaseNotification): - group_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class ImmediateNotification(UserNotification, GroupNotification): - pass - -class ScheduledNotification(UserNotification, GroupNotification): - pass - -async def SendNotification(Notification): - noification_url = Microservices['Notifications'] - async with aiohttp.ClientSession() as session: - async with session.post(noification_url, data=Notification) as response: - return response.status diff --git a/Back-End/Dockers/chatservice/chat/my_chat/serializers.py b/Back-End/Dockers/chatservice/chat/my_chat/serializers.py deleted file mode 100644 index d749f668..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/serializers.py +++ /dev/null @@ -1,23 +0,0 @@ -from rest_framework import serializers -from .models import ChatRoom, ChatMessage, UserProfile - -class userSerializer(serializers.ModelSerializer): - class Meta: - model = UserProfile - fields = '__all__' - - -class chat_roomSerializer(serializers.ModelSerializer): - class Meta: - model = ChatRoom - fields = ['room_id', 'room_name', 'room_description', 'creator', 'users'] - -class chat_messageSerializer(serializers.ModelSerializer): - message_id = serializers.IntegerField() - room_id = serializers.PrimaryKeyRelatedField(queryset=ChatRoom.objects.all()) - message = serializers.CharField() - sender = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()) - timestamp = serializers.DateTimeField() - class Meta: - model = ChatMessage - fields = '__all__' diff --git a/Back-End/Dockers/chatservice/chat/my_chat/tasks.py b/Back-End/Dockers/chatservice/chat/my_chat/tasks.py deleted file mode 100644 index 942afa17..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/tasks.py +++ /dev/null @@ -1,114 +0,0 @@ -from celery import shared_task -from .dicts import room_names -import random, requests -from sklearn.metrics.pairwise import cosine_similarity -import numpy as np -from .models import ChatRoom, UserProfile -from chat.settings import bufet_url -import csv -from .views import CreateChannelGroupView -import requests - -def get_users(): - # Get all users from the endpoint - response = requests.get(bufet_url) - users_data = response.json() - users_withtask = [] - for user_id, tasks in users_data.items(): - user = UserProfile.objects.get(user_id=user_id) - user.tasks = tasks - users_withtask.append(user) - - return users_withtask - -@shared_task -def create_chat_room(): - users = UserProfile.objects.all() - if len(users) < 4: - return False - else: - room = ChatRoom.objects.create( - room_name=room_names.get(random.randint(0, len(room_names) - 1)) + " Room", - room_description="Enjoy chatting with someone new!", - number_of_users=0 - ) - room.save() - already_added = set() - - user_with_taks = get_users() - # Rempove the users that were already selected - user_with_taks = [user for user in user_with_taks if user.id not in selected_users] - - # Calculate similarity between users and select the most similar ones - user_pairs = [(i, j) for i in range(len(user_with_taks)) for j in range(i + 1, len(user_with_taks))] - similarities = [(i, j, calculate_similarity(user_with_taks[i], user_with_taks[j])) for i, j in user_pairs] - similarities.sort(key=lambda x: x[2], reverse=True) - - # Select the top 4 most similar users - - selected_users = set() - for i, j, _ in similarities: - if len(selected_users) < 4: - selected_users.add(users[i]) - selected_users.add(users[j]) - if len(selected_users) >= 4: - break - - - # Add users to the chat room - createChatView = CreateChannelGroupView() - request = requests.Request() - response = createChatView.post(request, room_name=room.room_name, user_ids=[user.id for user in selected_users]) - if response.status_code != 200: - raise Exception('Failed create chat room') - - for user in selected_users: - already_added.add(user.id) - # otuput data to be visualized - with open('random_chats_data.csv', mode='a') as file: - writer = csv.writer(file) - writer.writerow([user.id for user in selected_users]) - # Output all similarities to be visualized - with open('similarities_data.csv', mode='a') as file: - writer = csv.writer(file) - for i, j, similarity in similarities: - writer.writerow([user_with_taks[i].id, user_with_taks[j].id, similarity]) - - # otuput data to be visualized - with open('random_chats_data.csv', mode='a') as file: - writer = csv.writer(file) - writer.writerow([user.id for user in selected_users]) - # Output all similarities to be visualized - with open('similarities_data.csv', mode='a') as file: - writer = csv.writer(file) - for i, j, similarity in similarities: - writer.writerow([user_with_taks[i].id, user_with_taks[j].id, similarity]) - - -def calculate_similarity(user1, user2): - # Convert user profiles to numpy arrays - user1_profile = np.array(user1.tasks) - user2_profile = np.array(user2.tasks) - - # Reshape the arrays to 2D (required by cosine_similarity) - user1_profile = user1_profile.reshape(1, -1) - user2_profile = user2_profile.reshape(1, -1) - - # Calculate cosine similarity - similarity = cosine_similarity(user1_profile, user2_profile) - - return similarity[0][0] - -def cancell_chat_room(): - - - room = ChatRoom.objects.last() - room.delete() -#input - # response = objDict() - # for x in range(len(tasks)): - # response = tasks[x] - # for y in range(len(task.users)): - # response[x] += task.users[y] - # print(response.json()) -#output \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/tests.py b/Back-End/Dockers/chatservice/chat/my_chat/tests.py deleted file mode 100644 index abfe6a56..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/tests.py +++ /dev/null @@ -1,29 +0,0 @@ -from channels.testing import WebsocketCommunicator -from django.contrib.auth import get_user_model -from django.test import TransactionTestCase -from chat.asgi import application -from asgiref.sync import sync_to_async - -class ChatTests(TransactionTestCase): - async def test_websocket_connection(self): - # Create a mock user using sync_to_async - User = get_user_model() - user = await sync_to_async(User.objects.create_user)(username='testuser', password='password') - - # Define the headers - headers = [(b'origin', b'http://localhost')] - - # Create the WebsocketCommunicator with the URL path and headers - communicator = WebsocketCommunicator(application, "/ws/chat/room_1/", headers=headers) - - # Set the user in the scope - communicator.scope['user'] = user - - connected, subprotocol = await communicator.connect() - self.assertTrue(connected) - - await communicator.send_json_to({'message': 'Hello, WebSocket!', 'room_id': 'room_1', 'sender': 'testuser', 'timestamp': '2021-01-01T00:00:00Z'}) - response = await communicator.receive_json_from() - self.assertEqual(response['message'], 'Hello, WebSocket!') - - await communicator.disconnect() \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/urls.py b/Back-End/Dockers/chatservice/chat/my_chat/urls.py deleted file mode 100644 index e68e3f6b..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.urls import path, re_path -from . import views - -# chat/... - -urlpatterns = [ - path('chat_rooms//get_message/', views.GetChatMessage.as_view()), # GET localhost:8000/chat_rooms/1/get_message/:20 - path('chat_rooms//', views.GetChatInfo.as_view()), - path('new_user/', views.new_user.as_view()), - path('create_channel_group/', views.CreateChannelGroupView.as_view()), - path('chat_rooms/create/', views.CreateChat.as_view()), - path('chat_rooms/getchat/', views.GetChats.as_view(), name='get_chats'), - path('chat_data/', views.DownloadChatRoomData.as_view()), - path('user_similarities/', views.DownloadSimilaritiesData.as_view()), - path('chat_rooms//add_user/', views.AddUsersToChat.as_view()), - ] \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/user_services.py b/Back-End/Dockers/chatservice/chat/my_chat/user_services.py deleted file mode 100644 index 1112493a..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/user_services.py +++ /dev/null @@ -1,5 +0,0 @@ -from .models import ChatRoom, ChatMessage, UserProfile -from channels.db import database_sync_to_async - -async def register_user(username): - await database_sync_to_async(UserProfile.objects.get_or_create)(username=username) \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/chat/my_chat/views.py b/Back-End/Dockers/chatservice/chat/my_chat/views.py deleted file mode 100644 index 65dfc844..00000000 --- a/Back-End/Dockers/chatservice/chat/my_chat/views.py +++ /dev/null @@ -1,148 +0,0 @@ -from rest_framework import generics, filters, permissions -from django.http import HttpResponse -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView -from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync -from .models import ChatRoom, UserProfile, ChatMessage -from django.contrib.auth.models import AnonymousUser -from .serializers import chat_roomSerializer, chat_messageSerializer, userSerializer -from .middleware import TokenAuthPermission, APIKeyPermission -from .authentications import TokenAuthentication -class GetChatMessage(generics.ListAPIView): - serializer_class = chat_messageSerializer - lookup_url_kwarg = 'room_id' - - def get_queryset(self): - room_id = self.kwargs.get(self.lookup_url_kwarg) - # user = self.request.user - - # if isinstance(user, AnonymousUser): - # raise ValueError('User is not authenticated') - - # if not ChatRoom.objects.filter(room_id=room_id, users=user).exists(): - # raise ValueError('User is not in the room') - # return ChatMessage.objects.none() - - return ChatMessage.objects.filter(room_id=room_id) - - -class GetChatInfo(generics.RetrieveAPIView): - serializer_class = chat_roomSerializer - lookup_url_kwarg = 'room_id' - - def get_queryset(self): - room_id = self.kwargs.get(self.lookup_url_kwarg) - user = self.request.user - - if isinstance(user, AnonymousUser): - raise ValueError('User is not authenticated') - - if not ChatRoom.objects.filter(room_id=room_id, users=user).exists(): - raise ValueError('User is not in the room') - return ChatRoom.objects.none() - - return ChatRoom.objects.filter(room_id=room_id) - -class new_user(generics.ListCreateAPIView): - serializer_class = userSerializer - queryset = UserProfile.objects.all() - - def get_permissions(self): - if self.request.method == 'POST': - print("POST here") - self.permission_classes = [APIKeyPermission] - else: - self.permission_classes = (permissions.AllowAny,) - return super().get_permissions() - - -class CreateChat(generics.ListCreateAPIView): - serializer_class = chat_roomSerializer - queryset = ChatRoom.objects.all() - -class GetChats(generics.ListAPIView): - serializer_class = chat_roomSerializer - permission_classes = [TokenAuthPermission] - - def get_queryset(self): - user_id = self.request.query_params.get('user_id') - if not user_id: - return ChatRoom.objects.none() - - return ChatRoom.objects.filter(users__user_id=user_id) - -class AddUsersToChat(generics.UpdateAPIView): - serializer_class = chat_roomSerializer - lookup_url_kwarg = 'room_id' - - def get_queryset(self): - room_id = self.kwargs.get(self.lookup_url_kwarg) - user = self.request.user - - if isinstance(user, AnonymousUser): - raise ValueError('User is not authenticated') - - if not ChatRoom.objects.filter(room_id=room_id, users=user).exists(): - raise ValueError('User is not in the room') - return ChatRoom.objects.none() - - return ChatRoom.objects.filter(room_id=room_id) - -class CreateChannelGroupView(APIView): - def post(self, request, *args, **kwargs): - room_id = request.data.get('room_id') - user_ids = request.data.get('user_ids', []) - - if not room_id: - return Response({'error': 'Room name is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - room = ChatRoom.objects.get(room_id=room_id) - except ChatRoom.DoesNotExist: - return Response({'error': 'Room does not exist'}, status=status.HTTP_404_NOT_FOUND) - - users = UserProfile.objects.filter(user_id__in=user_ids) - if not users.exists(): - return Response({'error': 'No valid users found with user_id: ' + user_ids}, status=status.HTTP_400_BAD_REQUEST) - - room.users.add(*users) - room.save() - - channel_layer = get_channel_layer() - group_name = f'chat_{room.room_name}' - - for user in users: - async_to_sync(channel_layer.group_add)( - group_name, - f'user_{user.user_id}' - ) - - return Response({'message': 'Users added to group'}, status=status.HTTP_200_OK) - -class DownloadChatRoomData(APIView): - def get(self, request): - with open('random_chats_data.csv', 'r') as file: - data = file.read() - if not data: - return Response({'error': 'No data found'}, status=status.HTTP_404_NOT_FOUND) - headers = { - 'Content-Type': 'text/csv', - 'Content-Disposition': 'attachment; filename=random_chats_data.csv' - } - response = HttpResponse(data, headers) - return Response(response, status=status.HTTP_200_OK) - -class DownloadSimilaritiesData(APIView): - def get(self, request): - with open('similarities_data.csv', 'r') as file: - data = file.read() - if not data: - return Response({'error': 'No data found'}, status=status.HTTP_404_NOT_FOUND) - headers = { - 'Content-Type': 'text/csv', - 'Content-Disposition': 'attachment; filename=similarities_data.csv' - } - response = HttpResponse(data, headers) - return Response(response, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/entrypoint.sh b/Back-End/Dockers/chatservice/entrypoint.sh deleted file mode 100644 index 776d25ad..00000000 --- a/Back-End/Dockers/chatservice/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -cd /chat - -# Apply datab -python3 manage.py makemigrations -python3 manage.py makemigrations my_chat -python3 manage.py migrate - -# Star -python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/Back-End/Dockers/chatservice/requirements.txt b/Back-End/Dockers/chatservice/requirements.txt deleted file mode 100644 index 61ab4065..00000000 --- a/Back-End/Dockers/chatservice/requirements.txt +++ /dev/null @@ -1,51 +0,0 @@ -amqp==5.3.1 -asgiref==3.8.1 -async-timeout==5.0.1 -attrs==24.2.0 -autobahn==24.4.2 -Automat==24.8.1 -billiard==4.2.1 -celery==5.4.0 -certifi==2024.8.30 -cffi==1.17.1 -channels==4.2.0 -charset-normalizer==3.4.0 -click==8.1.7 -click-didyoumean==0.3.1 -click-plugins==1.1.1 -click-repl==0.3.0 -constantly==23.10.4 -cryptography==44.0.0 -daphne==4.1.2 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 -django-oauth-toolkit==3.0.1 -django-redis==5.4.0 -djangorestframework==3.15.2 -hyperlink==21.0.0 -idna==3.10 -incremental==24.7.2 -jwcrypto==1.5.6 -kombu==5.4.2 -oauthlib==3.2.2 -prompt_toolkit==3.0.48 -psycopg==3.2.3 -pyasn1==0.6.1 -pyasn1_modules==0.4.1 -pycparser==2.22 -pyOpenSSL==24.3.0 -python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 -service-identity==24.2.0 -six==1.16.0 -sqlparse==0.5.2 -Twisted==24.11.0 -txaio==23.1.1 -typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 -vine==5.1.0 -wcwidth==0.2.13 -zope.interface==7.2 diff --git a/Back-End/Dockers/docker-compose.yml b/Back-End/Dockers/docker-compose.yml deleted file mode 100644 index ed7000fc..00000000 --- a/Back-End/Dockers/docker-compose.yml +++ /dev/null @@ -1,317 +0,0 @@ -# TODO add the celery wrokers and the celery beat -services: - usertask_db: - build: - context: ./usertask_db - dockerfile: Dockerfile - container_name: container_usertask_db - ports: - - "5434:5432" - networks: - - usertask_network - - exporters_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pasquale -d usertask_db"] - interval: 30s - timeout: 10s - retries: 5 - env_file: - - ./usertask_service/.env - - # usertask_service: - # build: - # context: ./usertask_service - # dockerfile: Dockerfile - # container_name: container_usertask_service - # ports: - # - "8002:8000" - # networks: - # - usertask_network - # - auth_network - # - notification_network - # depends_on: - # usertask_db: - # condition: service_healthy - # loginservice: - # condition: service_healthy - # env_file: - # - ./usertask_service/.env - - login_db: - build: - context: ./login_db - dockerfile: Dockerfile - container_name: container_login_db - ports: - - "5435:5432" - networks: - - login_network - - exporters_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pasquale -d login_db"] - interval: 30s - timeout: 10s - retries: 5 - env_file: - - ./loginservice/.env - - # loginservice: - # build: - # context: ./loginservice - # dockerfile: Dockerfile - # container_name: container_loginService - # ports: - # - "8000:8000" - # networks: - # - login_network - # - auth_network - # env_file: - # - ./loginservice/.env - # depends_on: - # login_db: - # condition: service_healthy - # healthcheck: - # test: ["CMD-SHELL", "curl -f http://localhost:8000/login/health/ || exit 1"] - # interval: 30s - # timeout: 10s - # retries: 5 - - chat_db: - build: - context: ./chat_db - dockerfile: Dockerfile - container_name: container_chat_db - ports: - - "5436:5432" - networks: - - exporters_network - - chat_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pasquale -d chat_db"] - interval: 30s - timeout: 10s - retries: 5 - env_file: - - ./chatservice/.env - - - # chatservice: - # build: - # context: ./chatservice - # dockerfile: Dockerfile - # container_name: container_chat_service - # ports: - # - "8001:8000" - # networks: - # - chat_network - # - notification_network - # - auth_network - # depends_on: - # chat_db: - # condition: service_healthy - # redis_chat: - # condition: service_healthy - # loginservice: - # condition: service_healthy - # env_file: - # - ./chatservice/.env - - notification_db: - build: - context: ./notification_db - dockerfile: Dockerfile - container_name: container_notification_db - ports: - - "5438:5432" - networks: - - back-end - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pasquale -d notification_db"] - interval: 30s - timeout: 10s - retries: 5 - - pong_db: - image: postgres:latest - container_name: container_pong_db - ports: - - "5439:5432" - networks: - - back-end - healthcheck: - test: ["CMD-SHELL", "pg_isready -U pasquale -d notification_db"] - interval: 30s - timeout: 10s - retries: 5 - environment: - - POSTGRES_DB=pong_db - - POSTGRES_USER=pasquale - - POSTGRES_PASSWORD=123 - - # notification_service: - # build: - # context: ./notification_service - # dockerfile: Dockerfile - # container_name: container_notification_service - # ports: - # - "8003:8000" - # networks: - # - internal_notification_network - # - auth_network - # - notification_network - - - redis_chat: - image: "redis:latest" - ports: - - "6700:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - networks: - - chat_network - - exporters_network - - redis_notification: - image: "redis:latest" - ports: - - "6701:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - networks: - - internal_notification_network - - exporters_network - - redis_taskuser: - image: "redis:latest" - ports: - - "6702:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - networks: - - usertask_network - - exporters_network - - # celery_worker_notificaton: - # build: - # context: ./celery_notification - # dockerfile: Dockerfile - # container_name: celery_worker_notification - # networks: - # - back-end - # depends_on: - # - notification_db - # - redis_notification - - # prometheus: - # build: - # context: ./prometheus - # dockerfile: Dockerfile - # container_name: prometheus - # ports: - # - "9091:9090" - # volumes: - # - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - # networks: - # - exporters_network - # - logging_network - - # loki: - # build: - # context: ./loki - # dockerfile: Dockerfile - # container_name: loki - # ports: - # - "3101:3100" - # volumes: - # - ./loki/loki-config.yaml:/etc/loki/local-config.yaml - # - loki-data:/var/lib/loki - # networks: - # - exporters_network - # - logging_network - - # grafana: - # build: - # context: ./grafana - # dockerfile: Dockerfile - # container_name: grafana - # ports: - # - "3001:3000" - # environment: - # - GF_SECURITY_ADMIN_PASSWORD=admin # TODO: Change this password - # volumes: - # - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources - # - grafana-data:/var/lib/grafana - # depends_on: - # - prometheus - # - loki - # networks: - # - logging_network - # - exporters_network - # promtail: - # image: grafana/promtail:latest - # container_name: promtail - # volumes: - # - /var/log:/var/log - # - ./promtail/promtail-config.yaml:/etc/promtail/config.yaml - # - ./promtail/positions.yaml:/tmp/positions.yaml # Persist positions file - # networks: - # - back-end - # depends_on: - # - loki - # postgres_exporter: - # image: prometheuscommunity/postgres-exporter - # container_name: postgres_exporter - # environment: - # DATA_SOURCE_NAME: "postgresql://pasquale:123@container_usertask_db:5432/usertask_db?sslmode=disable,postgresql://pasquale:123@container_login_db:5432/login_db?sslmode=disable,postgresql://pasquale:123@container_chat_db:5432/chat_db?sslmode=disable" - # ports: - # - "9187:9187" - # networks: - # - exporters_network - # depends_on: - # - usertask_db - # - login_db - # - chat_db - - # redis_exporter: - # image: bitnami/redis-exporter:latest - # container_name: redis_exporter - # environment: - # - REDIS_ADDR=redis:6379 - # ports: - # - "9121:9121" - # networks: - # - exporters_network - -volumes: - prometheus_data: {} - loki-data: {} - grafana-data: {} - -networks: - usertask_network: - driver: bridge - chat_network: - driver: bridge - login_network: - driver: bridge - internal_notification_network: - driver: bridge - exporters_network: - driver: bridge - auth_network: - driver: bridge - logging_network: - driver: bridge - notification_network: - driver: bridge - back-end: - driver: bridge diff --git a/Back-End/Dockers/grafana/Dockerfile b/Back-End/Dockers/grafana/Dockerfile deleted file mode 100644 index 66df6def..00000000 --- a/Back-End/Dockers/grafana/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM grafana/grafana:latest diff --git a/Back-End/Dockers/grafana/provisioning/datasources/datasources.yaml b/Back-End/Dockers/grafana/provisioning/datasources/datasources.yaml deleted file mode 100644 index d9690c22..00000000 --- a/Back-End/Dockers/grafana/provisioning/datasources/datasources.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: 1 - -datasources: - - name: Loki - type: loki - access: proxy - url: http://loki:3100 - isDefault: true - jsonData: - maxLines: 1000 - - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: false \ No newline at end of file diff --git a/Back-End/Dockers/login_db/Dockerfile b/Back-End/Dockers/login_db/Dockerfile deleted file mode 100644 index 535b69e9..00000000 --- a/Back-End/Dockers/login_db/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM postgres:latest - -EXPOSE 5432 - -#COPY ./init.sql /docker-entrypoint-initdb.d/ diff --git a/Back-End/Dockers/loginservice/Dockerfile b/Back-End/Dockers/loginservice/Dockerfile deleted file mode 100644 index 436764f5..00000000 --- a/Back-End/Dockers/loginservice/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.14-rc-bookworm - -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev - -COPY ./login /login -COPY ./requirements.txt /login/requirements.txt -WORKDIR /login - -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -EXPOSE 8000 - -COPY ./entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/entrypoint.sh b/Back-End/Dockers/loginservice/entrypoint.sh deleted file mode 100644 index b38113b5..00000000 --- a/Back-End/Dockers/loginservice/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -cd /login - -# Apply datab -python3 manage.py makemigrations -python3 manage.py makemigrations my_login -python3 manage.py migrate - -# Stars -python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/db.sqlite3 b/Back-End/Dockers/loginservice/login/db.sqlite3 deleted file mode 100644 index 949865a9..00000000 Binary files a/Back-End/Dockers/loginservice/login/db.sqlite3 and /dev/null differ diff --git a/Back-End/Dockers/loginservice/login/login/asgi.py b/Back-End/Dockers/loginservice/login/login/asgi.py deleted file mode 100644 index a02382f5..00000000 --- a/Back-End/Dockers/loginservice/login/login/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for login project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'login.settings') - -application = get_asgi_application() diff --git a/Back-End/Dockers/loginservice/login/login/settings.py b/Back-End/Dockers/loginservice/login/login/settings.py deleted file mode 100644 index df31c9e3..00000000 --- a/Back-End/Dockers/loginservice/login/login/settings.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Django settings for login project. - -Generated by 'django-admin startproject' using Django 4.2.16. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" - -from pathlib import Path -import os -import secrets - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-kp7qs)0l1ie$%muo93+829po%pe9*gz8z8ah6dy0)cskj-5l*c') -#'django-insecure-kp7qs)0l1ie$%muo93+829po%pe9*gz8z8ah6dy0)cskj-5l*c' -API_KEY = os.getenv('API_KEY', '123') -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG', True) - -Microservices = { - 'Login': os.getenv('LOGIN_SERVICE', 'http://localhost:8000'), - 'Chat': os.getenv('CHAT_SERVICE', 'http://localhost:8001'), - 'Users': os.getenv('USERS_SERVICE', 'http://localhost:8002'), - 'Notifications': os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003'), -} - -ALLOWED_HOSTS = [ - '*', - 'chat_service', - 'login_service', - 'users_service', - 'notifications_service', - 'localhost', - '127.0.0.1' -] -CORS_ALLOW_ALL_ORIGINS = True -CORS_ALLOW_CREDENTIALS = True - -CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost:8000', - 'http://localhost:8001', - 'http://localhost:8002', - 'http://localhost:8003', - Microservices['Chat'], - Microservices['Login'], - Microservices['Users'], - Microservices['Notifications'], -] - -CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', -] - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'my_login', - 'corsheaders', - 'oauth2_provider', -] - -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'login.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'login.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'login'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD' : os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5432'), - } -} - -#user model -AUTH_USER_MODEL = 'my_login.AppUser' - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), -} - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -OAUTH2_PROVIDER = { - 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, - 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, - 'REFRESH_TOKEN_EXPIRE_SECONDS': 36000, - 'ROTATE_REFRESH_TOKENS': True, - 'SCOPES': { - 'read': 'Read scope', - 'write': 'Write scope', - 'groups': 'Access to your groups', - }, - # 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', - 'OAUTH2_VALIDATOR_CLASS': 'my_login.validations.CustomOAuth2Validator', -} -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True -client = { - 'CLIENT_ID' : '', - 'CLIENT_SECRET' : '', -} - -client['CLIENT_ID'] = os.getenv('CLIENT_ID', '2') -client['CLIENT_SECRET'] = os.getenv('CLIENT_SECRET', '123') - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = '/home/lollo/Documents/challenge_fides/Back-End/login/staticfiles' -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -SERVICE_PASSWORD = os.getenv('SERVICE_PASSWORD','123') # this is the password that the service will use to authenticate itself to the OAuth2 server - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - }, - }, - 'root': { - 'handlers': ['console'], - 'level': 'DEBUG', - }, -} - -# LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'handlers': { -# 'loki': { -# 'class': 'logging.StreamHandler', -# 'formatter': 'detailed', -# 'stream': 'ext://sys.stdout', # Sends logs to stdout for Loki -# }, -# }, -# 'loggers': { -# 'django': { -# 'handlers': ['loki'], -# 'level': 'INFO', -# 'propagate': True, -# }, -# }, -# 'formatters': { -# 'detailed': { -# 'format': '{levelname} {asctime} {module} {message}', -# 'style': '{', -# }, -# }, -# } diff --git a/Back-End/Dockers/loginservice/login/login/urls.py b/Back-End/Dockers/loginservice/login/login/urls.py deleted file mode 100644 index 5e8b53c5..00000000 --- a/Back-End/Dockers/loginservice/login/login/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib import admin -from django.urls import path, include -from oauth2_provider import urls as oauth2_urls -from oauth2_provider import views as oauth2_views -from my_login.views import CustomIntrospect - -urlpatterns = [ - path('admin/', admin.site.urls), - path('login/', include('my_login.urls')), - path('o/', include(oauth2_urls)), -] - -oauth2_urls = [ - path('authorize/', oauth2_views.AuthorizationView.as_view(), name="authorize"), - path('token/', oauth2_views.TokenView.as_view(), name="token"), - path('revoke_token/', oauth2_views.RevokeTokenView.as_view(), name="revoke-token"), - path('introspect/', CustomIntrospect.as_view(), name="myintrospect"), - path('applications/', oauth2_views.ApplicationList.as_view(), name="list"), - path('applications/register/', oauth2_views.ApplicationRegistration.as_view(), name="register"), -] diff --git a/Back-End/Dockers/loginservice/login/login/wsgi.py b/Back-End/Dockers/loginservice/login/login/wsgi.py deleted file mode 100644 index 6683f8fc..00000000 --- a/Back-End/Dockers/loginservice/login/login/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for login project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'login.settings') - -application = get_wsgi_application() diff --git a/Back-End/Dockers/loginservice/login/manage.py b/Back-End/Dockers/loginservice/login/manage.py deleted file mode 100755 index 8a1d90e9..00000000 --- a/Back-End/Dockers/loginservice/login/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'login.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/Back-End/Dockers/loginservice/login/my_login/__init__.py b/Back-End/Dockers/loginservice/login/my_login/__init__.py deleted file mode 100644 index d0d81c4c..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'my_login.apps.MyLoginConfig' \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/admin.py b/Back-End/Dockers/loginservice/login/my_login/admin.py deleted file mode 100644 index 5a5bb39b..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin -from .models import AppUser - -@admin.register(AppUser) -class AppUserAdmin(admin.ModelAdmin): - list_display = ('email', 'username', 'is_staff', 'is_superuser', 'is_active') - search_fields = ('email', 'username') - -# Register your models here. \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/apps.py b/Back-End/Dockers/loginservice/login/my_login/apps.py deleted file mode 100644 index da7ac414..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig - - -class MyLoginConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'my_login' - - def ready(self): - from .signals import create_oauth2_application_and_superuser_signal - print('Created superuser and OAuth2 application') \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/errors.py b/Back-End/Dockers/loginservice/login/my_login/errors.py deleted file mode 100644 index 6741343d..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from rest_framework import status - -error_codes = { - 'an email is needed': 970, -} diff --git a/Back-End/Dockers/loginservice/login/my_login/middleware.py b/Back-End/Dockers/loginservice/login/my_login/middleware.py deleted file mode 100644 index 59e244fe..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/middleware.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.conf import settings -from rest_framework.permissions import BasePermission - -class APIKeyPermission(BasePermission): - def has_permission(self, request, view): - api_key = request.META.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False - - def has_permission(self, request, view): - api_key = request.META.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/models.py b/Back-End/Dockers/loginservice/login/my_login/models.py deleted file mode 100644 index 5c8aa131..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/models.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db import models -from django.contrib.auth.base_user import BaseUserManager -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin - -class AppUserManager(BaseUserManager): - def create_user(self, email, password=None): - if not email: - raise ValueError('An email is required.') - if not password: - raise ValueError('A password is required.') - email = self.normalize_email(email) - user = self.model(email=email) - user.set_password(password) - user.save() - return user - def create_superuser(self, email, password=None): - if not email: - raise ValueError('An email is required.') - if not password: - raise ValueError('A password is required.') - user = self.create_user(email, password) - user.is_superuser = True - user.is_staff = True - user.save() - return user - - -class AppUser(AbstractBaseUser, PermissionsMixin): - user_id = models.AutoField(primary_key=True) - email = models.EmailField(max_length=50, unique=True) - username = models.CharField(max_length=100) - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['username'] - is_staff = models.BooleanField(default=False) - objects = AppUserManager() - def __str__(self): - return self.username - - diff --git a/Back-End/Dockers/loginservice/login/my_login/serializers.py b/Back-End/Dockers/loginservice/login/my_login/serializers.py deleted file mode 100644 index a632b5a2..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/serializers.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.forms import ValidationError -from rest_framework import serializers -from django.contrib.auth import get_user_model, authenticate - -UserModel = get_user_model() - -class UserRegisterSerializer(serializers.ModelSerializer): - class Meta: - model = UserModel - fields = '__all__' - def create(self, clean_data): - user_obj = UserModel.objects.create_user(email=clean_data['email'], password=clean_data['password']) - user_obj.username = clean_data['username'] - user_obj.save() - return user_obj - -class UserLoginSerializer(serializers.Serializer): - email = serializers.EmailField() - password = serializers.CharField() - ## - def check_user(self, clean_data): - user = authenticate(username=clean_data['email'], password=clean_data['password']) - if not user: - raise ValidationError('user not found') - return user - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = UserModel - fields = ('email', 'username') diff --git a/Back-End/Dockers/loginservice/login/my_login/signals.py b/Back-End/Dockers/loginservice/login/my_login/signals.py deleted file mode 100644 index 6abfe821..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/signals.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -from django.db.models.signals import post_migrate -from django.dispatch import receiver -from django.contrib.auth import get_user_model -from oauth2_provider.models import Application -from login.settings import client - -User = get_user_model() - - -def create_oauth2_application_and_superuser(**kwargs): - if not User.objects.filter(email='pasquale@example.com').exists(): - User.objects.create_superuser(email='pasquale@example.com', password=os.getenv('SUPERUSER_PASSWORD', '123')) - - if Application.objects.filter(name = 'my_login').exists(): - return - - client_id = client['CLIENT_ID'] - client_secret = client['CLIENT_SECRET'] - application = Application.objects.create( - name='my_login', - client_id=client_id, - client_secret=client_secret, - client_type=Application.CLIENT_CONFIDENTIAL, - authorization_grant_type=Application.GRANT_PASSWORD, - redirect_uris='', - ) - # Add all permissions to the application - application.scope = '__all__' - application.save() - if application: - print('Created OAuth2 application') - - -from oauth2_provider.signals import app_authorized - -def handle_app_authorized(sender, request, token, **kwargs): - print(f'App {token.application.name=} was authorized') - -app_authorized.connect(handle_app_authorized) - -@receiver(post_migrate) -def create_oauth2_application_and_superuser_signal(sender, **kwargs): - create_oauth2_application_and_superuser() - print('Created superuser and OAuth2 application') \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/tests.py b/Back-End/Dockers/loginservice/login/my_login/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Back-End/Dockers/loginservice/login/my_login/urls.py b/Back-End/Dockers/loginservice/login/my_login/urls.py deleted file mode 100644 index 19be563f..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('register', views.UserRegister.as_view(), name='register'), - path('login', views.UserLogin.as_view(), name='login'), - path('logout', views.UserLogout.as_view(), name='logout'), - path('user', views.UserView.as_view(), name='user'), - path('get_csrf_token', views.get_csrf_token, name='get_csrf_token'), - path('register_service', views.ServiceRegister.as_view(), name='Serviceregister'), - path('health/', views.health_check, name='health'), -] diff --git a/Back-End/Dockers/loginservice/login/my_login/validations.py b/Back-End/Dockers/loginservice/login/my_login/validations.py deleted file mode 100644 index da1af4dc..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/validations.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.core.exceptions import ValidationError -from django.contrib.auth import get_user_model -UserModel = get_user_model() - -def custom_validation(data): - email = data['email'].strip() - username = data['username'].strip() - password = data['password'].strip() - ## - if not email: - raise ValidationError('an email is needed') - if UserModel.objects.filter(email=email).exists(): - raise ValidationError('email already in use') - ## - if not password or len(password) < 8: - raise ValidationError('weak password') - ## - if not username or UserModel.objects.filter(username=username).exists(): - raise ValidationError('username already in use') - return data - - -def validate_email(data): - email = data['email'].strip() - if not email: - raise ValidationError('an email is needed') - return True - -def validate_username(data): - username = data['username'].strip() - if not username: - raise ValidationError('choose another username') - return True - -def validate_password(data): - password = data['password'].strip() - if not password: - raise ValidationError('a password is needed') - return True - - -from oauth2_provider.oauth2_validators import OAuth2Validator -from oauth2_provider.models import AccessToken - -class CustomOAuth2Validator(OAuth2Validator): - def save_bearer_token(self, token, request, *args, **kwargs): - super().save_bearer_token(token, request, *args, **kwargs) - if request.user: - token['user_id'] = request.user.pk - token['username'] = request.user.username \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/login/my_login/views.py b/Back-End/Dockers/loginservice/login/my_login/views.py deleted file mode 100644 index efdefa8b..00000000 --- a/Back-End/Dockers/loginservice/login/my_login/views.py +++ /dev/null @@ -1,257 +0,0 @@ -from urllib.parse import urlencode -from django.contrib.auth import get_user_model, login, logout -from django.utils.decorators import method_decorator -from django.utils.decorators import method_decorator -import requests -from rest_framework.views import APIView -from rest_framework.response import Response -from .serializers import UserRegisterSerializer, UserLoginSerializer, UserSerializer -from rest_framework import permissions, status -from .validations import custom_validation, validate_email, validate_password -from oauth2_provider.contrib.rest_framework import OAuth2Authentication , TokenHasScope, TokenHasReadWriteScope -from .errors import error_codes -from oauth2_provider.models import AccessToken , Application, RefreshToken -from oauthlib.common import generate_token -from django.http import HttpRequest, JsonResponse -from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt -from django.middleware.csrf import get_token -from oauth2_provider.views import TokenView -from django.contrib.auth import login -from login.settings import client -from login.settings import SERVICE_PASSWORD -from oauth2_provider.models import AccessToken -from django.utils.decorators import method_decorator -from .models import AppUser -from login.settings import Microservices -from django.utils import timezone -from datetime import timedelta -from django.conf import settings - -def get_access_token(): - app = Application.objects.get(name='my_login') - - token = AccessToken.objects.create( - user=app.user, - token=generate_token(), - application=app, - scope='read write', - expires=timezone.now() + timedelta(seconds=36000), - ) - return token.token - -def CreateOnOtherServices(user): - Chat_url = Microservices['Chat'] + "/chat/new_user/" - Notification_url = Microservices['Notifications'] + "/notification/add_user" - User_url = Microservices['Users'] + "/user/user" - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {get_access_token()}', - 'X-API-KEY': settings.API_KEY, - } - - user_data = { - 'user_id': user.user_id, - 'username': user.username, - 'email': user.email, - } - user_response = requests.post(User_url, json=user_data, headers=headers) - if user_response.status_code != 201: - print(user_response.json()) - raise ValueError('User service failed to create user') - chat_response = requests.post(Chat_url, json=user_data, headers=headers) - if chat_response.status_code != 201: - raise ValueError('Chat service failed to create user') -# notification_response = requests.post(Notification_url, json=user_data, headers=headers) -# if notification_response.status_code != 201: -# raise ValueError('Notification service failed to create user') - - - - -class UserRegister(APIView): - permission_classes = (permissions.AllowAny,) - def post(self, request): - try: - clean_data = custom_validation(request.data) - serializer = UserRegisterSerializer(data=clean_data) - if serializer.is_valid(raise_exception=True): - user = serializer.create(clean_data) - if user: - serializer.data['password'] = user.password - CreateOnOtherServices(user) - return Response({'user': serializer.data, 'user_id': user.user_id}, status=status.HTTP_201_CREATED) - except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) - - - -@method_decorator(csrf_exempt, name='dispatch') -class UserLogin(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (OAuth2Authentication,) - def post(self, request): - try: - - data = request.data - - - # Validate email and password - if not validate_email(data): - return Response({'error': 'Invalid email'}, status=status.HTTP_400_BAD_REQUEST) - if not validate_password(data): - return Response({'error': 'Invalid password'}, status=status.HTTP_400_BAD_REQUEST) - - # Serialize and validate user data - serializer = UserLoginSerializer(data=data) - serializer.is_valid(raise_exception=True) - user = serializer.check_user(data) - login(request, user) - - # Prepare data for tocken request - - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': f'Basic {client["CLIENT_ID"]}:{client["CLIENT_SECRET"]}', - 'Accept': 'application/json', - 'X-CSRFToken': get_token(request), - } - - token_data = { - 'grant_type': 'password', - 'username': data.get('email'), - 'password': data.get('password'), - 'client_id': client['CLIENT_ID'], - 'client_secret': client['CLIENT_SECRET'], - 'scope': 'read write', - } - - # Make token request - - token_view = TokenView.as_view() - token_request = HttpRequest() - token_request.method = 'POST' - token_request.POST = token_data - token_request.META['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' - token_request.META['HTTP_AUTHORIZATION'] = f'Basic {client["CLIENT_ID"]}:{client["CLIENT_SECRET"]}' - token_request.META['HTTP_X_CSRFTOKEN'] = get_token(request) - - token_response = token_view(token_request) - - - return token_response - except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) - -@method_decorator(csrf_exempt, name='dispatch') -class ServiceRegister(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (OAuth2Authentication,) - ## - def post(self, request): - try: - data = request.data - service_name = data.get('name') - service_password = data.get('service_password') - if service_password == SERVICE_PASSWORD: - # Create an app - if not Application.objects.filter(name=service_name).exists(): - app = Application.objects.create( - name=service_name, - client_type=Application.CLIENT_CONFIDENTIAL, - client_id=data.get('client_id'), - client_secret=data.get('client_secret'), - authorization_grant_type=Application.GRANT_PASSWORD, - redirect_uris='http://localhost:8000', - ) - app.scope = '__all__' - app.save() - return Response({'message': 'Service created successfully', 'client_id': app.client_id, 'client_secret': app.client_secret}, - status=status.HTTP_201_CREATED) - - ## Login the app as a staff user - # if not AppUser.objects.filter(username=service_name).exists(): - # app_user = AppUser.objects.create(username=service_name, is_staff=True) - # app_user.set_password(data.get('client_secret')) - # app_user.save() - ## Get an access token - # Tokenview = TokenView.as_view() - # token_request = HttpRequest() - # token_request.method = 'POST' - # token_request.POST = { - # 'grant_type': 'client_credentials', - # 'client_id': app.client_id, - # 'client_secret': app.client_secret, - # } - # token_request.META['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' - # token_request.META['HTTP_AUTHORIZATION'] = f'Basic {app.client_id}:{app.client_secret}' - # token_request.META['HTTP_X_CSRFTOKEN'] = data.get('csrf_token') - # token_response = Tokenview(token_request) - # ## Return the client_id and client_secret and token - # token_response.data['client_id'] = app.client_id - # token_response.data['client_secret'] = app.client_secret - # return token_response - else: - return Response({'error': 'Service already exists'}, status=status.HTTP_400_BAD_REQUEST) - else: - return Response({'error': 'Invalid service password'}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) - - - -class UserLogout(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = () - def post(self, request): - try: - access_token = AccessToken.objects.get(token=request.auth.token) - RefreshToken.objects.get(access_token=access_token).delete() - access_token.delete() - logout(request) - return Response(status=status.HTTP_200_OK) - except AccessToken.DoesNotExist: - return Response({'error': 'invalid token'}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) - - -class UserView(APIView): - permission_classes = (permissions.IsAuthenticated, TokenHasScope , TokenHasReadWriteScope) - authentication_classes = (OAuth2Authentication,) - ## - required_scopes = ['read'] - def get(self, request): - serializer = UserSerializer(request.user) - user = AppUser.objects.get(username=serializer.data['username']) - return Response({'user': serializer.data, 'user_id': user.user_id}, status=status.HTTP_200_OK) - -@ensure_csrf_cookie -def get_csrf_token(request): - return JsonResponse({'detail': 'CSRF cookie set'}) - -from oauth2_provider.views import IntrospectTokenView -from oauth2_provider.models import AccessToken - -class CustomIntrospect(IntrospectTokenView): - permissions = (permissions.AllowAny,) - def get_token_response(self, token): - try: - token = AccessToken.objects.get(token=token) - if token: - return { - 'active': True, - 'scope': token.scope, - 'user_id': token.user.user_id, - 'username': token.user.username, - 'exp': token.expires.timestamp(), - } - except AccessToken.DoesNotExist: - return {'active': False} - -@csrf_exempt -def health_check(request): - return JsonResponse({'status': 'Ok'}, status=200) \ No newline at end of file diff --git a/Back-End/Dockers/loginservice/requirements.txt b/Back-End/Dockers/loginservice/requirements.txt deleted file mode 100644 index 97a960cc..00000000 --- a/Back-End/Dockers/loginservice/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -asgiref==3.8.1 -certifi==2024.8.30 -cffi==1.17.1 -charset-normalizer==3.4.0 -cryptography==44.0.0 -Django==5.1.3 -django-cors-headers==4.6.0 -django-oauth-toolkit==3.0.1 -djangorestframework==3.15.2 -idna==3.10 -jwcrypto==1.5.6 -oauthlib==3.2.2 -psycopg==3.2.3 -pycparser==2.22 -requests==2.32.3 -sqlparse==0.5.2 -typing_extensions==4.12.2 -urllib3==2.2.3 diff --git a/Back-End/Dockers/loki/Dockerfile b/Back-End/Dockers/loki/Dockerfile deleted file mode 100644 index f3840520..00000000 --- a/Back-End/Dockers/loki/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM alpine:latest - -# Set the Loki version to download -ARG LOKI_VERSION=2.9.2 - -# Install necessary tools -RUN apk add --no-cache curl unzip - -# Download and extract the prebuilt Loki binary -ADD https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/loki-linux-amd64.zip /tmp/loki.zip -RUN unzip /tmp/loki.zip -d /usr/bin/ && rm /tmp/loki.zip && chmod +x /usr/bin/loki-linux-amd64 && mv /usr/bin/loki-linux-amd64 /usr/bin/loki - -# Copy the Loki configuration file -COPY ./loki-config.yaml /etc/loki/local-config.yaml - -# Set up the environment -RUN addgroup -g 10001 -S loki && \ - adduser -u 10001 -S loki -G loki && \ - mkdir -p /tmp/loki/compactor && \ - mkdir -p /tmp/loki/tsdb-index && \ - mkdir -p /tmp/loki/tsdb-cache && \ - mkdir -p /tmp/loki/chunks && \ - mkdir -p /var/lib/loki/compactor && \ - chown -R loki:loki /tmp/loki && \ - chown -R loki:loki /etc/loki && \ - chown -R loki:loki /var/lib/loki - -USER 10001 -EXPOSE 3100 -ENTRYPOINT [ "/usr/bin/loki" ] -CMD ["-config.file=/etc/loki/local-config.yaml"] \ No newline at end of file diff --git a/Back-End/Dockers/loki/loki-config.yaml b/Back-End/Dockers/loki/loki-config.yaml deleted file mode 100644 index 75223890..00000000 --- a/Back-End/Dockers/loki/loki-config.yaml +++ /dev/null @@ -1,48 +0,0 @@ -auth_enabled: false - -server: - http_listen_port: 3100 - -common: - path_prefix: /var/lib/loki - -ingester: - lifecycler: - ring: - kvstore: - store: inmemory - replication_factor: 1 - chunk_idle_period: 5m - chunk_retain_period: 30s - -schema_config: - configs: - - from: "2023-01-05" - index: - period: 24h - prefix: index_ - object_store: filesystem - schema: v13 - store: tsdb - -storage_config: - tsdb_shipper: - active_index_directory: /tmp/loki/tsdb-index - cache_location: /tmp/loki/tsdb-cache - -query_scheduler: - # the TSDB index dispatches many more, but each individually smaller, requests. - # We increase the pending request queue sizes to compensate. - max_outstanding_requests_per_tenant: 32768 - -querier: - max_concurrent: 16 - -limits_config: - reject_old_samples: true - reject_old_samples_max_age: 168h - allow_structured_metadata: false - -table_manager: - retention_deletes_enabled: true - retention_period: 168h \ No newline at end of file diff --git a/Back-End/Dockers/m2k_collect/images/srcs-adminer-latest-19a0f9f063d5ba15.yaml b/Back-End/Dockers/m2k_collect/images/srcs-adminer-latest-19a0f9f063d5ba15.yaml deleted file mode 100644 index 0f6ba122..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-adminer-latest-19a0f9f063d5ba15.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-adminer:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-ftp-latest-b5941740437d9998.yaml b/Back-End/Dockers/m2k_collect/images/srcs-ftp-latest-b5941740437d9998.yaml deleted file mode 100644 index b5672670..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-ftp-latest-b5941740437d9998.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-ftp:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-mariadb-latest-6d4ea30b4546b033.yaml b/Back-End/Dockers/m2k_collect/images/srcs-mariadb-latest-6d4ea30b4546b033.yaml deleted file mode 100644 index 0de499ad..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-mariadb-latest-6d4ea30b4546b033.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-mariadb:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-netdata-latest-651efc8a48959625.yaml b/Back-End/Dockers/m2k_collect/images/srcs-netdata-latest-651efc8a48959625.yaml deleted file mode 100644 index 6317df0e..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-netdata-latest-651efc8a48959625.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-netdata:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-nginx-latest-d5232657003de412.yaml b/Back-End/Dockers/m2k_collect/images/srcs-nginx-latest-d5232657003de412.yaml deleted file mode 100644 index 52e8785b..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-nginx-latest-d5232657003de412.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-nginx:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-redis-latest-63042b04c7a34936.yaml b/Back-End/Dockers/m2k_collect/images/srcs-redis-latest-63042b04c7a34936.yaml deleted file mode 100644 index 24e36d5f..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-redis-latest-63042b04c7a34936.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-redis:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-static-latest-57b971f13ce7d34b.yaml b/Back-End/Dockers/m2k_collect/images/srcs-static-latest-57b971f13ce7d34b.yaml deleted file mode 100644 index 7051b558..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-static-latest-57b971f13ce7d34b.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-static:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/m2k_collect/images/srcs-wordpress-latest-8a7dd76b7ab884ef.yaml b/Back-End/Dockers/m2k_collect/images/srcs-wordpress-latest-8a7dd76b7ab884ef.yaml deleted file mode 100644 index 99b0993c..00000000 --- a/Back-End/Dockers/m2k_collect/images/srcs-wordpress-latest-8a7dd76b7ab884ef.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: move2kube.konveyor.io/v1alpha1 -kind: ImageMetadata -spec: - tags: - - srcs-wordpress:latest - ports: [] - accessedDirs: - - "" - userID: -1 diff --git a/Back-End/Dockers/notification_db/Dockerfile b/Back-End/Dockers/notification_db/Dockerfile deleted file mode 100644 index 09311ad3..00000000 --- a/Back-End/Dockers/notification_db/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM postgres:latest - -ENV POSTGRES_USER=pasquale -ENV POSTGRES_DB=Notification_db -ENV POSTGRES_PASSWORD=123 - -EXPOSE 5432 - -COPY ./init.sql /docker-entrypoint-initdb.d/ diff --git a/Back-End/Dockers/notification_db/init.sql b/Back-End/Dockers/notification_db/init.sql deleted file mode 100644 index af4d7601..00000000 --- a/Back-End/Dockers/notification_db/init.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Create a test table -CREATE TABLE test_table ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL -); - --- Insert some test data -INSERT INTO test_table (name) VALUES ('Alice'), ('Bob'), ('Charlie'); - --- Query the test data -SELECT * FROM test_table; \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Dockerfile b/Back-End/Dockers/notification_service/Dockerfile deleted file mode 100644 index db0ab38a..00000000 --- a/Back-End/Dockers/notification_service/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.14-rc-bookworm - -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev - -COPY ./Notifications /Notifications -COPY ./requirements.txt /Notifications/requirements.txt -WORKDIR /Notifications - -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -EXPOSE 8000 - -COPY ./entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/asgi.py b/Back-End/Dockers/notification_service/Notifications/Notifications/asgi.py deleted file mode 100644 index 6dff0527..00000000 --- a/Back-End/Dockers/notification_service/Notifications/Notifications/asgi.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -ASGI config for Notifications project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ -""" - -import os -from channels.routing import ProtocolTypeRouter, URLRouter -from django.core.asgi import get_asgi_application -from my_notifications.routing import websocket_urlpatterns -from my_notifications.middleware import TokenAuthMiddlewareStack - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Notifications.settings') - -application = ProtocolTypeRouter({ - 'http': get_asgi_application(), - 'websocket': TokenAuthMiddlewareStack( - URLRouter(websocket_urlpatterns) - ), -}) diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/celery.py b/Back-End/Dockers/notification_service/Notifications/Notifications/celery.py deleted file mode 100644 index e986ca79..00000000 --- a/Back-End/Dockers/notification_service/Notifications/Notifications/celery.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import absolute_import, unicode_literals -import os -from celery import Celery - -# set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') - -app = Celery('notifications') - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -app.config_from_object('django.conf:settings', namespace='CELERY') - -app.autodiscover_tasks(['my_notifications']) \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/settings.py b/Back-End/Dockers/notification_service/Notifications/Notifications/settings.py deleted file mode 100644 index a1650154..00000000 --- a/Back-End/Dockers/notification_service/Notifications/Notifications/settings.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -Django settings for Notifications project. - -Generated by 'django-admin startproject' using Django 5.1.1. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path -import secrets , os -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-u+0oqxb+f!fa)0f_m6ezqtb*m-86^vxt$9rn%-_nbxc3@qo_@0' -API_KEY = os.getenv('API_KEY', '123') - -def arise(exception): - raise(exception) - - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG', True) - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'oauth2_provider', - 'corsheaders', - 'channels', - 'celery', - 'django_redis', - 'django_filters', - 'my_notifications', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'my_notifications.middleware.TokenAuthMiddlewareHTTP', - 'my_notifications.middleware.APIKeyAuthMiddleware', -] - -REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] -} - -ROOT_URLCONF = 'Notifications.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'Notifications.wsgi.application' -ASGI_APPLICATION = 'Notifications.asgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'notifications'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5432'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - -oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password -} - -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'my_notifications.middleware.TokenAuthPermission', - 'my_notifications.middleware.APIKeyAuthPermission', - ], -} - - -CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://172.18.0.1:6701/1', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } -} - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [('172.18.0.1', 6701)], - }, - } -} - -CELERY_BROKER_URL = 'redis://localhost:6701/0' -CELERY_RESULT_BACKEND = 'redis://localhost:6701/0' -CELERY_ACCEPT_CONTENT = ['json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -CELERY_TIMEZONE = 'UTC' - - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# Microservices -Microservices = { - 'Login': os.getenv('loginservice', 'http://localhost:8000'), - 'Chat': os.getenv('chatservice', 'http://localhost:8001'), - 'Users': os.getenv('userService', 'http://localhost:8002'), - 'Notifications': os.getenv('notificationService', 'http://localhost:8003'), - 'Personal' : "Self", -} - -Types = { - 'IM' : 'Immediate', - 'QU' : 'Queued', - 'SC' : 'Scheduled', - 'SE' : 'Sent', -} \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/urls.py b/Back-End/Dockers/notification_service/Notifications/Notifications/urls.py deleted file mode 100644 index d3a675fb..00000000 --- a/Back-End/Dockers/notification_service/Notifications/Notifications/urls.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -URL configuration for Notifications project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path - -urlpatterns = [ - path('admin/', admin.site.urls), -] diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/wsgi.py b/Back-End/Dockers/notification_service/Notifications/Notifications/wsgi.py deleted file mode 100644 index 26aae145..00000000 --- a/Back-End/Dockers/notification_service/Notifications/Notifications/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for Notifications project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Notifications.settings') - -application = get_wsgi_application() diff --git a/Back-End/Dockers/notification_service/Notifications/manage.py b/Back-End/Dockers/notification_service/Notifications/manage.py deleted file mode 100755 index 891a7d94..00000000 --- a/Back-End/Dockers/notification_service/Notifications/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Notifications.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/admin.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/consumers.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/consumers.py deleted file mode 100644 index 68e537bd..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/consumers.py +++ /dev/null @@ -1,105 +0,0 @@ -from channels.layers import get_channel_layer -from channels.generic.websocket import AsyncWebsocketConsumer -import json - -class UserNotificationConsumer(AsyncWebsocketConsumer): - async def connect(self): - self.user = self.scope['user'] - self.group_name = f'user_notifications_{self.user.id}' - - await self.channel_layer.group_add( - self.group_name, - self.channel_name - ) - - await self.accept() - from .models import UserProfile - UserProfile.objects.filter(user_id=self.user.id).update(is_online=True) - - from .models import QueuedNotification - - queued_notifications = QueuedNotification.objects.filter(user_id=self.user.id, is_sent=False) - for notification in queued_notifications: - await self.send(text_data=json.dumps(notification.message)) - notification.is_sent = True - notification.save() # after the save the notifications are moved elsewhere - - async def disconnect(self, close_code): - from .models import UserProfile - UserProfile.objects.filter(user_id=self.user.id).update(is_online=False) - - await self.channel_layer.group_discard( - self.group_name, - self.channel_name - ) - - async def receive(self, text_data): - pass - - async def send_notification(self, event): - await self.send(text_data=json.dumps(event['text'])) - - async def check_online(self, event): - if await self.send(text_data=json.dumps({'is_online': True})): - return True - else: - return False - -class GroupNotificationConsumer(AsyncWebsocketConsumer): - async def connect(self): - self.group_id = self.scope['url_route']['kwargs']['group_id'] - self.group_name = f'group_notifications_{self.group_id}' - - await self.channel_layer.group_add( - self.group_name, - self.channel_name - ) - - await self.accept() - - async def disconnect(self, close_code): - await self.channel_layer.group_discard( - self.group_name, - self.channel_name - ) - - async def receive(self, text_data): - pass - - async def send_notification(self, event): - - message = event['text'] - - await self.channel_layer.group_send( - self.group_name, - { - 'type': 'send_notification', - 'text': message - } - ) - from .models import NotificationsGroup - group = NotificationsGroup.objects.get(id=self.group_id) - users_in_group = group.users.all() - - - for user in users_in_group: - user_group_name = f'notifications_{user.user_id}' - - # Check if the user is online - online = await self.check_user_online(user_group_name) - - if not online: - from .models import QueuedNotification - - QueuedNotification.objects.create( - user_id=user.user_id, - group_id=None, - message=message, - is_read=False - ) - - async def check_online(self, event): - await self.send(text_data=json.dumps({'is_online': True})) - - - \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/middleware.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/middleware.py deleted file mode 100644 index 84c52449..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/middleware.py +++ /dev/null @@ -1,198 +0,0 @@ -from urllib.parse import parse_qs -from channels.middleware import BaseMiddleware -from channels.db import database_sync_to_async -from django.contrib.auth.models import AnonymousUser -from channels.auth import AuthMiddlewareStack -from django.core.cache import cache -from django.contrib.auth import get_user_model -import requests , base64 -from .models import UserProfile -from django.utils.deprecation import MiddlewareMixin -from Notifications.settings import oauth2_settings - -@database_sync_to_async -def get_user_from_token(token, scope): - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - User = UserProfile.objects.get(email=email) - try: - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except e: - return AnonymousUser() - else: - return AnonymousUser() - except requests.RequestException as e: - return AnonymousUser() - - return AnonymousUser() - -class TokenAuthMiddleware(BaseMiddleware): - async def __call__(self, scope, receive, send): - query_string = parse_qs(scope["query_string"].decode()) - - try: - token = query_string.get("token") - if token: - scope["user"] = await get_user_from_token(token[0], scope) - else: - scope["user"] = AnonymousUser() - except Exception as e: - scope["user"] = AnonymousUser() - return await super().__call__(scope, receive, send) - -def TokenAuthMiddlewareStack(inner): - return TokenAuthMiddleware(AuthMiddlewareStack(inner)) - -class TokenAuthMiddlewareHTTP(MiddlewareMixin): - def process_request(self, request): - token = request.META.get('HTTP_AUTHORIZATION') - if token: - token = token.replace("Bearer ", "") - user = self.get_user_from_token_sync(token) - request.user = user - else: - request.user = AnonymousUser() - - def get_user_from_token_sync(self, token): - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - User = UserProfile.objects.get(email=email) - try: - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except e: - return AnonymousUser() - else: - print('Token introspection failed:' f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return AnonymousUser() - - -from rest_framework.permissions import BasePermission -from django.contrib.auth.models import AnonymousUser -from django.core.cache import cache -from Notifications.settings import oauth2_settings -import requests -import base64 -from .models import UserProfile - -class TokenAuthPermission(BasePermission): - def get_user_from_token(self, token): - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers, data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - user = UserProfile.objects.get(email=email) - try: - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, user, timeout=300) - return user - except UserProfile.DoesNotExist: - return AnonymousUser() - else: - print('Token introspection failed:', f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return AnonymousUser() - - return AnonymousUser() - - def has_permission(self, request, view): - token = request.META.get('HTTP_AUTHORIZATION') - if token: - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - if user and not isinstance(user, AnonymousUser): - request.user = user - return True - return False - -from django.conf import settings - -class APIKeyPermission(BasePermission): - def has_permission(self, request, view): - api_key = request.META.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False - - def has_permission(self, request, view): - api_key = request.META.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/models.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/models.py deleted file mode 100644 index 7b381e77..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/models.py +++ /dev/null @@ -1,101 +0,0 @@ -from datetime import timezone -from django.db import models -from Notifications.settings import Microservices , Types -from .consumers import UserNotificationConsumer - -class BaseNotification(models.Model): - id = models.AutoField(primary_key=True) - Sender = models.CharField(max_length=20, choices=Microservices) - message = models.TextField() - is_sent = models.BooleanField(default=False) - class Meta: - abstract = True - -class UserNotification(BaseNotification): - user_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class GroupNotification(BaseNotification): - group_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class SentNotification(UserNotification, GroupNotification): - sent_time = models.DateTimeField(auto_now_add=True) - -class ImmediateNotification(UserNotification, GroupNotification): - pass - -class QueuedNotification(UserNotification, GroupNotification): - def save(self, *args, **kwargs): - if self.is_sent is True: - SentNotification.objects.create( - user_id=self.user_id, - group_id=self.group_id, - message=self.message, - is_read=self.is_read, - creation_time=self.creation_time - ) - self.delete() - else: - super().save(*args, **kwargs) - - -class ScheduledNotification(UserNotification, GroupNotification): - send_time = models.DateTimeField() - - def on_create(self): - if self.send_time <= timezone.now(): - ImmediateNotification.objects.create( - user_id=self.user_id, - group_id=self.group_id, - message=self.message, - is_read=self.is_read, - creation_time=self.creation_time - ) - self.delete() - else: - from .tasks import send_scheduled_notifications - send_scheduled_notifications(self.id).apply_async(eta=self.send_time) - -class UserProfile(models.Model): - user_id = models.IntegerField(primary_key=True) - email = models.EmailField() - is_staff = models.BooleanField(default=False) - is_online = models.BooleanField(default=False, null=True) - - def get_is_online(self): - return UserNotificationConsumer.check_online(self.user_id) - - -class NotificationsGroup(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - Type = models.CharField(max_length=20, choices=Microservices) - members = models.ManyToManyField(UserProfile, related_name='Groups') - - # def send(self): - # if (send_notification(self.user_id, self.group_id, self.message)): - # SentNotification.objects.create( - # user_id=self.user_id, - # group_id=self.group_id, - - # message=self.message, - # is_read=self.is_read, - # creation_time=self.creation_time - # ) - # self.delete() - # return True - # else: - # QueuedNotification.objects.create( - # user_id=self.user_id, - # group_id=self.group_id, - # message=self.message, - # is_read=self.is_read, - # creation_time=self.creation_time - # ) - # self.delete() - # return False \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/routing.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/routing.py deleted file mode 100644 index 4d6d1667..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/routing.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path -from .consumers import UserNotificationConsumer, GroupNotificationConsumer - -websocket_urlpatterns = [ - path('ws/user_notifications/', UserNotificationConsumer.as_asgi()), - path('ws/group_notifications//', GroupNotificationConsumer.as_asgi()), -] \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/serializers.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/serializers.py deleted file mode 100644 index a3f5ab4c..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/serializers.py +++ /dev/null @@ -1,59 +0,0 @@ -from rest_framework import serializers -from .models import ImmediateNotification , QueuedNotification , ScheduledNotification , UserProfile , NotificationsGroup , BaseNotification - -class ImmediateNotificationSerializer(serializers.ModelSerializer): - class Meta: - model = ImmediateNotification - fields = '__all__' - -class QueuedNotificationSerializer(serializers.ModelSerializer): - class Meta: - model = QueuedNotification - fields = '__all__' - -class ScheduledNotificationSerializer(serializers.ModelSerializer): - class Meta: - model = ScheduledNotification - fields = '__all__' - -class UserProfileSerializer(serializers.ModelSerializer): - class Meta: - model = UserProfile - fields = '__all__' - -class NotificationsGroupSerializer(serializers.ModelSerializer): - class Meta: - model = NotificationsGroup - fields = '__all__' - -class UniversalNotificationSerializer(serializers.ModelSerializer): - notification = serializers.SerializerMethodField() - - class Meta: - model = BaseNotification - fields = ['id', 'Type', 'Sender', 'message', 'notification'] - - def get_notification(self, obj): - if isinstance(obj, ImmediateNotification): - return ImmediateNotificationSerializer(obj).data - elif isinstance(obj, QueuedNotification): - return QueuedNotificationSerializer(obj).data - elif isinstance(obj, ScheduledNotification): - return ScheduledNotificationSerializer(obj).data - return None - - -# class UniversalNotificationSerializer(serializers.ModelSerializer): -# notification = serializers.SerializerMethodField() - -# class Meta: -# model = BaseNotification -# fields = '__all__' - -# def get_notification(self, obj): -# if obj.Type == 'IM': -# return ImmediateNotificationSerializer(obj.immediatenotification).data -# elif obj.Type == 'QU': -# return QueuedNotificationSerializer(obj.queuednotification).data -# elif obj.Type == 'SC': -# return ScheduledNotificationSerializer(obj.schedulednotification).data \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/signals.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/signals.py deleted file mode 100644 index 78b90105..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/signals.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from .models import ImmediateNotification, ScheduledNotification, UserProfile , NotificationsGroup -from .views import send_notification -import logging - - -@receiver(post_save, sender=ImmediateNotification) -def send_notification(sender, instance, created, **kwargs): - if created: - if (send_notification(instance.user_id, instance.group_id, instance)): - logger = logging.getLogger(__name__) - logger.info(f'Notification sent: {instance}') - else: - logger = logging.getLogger(__name__) - logger.error(f'Notification not sent: {instance}') - -@receiver(post_save, sender=ScheduledNotification) -def schedule_notification(sender, instance, created, **kwargs): - if created: - instance.on_create() - logger = logging.getLogger(__name__) - logger.info(f'Notification scheduled: {instance}') - -@receiver(post_save, sender=UserProfile) -def create_user_profile(sender, instance, created, **kwargs): - if created: - NotificationsGroup.objects.create( - name='Personal', - type='Personal', - members=[instance], - owner=instance - ) - logger = logging.getLogger(__name__) - logger.info(f'User profile created: {instance}') - -# @receiver(post_save, sender=NotificationsGroup) -# def create_group(sender, instance, created, **kwargs): -# if created: -# instance.on_create() -# logger = logging.getLogger(__name__) -# logger.info(f'Group created: {instance}') diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/tasks.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/tasks.py deleted file mode 100644 index 9e14e0c7..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/tasks.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -from celery import shared_task -from my_notifications.models import ScheduledNotification, SentNotification, QueuedNotification -from my_notifications.views import send_notification - -@shared_task -def send_scheduled_notifications(notification_id): - try: - notification = ScheduledNotification.objects.get(id=notification_id) - if (send_notification(notification.user_id, notification.group_id, notification.message)): - SentNotification.objects.create( - user_id=notification.user_id, - group_id=notification.group_id, - message=notification.message, - is_read=notification.is_read, - creation_time=notification.creation_time - ) - notification.delete() - else: - QueuedNotification.objects.create( - user_id=notification.user_id, - group_id=notification.group_id, - message=notification.message, - is_read=notification.is_read, - creation_time=notification.creation_time - ) - notification.delete() - except ScheduledNotification.DoesNotExist: - logger = logging.getLogger(__name__) - logger.error(f'Notification not found: {notification_id}') \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/tests.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/urls.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/urls.py deleted file mode 100644 index 9d7ac7aa..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from .views import AddUserToGroupView , NewUser , SentNotification , NewNotification - -urlpatterns = [ - path('groups//add_user/', AddUserToGroupView.as_view(), name='add_user_to_group'), - path('add_user', NewUser.as_view(), name='new_user'), - path('notification_history', SentNotification.as_view(), name='notification_history'), - path('new_notification', NewNotification.as_view(), name='new_notification') -] \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/views.py b/Back-End/Dockers/notification_service/Notifications/my_notifications/views.py deleted file mode 100644 index 0f42dafc..00000000 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/views.py +++ /dev/null @@ -1,108 +0,0 @@ -from django.shortcuts import render -from rest_framework import viewsets , generics -from rest_framework.response import Response -from asgiref.sync import async_to_sync -from rest_framework import permissions , status -from channels.layers import get_channel_layer -from .models import ImmediateNotification, QueuedNotification, ScheduledNotification, NotificationsGroup -from .serializers import UniversalNotificationSerializer, UserProfileSerializer , NotificationsGroupSerializer -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.views import APIView -from rest_framework.pagination import CursorPagination -from .middleware import TokenAuthMiddlewareHTTP, APIKeyPermission -class NotificationViewSet(viewsets.ViewSet): - def send_notification(self, user_id, group_id, notification): - if user_id is not None: - return self.send_user_notification(user_id, notification) - elif group_id is not None: - return self.send_group_notification(group_id, notification) - else: - return Response({'error': 'No user or group specified'}) - - def send_user_notification(user_id, notification): - channel_layer = get_channel_layer() - serialized_notification = UniversalNotificationSerializer(notification) - is_online = async_to_sync(channel_layer.group_send)( - f'notifications_{user_id}', - { - 'type': 'check_online', - 'text': 'Checking if user is online' - } - ) - if is_online: - sent = async_to_sync(channel_layer.group_send)( - f'notifications_{user_id}', - { - 'type': 'notification', - 'text': serialized_notification - } - ) - if sent is True: - SentNotification.objects.create( - is_sent=True, - UserNotification=serialized_notification - ) - notification.delete() - return sent - return sent - else: - QueuedNotification.objects.create( - is_sent=False, - UserNotification=serialized_notification - ) - return False - -class NewUser(generics.CreateAPIView): - permissionClasses = [APIKeyPermission] - serializer_class = UserProfileSerializer - fields = ['user_id', 'email', 'is_online'] - -class NewNotification(generics.CreateAPIView): - permission_classes = [APIKeyPermission] - serializers = UniversalNotificationSerializer - def get_queryset(self): - ImmediateNotifications = ImmediateNotification.objects.all() - QueuedNotifications = QueuedNotification.objects.all() - ScheduledNotifications = ScheduledNotification.objects.all() - return ImmediateNotifications.union(QueuedNotifications, ScheduledNotifications) - - -class CursorNotificationPagination(CursorPagination): - page_size = 10 - ordering = '-creation_time' - -class SentNotification(generics.ListAPIView): - from .models import SentNotification - # permissionClasses = (permissions.AllowAny,) - serializer_class = UniversalNotificationSerializer - filter_backends = [DjangoFilterBackend] - filterset_fields = ['user_id', 'group_id'] - queryset = SentNotification.objects.all() - -class GroupNotification(generics.ListCreateAPIView): - # permissionClasses = (permissions.AllowAny,) - serializer_class = NotificationsGroupSerializer - filter_backends = [DjangoFilterBackend] - filterset_fields = ['group_id'] - queryset = ImmediateNotification.objects.all() - -class AddUserToGroupView(APIView): - def post(self, request, group_id): - serializer = UserProfileSerializer(data=request.data) - if serializer.is_valid(): - user_id = serializer.data['user_id'] - try: - from .models import UserProfile - UserProfile.objects.get(user_id=user_id) - except UserProfile.DoesNotExist: - return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) - - try: - group = NotificationsGroup.objects.get(id=group_id) - group.users.add(user_id) - group.save() - return Response({'success': 'User added to group'}, status=status.HTTP_200_OK) - except NotificationsGroup.DoesNotExist: - return Response({'error': 'Group not found'}, status=status.HTTP_404_NOT_FOUND) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/entrypoint.sh b/Back-End/Dockers/notification_service/entrypoint.sh deleted file mode 100644 index 2a17f09a..00000000 --- a/Back-End/Dockers/notification_service/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -cd /Notifications - -# Apply datab -python3 manage.py makemigrations -python3 manage.py makemigrations my_notifications -python3 manage.py migrate - -# Stars -python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/Back-End/Dockers/notification_service/requirements.txt b/Back-End/Dockers/notification_service/requirements.txt deleted file mode 100644 index eca8004d..00000000 --- a/Back-End/Dockers/notification_service/requirements.txt +++ /dev/null @@ -1,36 +0,0 @@ -amqp==5.3.1 -asgiref==3.8.1 -async-timeout==5.0.1 -billiard==4.2.1 -celery==5.4.0 -certifi==2024.8.30 -cffi==1.17.1 -channels==4.2.0 -charset-normalizer==3.4.0 -click==8.1.7 -click-didyoumean==0.3.1 -click-plugins==1.1.1 -click-repl==0.3.0 -cryptography==44.0.0 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 -django-oauth-toolkit==3.0.1 -django-redis==5.4.0 -idna==3.10 -jwcrypto==1.5.6 -kombu==5.4.2 -oauthlib==3.2.2 -prompt_toolkit==3.0.48 -psycopg==3.2.3 -pycparser==2.22 -python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 -six==1.16.0 -sqlparse==0.5.2 -typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 -vine==5.1.0 -wcwidth==0.2.13 diff --git a/Back-End/Dockers/prometheus/Dockerfile b/Back-End/Dockers/prometheus/Dockerfile deleted file mode 100644 index fead0dcf..00000000 --- a/Back-End/Dockers/prometheus/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM prom/prometheus:latest -COPY prometheus.yml /etc/prometheus/prometheus.yml diff --git a/Back-End/Dockers/prometheus/prometheus.yml b/Back-End/Dockers/prometheus/prometheus.yml deleted file mode 100644 index bf54ea6b..00000000 --- a/Back-End/Dockers/prometheus/prometheus.yml +++ /dev/null @@ -1,11 +0,0 @@ -global: - scrape_interval: 15s - -scrape_configs: - - job_name: 'postgres' - static_configs: - - targets: ['postgres_exporter:9187'] - - - job_name: 'redis' - static_configs: - - targets: ['redis_exporter:9121'] \ No newline at end of file diff --git a/Back-End/Dockers/promtail/promtail-config.yaml b/Back-End/Dockers/promtail/promtail-config.yaml deleted file mode 100644 index b3d9dfd0..00000000 --- a/Back-End/Dockers/promtail/promtail-config.yaml +++ /dev/null @@ -1,19 +0,0 @@ - -server: - http_listen_port: 9080 - grpc_listen_port: 0 - -positions: - filename: /tmp/positions.yaml - -clients: - - url: http://loki:3100/loki/api/v1/push - -scrape_configs: - - job_name: system - static_configs: - - targets: - - localhost - labels: - job: varlogs - __path__: /var/log/*.log diff --git a/Back-End/Dockers/rm.sh b/Back-End/Dockers/rm.sh deleted file mode 100755 index c018fc9d..00000000 --- a/Back-End/Dockers/rm.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Get a list of all image IDs -all_images=$(docker images -q) - -# Get a list of image IDs used in docker-compose.yml -used_images=$(docker-compose config | grep 'image:' | awk '{print $2}' | xargs -I {} docker images {} -q) - -# Find the difference between all images and used images -unused_images=$(comm -23 <(echo "$all_images" | sort) <(echo "$used_images" | sort)) - -# Remove unused images -if [ -n "$unused_images" ]; then - echo "Removing unused images..." - for image in $unused_images; do - # Remove any stopped containers using the image - containers=$(docker ps -a -q --filter ancestor=$image) - if [ -n "$containers" ]; then - sudo docker rm -f $containers - fi - # Forcefully remove the image - sudo docker rmi -f $image - done -else - echo "No unused images to remove." -fi \ No newline at end of file diff --git a/Back-End/Dockers/user_db/Dockerfile b/Back-End/Dockers/user_db/Dockerfile deleted file mode 100644 index c79ecd4c..00000000 --- a/Back-End/Dockers/user_db/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM postgres:latest - -ENV POSTGRES_USER=pasquale -ENV POSTGRES_DB=user_db -ENV POSTGRES_PASSWORD=123 - -EXPOSE 5432 - -COPY ./init.sql /docker-entrypoint-initdb.d/ \ No newline at end of file diff --git a/Back-End/Dockers/user_db/init.sql b/Back-End/Dockers/user_db/init.sql deleted file mode 100644 index 9d7e169e..00000000 --- a/Back-End/Dockers/user_db/init.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Connect to the new database -\c user_db; --- Create a sample table --- CREATE TABLE avatars ( --- id SERIAL PRIMARY KEY --- ); - --- CREATE TABLE users ( --- id SERIAL PRIMARY KEY, --- first_name VARCHAR(100), --- last_name VARCHAR(100), --- birth_date TIMESTAMP, --- lvl NUMERIC(6, 3), --- bio VARCHAR(500), --- id_avatar INTEGER REFERENCES avatars(id), --- id_account INTEGER --- ); - --- CREATE TABLE friends ( --- id SERIAL PRIMARY KEY, --- user_1 INTEGER REFERENCES users(id) ON DELETE CASCADE, --- user_2 INTEGER REFERENCES users(id) ON DELETE CASCADE --- ); - --- Insert some initial data \ No newline at end of file diff --git a/Back-End/Dockers/usertask_db/Dockerfile b/Back-End/Dockers/usertask_db/Dockerfile deleted file mode 100644 index 4e6c266f..00000000 --- a/Back-End/Dockers/usertask_db/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM postgres:latest - -# ENV POSTGRES_USER=pasquale -# ENV POSTGRES_DB=usertask_db -# ENV POSTGRES_PASSWORD=123 - -EXPOSE 5432 - -COPY ./init.sql /docker-entrypoint-initdb.d/ diff --git a/Back-End/Dockers/usertask_db/init.sql b/Back-End/Dockers/usertask_db/init.sql deleted file mode 100644 index 19fe8075..00000000 --- a/Back-End/Dockers/usertask_db/init.sql +++ /dev/null @@ -1,37 +0,0 @@ --- Connect to the new database -\c usertask_db; - --- Create a sample table - --- CREATE TABLE categories ( --- id SERIAL PRIMARY KEY, --- "name" VARCHAR(100), --- "description" VARCHAR(500) --- ); - --- CREATE TABLE tasks ( --- id SERIAL PRIMARY KEY, --- id_category INTEGER REFERENCES categories(id), --- "name" VARCHAR(100), --- "description" VARCHAR(500), --- previous_task INTEGER REFERENCES tasks(id), --- next_task INTEGER REFERENCES tasks(id), --- duration TIMESTAMP --- ); - --- CREATE TABLE progresses ( --- id SERIAL PRIMARY KEY, --- id_task INTEGER REFERENCES tasks(id) ON DELETE CASCADE, --- id_user INTEGER, --- pregession_rate NUMERIC(6, 3), --- joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP --- ); - --- -- Insert some initial data --- INSERT INTO categories ("name", "description") --- VALUES --- ('sport', 'prenditi cura del tuo corpo'), --- ('art', 'attivita` artistiche'), --- ('culture', 'attivita` culturali'), --- ('alimentation', 'cura la tua alimentazione'), --- ('mental', 'migliora il tuo equilibrio mentale'); \ No newline at end of file diff --git a/Back-End/Dockers/usertask_db/tasks.json b/Back-End/Dockers/usertask_db/tasks.json deleted file mode 100644 index d124dd35..00000000 --- a/Back-End/Dockers/usertask_db/tasks.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Docker Compose Up", - "type": "shell", - "command": "docker-compose up", - "options": { - "cwd": "${workspaceFolder}/Back-End/Dockers" - }, - "problemMatcher": [], - "group": "build" - }, - { - "label": "Wait 10 Seconds", - "type": "shell", - "command": "sleep 10", - "problemMatcher": [], - "group": "build", - }, - { - "label": "Check and Create Venv", - "type": "shell", - "command": "[ -d venv ] || python3 -m venv venv", - "options": { - "cwd": "${workspaceFolder}/Back-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Wait 10 Seconds" - }, - { - "label": "Activate Venv and Install Requirements", - "type": "shell", - "command": "source venv/bin/activate && pip install -r requirements.txt", - "options": { - "cwd": "${workspaceFolder}/Back-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Check and Create Venv" - }, - { - "label": "Run Login Server", - "type": "shell", - "command": "source venv/bin/activate && cd login && python3 manage.py makemigrations && python3 manage.py migrate && python3 manage.py runserver", - "options": { - "cwd": "${workspaceFolder}/Back-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Activate Venv and Install Requirements" - }, - { - "label": "Run Chat Server", - "type": "shell", - "command": "source venv/bin/activate && cd chat && python3 manage.py makemigrations && python3 manage.py makemigrations my_chat && python3 manage.py migrate && python3 manage.py runserver 8001", - "options": { - "cwd": "${workspaceFolder}/Back-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Activate Venv and Install Requirements" - }, - { - "label": "Run Task Server", - "type": "shell", - "command": "source venv/bin/activate && cd task_user && python3 manage.py makemigrations && python3 manage.py migrate && python3 manage.py runserver 8002", - "options": { - "cwd": "${workspaceFolder}/Back-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Activate Venv and Install Requirements" - }, - { - "label": "Start Front-End", - "type": "shell", - "command": "npm start", - "options": { - "cwd": "${workspaceFolder}/Front-End" - }, - "problemMatcher": [], - "group": "build", - "dependsOn": "Wait 10 Seconds" - }, - { - "label": "Run All", - "type": "shell", - "command": "echo 'Running all tasks...'", - "problemMatcher": [], - "dependsOn": [ - "Docker Compose Up", - "Wait 10 Seconds", - "Run Login Server", - "Run Chat Server", - "Run Task Server", - "Start Front-End" - ], - "group": "build" - } - ] -} \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/Dockerfile b/Back-End/Dockers/usertask_service/Dockerfile deleted file mode 100644 index b707ca04..00000000 --- a/Back-End/Dockers/usertask_service/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.14-rc-bookworm - -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev - -COPY ./task_user /task_user -COPY ./requirements.txt /task_user/requirements.txt -WORKDIR /task_user - -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -EXPOSE 8000 - -COPY ./entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/entrypoint.sh b/Back-End/Dockers/usertask_service/entrypoint.sh deleted file mode 100644 index 54057500..00000000 --- a/Back-End/Dockers/usertask_service/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -cd /task_user - -# Apply datab -python3 manage.py makemigrations -python3 manage.py makemigrations user_app task_app -python3 manage.py migrate - -# Stars -python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/requirements.txt b/Back-End/Dockers/usertask_service/requirements.txt deleted file mode 100644 index b5591f80..00000000 --- a/Back-End/Dockers/usertask_service/requirements.txt +++ /dev/null @@ -1,44 +0,0 @@ -aiohappyeyeballs==2.4.4 -aiohttp==3.11.9 -aiosignal==1.3.1 -amqp==5.3.1 -asgiref==3.8.1 -async-timeout==5.0.1 -attrs==24.2.0 -billiard==4.2.1 -celery==5.4.0 -certifi==2024.8.30 -cffi==1.17.1 -charset-normalizer==3.4.0 -click==8.1.7 -click-didyoumean==0.3.1 -click-plugins==1.1.1 -click-repl==0.3.0 -cryptography==44.0.0 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 -django-oauth-toolkit==3.0.1 -django-redis==5.4.0 -djangorestframework==3.15.2 -frozenlist==1.5.0 -idna==3.10 -jwcrypto==1.5.6 -kombu==5.4.2 -multidict==6.1.0 -oauthlib==3.2.2 -prompt_toolkit==3.0.48 -propcache==0.2.1 -psycopg==3.2.3 -pycparser==2.22 -python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 -six==1.16.0 -sqlparse==0.5.2 -typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 -vine==5.1.0 -wcwidth==0.2.13 -yarl==1.18.3 diff --git a/Back-End/Dockers/usertask_service/task_user/db.sqlite3 b/Back-End/Dockers/usertask_service/task_user/db.sqlite3 deleted file mode 100644 index 903b38ee..00000000 Binary files a/Back-End/Dockers/usertask_service/task_user/db.sqlite3 and /dev/null differ diff --git a/Back-End/Dockers/usertask_service/task_user/manage.py b/Back-End/Dockers/usertask_service/task_user/manage.py deleted file mode 100755 index 6bfae2d7..00000000 --- a/Back-End/Dockers/usertask_service/task_user/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_user.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/Back-End/Dockers/usertask_service/task_user/media/avatar/cloud.png b/Back-End/Dockers/usertask_service/task_user/media/avatar/cloud.png deleted file mode 100644 index 37555fbd..00000000 Binary files a/Back-End/Dockers/usertask_service/task_user/media/avatar/cloud.png and /dev/null differ diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/__init__.py b/Back-End/Dockers/usertask_service/task_user/task_app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/admin.py b/Back-End/Dockers/usertask_service/task_user/task_app/admin.py deleted file mode 100644 index 44ea1c77..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin -from django.contrib.auth import get_user_model -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -# Register your models here. - -User = get_user_model() - -@receiver(post_migrate) -def create_superuser(sender, **kwargs): - if not User.objects.filter(email='pasquale@example.com').exists(): - User.objects.create_superuser(username='pasquale', email='pasquale@example.com', password='123') - print('Superuser created successfully.') \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/apps.py b/Back-End/Dockers/usertask_service/task_user/task_app/apps.py deleted file mode 100644 index 7c1d5e08..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/apps.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.apps import AppConfig - -class TaskAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'task_app' - - def ready(self): - try: - # from .authentications import register_self , user_register_self - from .admin import create_superuser - # from .start_tables import CreateTasksSignal - #user_register_self() - #print("User login successful") - # register_self() - except Exception as e: - # Optionally log the exception - print(f"Error during registration: {e}") - pass \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/authentications.py b/Back-End/Dockers/usertask_service/task_user/task_app/authentications.py deleted file mode 100644 index 1d4dedfb..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/authentications.py +++ /dev/null @@ -1,141 +0,0 @@ -import requests -from oauth2_provider.models import AccessToken -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -from django.conf import settings -from datetime import datetime, timedelta -from task_user.settings import oauth2_settings -import json - -def login_self(): - login_url = 'http://localhost:8000/login/login' - data = { - 'email': 'chat@chat.me', - 'password' : 'chat_password', - } - - response = requests.post(login_url, json=data) - - if response.status_code != 200: - raise Exception('Failed to login user') - return response.json() - -def user_register_self(): - register_url = 'http://localhost:8000/login/register' - data = { - 'email': 'chat@chat.me', - 'username': 'my_chat', - 'password': 'chat_password', - } - response = requests.post(register_url, json=data) - - if response.status_code == 400: - login_self() - elif response.status_code != 201: - raise Exception('Failed to register user') - else: - login_self() - - -def register_self(): - csrf_url = 'http://localhost:8000/login/get_csrf_token' - register_url = 'http://localhost:8000/login/Serviceregister' - - data = { - 'name': 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S'), - 'service_password': oauth2_settings['SERVICE_PASSWORD'], - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'redirect_uris': 'http://localhost:8000', - 'client_id': oauth2_settings['CLIENT_ID'], - 'client_secret': oauth2_settings['CLIENT_SECRET'], - } - - session = requests.Session() - csrf_resp = session.get(csrf_url) - # print("Response cookies:", json.dumps(dict(csrf_resp.cookies), indent=4)) - # print("Response headers:", json.dumps(dict(csrf_resp.headers), indent=4)) - - if 'csrftoken' not in csrf_resp.cookies: - raise Exception('CSRF token not found in response cookies') - - csrf_token = csrf_resp.cookies['csrftoken'] - - access_token = oauth2_settings['TOKEN'] - headers = { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrf_token, - 'Authorization': 'Bearer ' + access_token, - } - - response = session.post(register_url, json=data, headers=headers) - if response.status_code != 200: - raise Exception('Failed to register application') - try: - app_data = response.json() - except json.JSONDecodeError: - raise Exception('Failed to parse JSON response') - oauth2_settings['CLIENT_ID'] = app_data['client_id'] - oauth2_settings['CLIENT_SECRET'] = app_data['client_secret'] - - -#auth classes -import requests -from django.conf import settings -from django.core.cache import cache -from django.contrib.auth.models import AnonymousUser -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed - -from django.contrib.auth import get_user_model - -class TokenAuthentication(BaseAuthentication): - def authenticate(self, request): - # Get token from Authorization header - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - return None # No token provided; DRF will treat it as an anonymous request - - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - - if user is not None: - return (user, token) # Return tuple (user, auth) as required by DRF - else: - raise AuthenticationFailed("Invalid or inactive token") - - def get_user_from_token(self, token): - # Check the cache for user - user = cache.get(token) - if user: - return user - - # Token introspection request - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - try: - response = requests.post(introspection_url, data={ - 'token': token, - 'client_id': client_id, - 'client_secret': client_secret, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - user_id = data.get('user_id') - User = get_user_model() - try: - user = User.objects.get(id=user_id) - # Cache user for token expiry duration (default to 5 min if not provided) - expires_in = data.get('expires_in', 300) - cache.set(token, user, timeout=expires_in) - return user - except User.DoesNotExist: - pass - except requests.RequestException as e: - raise AuthenticationFailed(f"Token introspection failed: {str(e)}") - - return None # Default to None if authentication fails \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/celery_task.py b/Back-End/Dockers/usertask_service/task_user/task_app/celery_task.py deleted file mode 100644 index 7c53767a..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/celery_task.py +++ /dev/null @@ -1,7 +0,0 @@ -from celery import shared_task -from .models import * -import requests - -@shared_task -def task_notify(): - pass \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/dictionaries.py b/Back-End/Dockers/usertask_service/task_user/task_app/dictionaries.py deleted file mode 100644 index 9ae332c3..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/dictionaries.py +++ /dev/null @@ -1,9 +0,0 @@ -TASK_CATEGORIES = { - "00": "NONE", - "SP": "Sport", - "ED": "Education", - "HE": "Health", - "AR": "Art", - "SS": "Social Skills", - "MD": "Mindset", -} \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/models.py b/Back-End/Dockers/usertask_service/task_user/task_app/models.py deleted file mode 100644 index cd457c3d..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models -from django.conf import settings -from .dictionaries import TASK_CATEGORIES -from user_app.models import Users - -# Create your models here. -# class Categories(models.Model): -# id = models.AutoField(primary_key=True) -# name = models.CharField(max_length=255, unique=True) -# description = models.TextField() - -class Tasks(models.Model): - id = models.AutoField(primary_key=True, unique=True) - author = models.ForeignKey(Users, on_delete=models.SET_NULL, null=True, related_name="creator") - name = models.CharField(max_length=255) - description = models.TextField() - duration = models.DurationField() - exp = models.PositiveIntegerField() - category = models.CharField(max_length=3, choices=TASK_CATEGORIES, default='00') - previous_task = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, related_name='sequence') - # previous_task = models.ManyToManyField("self", null=True, related_name="father") - # next_task = models.ForeignKey('Tasks', on_delete=models.SET(0)) - -class Progresses(models.Model): - id = models.AutoField(primary_key=True, unique=True) - task = models.ForeignKey(Tasks, on_delete=models.CASCADE, related_name="job") - user = models.ForeignKey(Users, on_delete=models.CASCADE, related_name="joiner") - rate = models.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = models.DateTimeField(auto_now_add=True) - last_modified = models.DateTimeField(auto_now=True) - finish_date = models.DateTimeField(null=True, default=None) diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/serializer.py b/Back-End/Dockers/usertask_service/task_user/task_app/serializer.py deleted file mode 100644 index 6557f3a8..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/serializer.py +++ /dev/null @@ -1,98 +0,0 @@ -from rest_framework import serializers -from .models import Tasks, Progresses -from user_app.models import Users -from .dictionaries import TASK_CATEGORIES -import datetime - -class TasksSerializer(serializers.ModelSerializer): - author = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all()) - name = serializers.CharField(max_length=255) - description = serializers.CharField() - duration = serializers.DurationField() - exp = serializers.IntegerField() - category = serializers.ChoiceField(choices=TASK_CATEGORIES, required=True) - previous_task = serializers.PrimaryKeyRelatedField(queryset=Tasks.objects.all(), required=False) - - def validate_name(self, value): - if len(str(value)) < 1: - raise serializers.ValidationError('name is not valid') - return value - - def validate_description(self, value): - if len(str(value)) < 1: - raise serializers.ValidationError('description is not valid') - return value - - def validate_exp(self, value): - if int(value) <= 0: - raise serializers.ValidationError('exp is not valid') - return value - - class Meta: - model = Tasks - fields = '__all__' - -class ProgressesSerializer(serializers.ModelSerializer): - task = serializers.PrimaryKeyRelatedField(queryset=Tasks.objects.all(), many=False) - user = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - def validate_rate(self, value): - if float(value) < 0: - raise serializers.ValidationError("rate is not valid") - return value - - def validate(self, data): - if Progresses.objects.filter(task=data['task'], user=data['user']): - raise serializers.ValidationError("user is already registered to this task") - t = data['task'] - if t.previous_task is None: - return data - if Progresses.objects.filter(task=t.previous_task, user=data['user']): - if Progresses.objects.get(task=t.previous_task, user=data['user']).rate >= 100: - return data - raise serializers.ValidationError("user must complete previous task before join this one") - - class Meta: - model = Progresses - fields = '__all__' - -class ProgressesReadSerializer(serializers.ModelSerializer): - task = TasksSerializer() - user = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - class Meta: - model = Progresses - fields = '__all__' - -class ProgressManageSerializer(serializers.ModelSerializer): - task = serializers.PrimaryKeyRelatedField(read_only=True) - user = serializers.PrimaryKeyRelatedField(read_only=True) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, required=True) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - def validate_rate(self, value): - if float(value) < 0: - raise serializers.ValidationError("rate is not valid") - return value - - def validate(self, data): - try: - if data['rate'] >= 100: - data['finish_date'] = datetime.datetime.now() - return data - except KeyError: - raise serializers.ValidationError("rate is not provided") - - class Meta: - model = Progresses - fields = '__all__' diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/start_tables.py b/Back-End/Dockers/usertask_service/task_user/task_app/start_tables.py deleted file mode 100644 index 8e1f147e..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/start_tables.py +++ /dev/null @@ -1,74 +0,0 @@ -from .models import Tasks -from user_app.models import Avatars, Users -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -def CreateTasks(**kwargs): - if not Avatars.objects.all(): - a = Avatars.objects.create( - name='default avatar' - ) - a.save() - if not Users.objects.all(): - u = Users.objects.create( - user_id=1, - first_name="mario", - last_name="rossi", - birth_date="2000-1-1", - bio="bio dei miei ciglioni", - avatar=Avatars.objects.get(id=1) - ) - u.save() - u = Users.objects.create( - user_id=2, - first_name="vincenzo", - last_name="bianchi", - birth_date="1999-11-4", - bio="bio dei miei ciglioni", - avatar=Avatars.objects.get(id=1) - ) - u.save() - u = Users.objects.create( - user_id=3, - staff=True, - first_name="guglielmo", - last_name="duranti", - birth_date="1900-11-4", - bio="ma ti fanno anche da uomo", - avatar=Avatars.objects.get(id=1) - ) - u.save() - if not Tasks.objects.all(): - t = Tasks.objects.create( - author=Users.objects.get(user_id=3), - name='walk', - description='walk at least 10k step each day', - duration='7 00:00:00', - exp=1000, - category='SP' - ) - t.save() - t = Tasks.objects.create( - author=Users.objects.get(user_id=3), - name='run', - description='improve your body health step by step', - duration='30 00:00:00', - exp=2000, - category='SP', - previous_task=Tasks.objects.get(id=1) - ) - t.save() - t = Tasks.objects.create( - author=Users.objects.get(user_id=3), - name='drink water', - description='drink 2l of water a day', - duration='7 00:00:00', - exp=500, - category='HE' - ) - t.save() - -@receiver(post_migrate) -def CreateTasksSignal(sender, **kwargs): - CreateTasks() - print('done') \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/tests.py b/Back-End/Dockers/usertask_service/task_user/task_app/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/urls.py b/Back-End/Dockers/usertask_service/task_user/task_app/urls.py deleted file mode 100644 index 4b7b910f..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('task', views.TaskGen.as_view(), name='task_gen'), - path('task//', views.TaskManage.as_view(), name='task_manage'), - path('progress', views.ProgressGen.as_view(), name='progress_gen'), - path('progress//', views.ProgressDelete.as_view(), name='progress_manage'), - path('progress/&/', views.ProgressManage.as_view(), name='user_progress'), - path('ubt', views.GetUsersByTask.as_view(), name='users_by_task'), - path('tbu', views.GetTasksByUser.as_view(), name='task_by_user'), - # path('tasks_by_category', views.GetTasksByCategory.as_view(), name='tasks_by_category'), - # path('gufet', views.GetUsersForEachTask.as_view(), name='gufet'), - # path('gtfeu', views.GetTasksForEachUser.as_view(), name='gtfeu') -] \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_app/views.py b/Back-End/Dockers/usertask_service/task_user/task_app/views.py deleted file mode 100644 index ec12f083..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_app/views.py +++ /dev/null @@ -1,115 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from rest_framework import permissions, status, generics, filters -from rest_framework.views import APIView -from rest_framework.response import Response -from .models import Tasks, Progresses -from .serializer import TasksSerializer, ProgressesSerializer, ProgressManageSerializer, ProgressesReadSerializer -from user_app.models import Users - -class MultipleFieldLookupMixin: - """ - Apply this mixin to any view or viewset to get multiple field filtering - based on a `lookup_fields` attribute, instead of the default single field filtering. - """ - def get_object(self): - queryset = self.get_queryset() # Get the base queryset - queryset = self.filter_queryset(queryset) # Apply any filter backends - filter = {} - for field in self.lookup_fields: - if self.kwargs.get(field): # Ignore empty fields. - filter[field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter) # Lookup the object - self.check_object_permissions(self.request, obj) - return obj - -class TaskGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = TasksSerializer - lookup_fields = ['author__user_id', 'category', 'duration', 'exp'] - - def get_queryset(self): - queryset = Tasks.objects.all() - if self.request.method == 'GET': - user_id = self.request.query_params.get('user_id') - if user_id: - My_progress = Progresses.objects.filter(user_id=user_id) - queryset = queryset.exclude(id__in=[x.task.id for x in My_progress]) - return queryset - return Tasks.objects.all() - - - - -class TaskManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = TasksSerializer - # lookup_field = 'id' - lookup_url_kwarg = 'id' - queryset = Tasks.objects.all() - -class ProgressGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) - filter_backends = [filters.SearchFilter] - search_fields = ['user__user_id', 'task__id'] - queryset = Progresses.objects.all() - - def get_serializer_class(self): - if self.request.method == 'POST': - return ProgressesSerializer - return ProgressesReadSerializer - - def get_queryset(self): - queryset = super().get_queryset() - user_id = self.request.query_params.get('user_id', None) - if user_id is not None: - queryset = queryset.filter(user__user_id=user_id) - return queryset - -class ProgressDelete(generics.DestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = ProgressesSerializer - # lookup_field = 'id' - lookup_url_kwarg = 'id' - queryset = Progresses.objects.all() - -class ProgressManage(MultipleFieldLookupMixin, generics.RetrieveUpdateAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = ProgressManageSerializer - lookup_fields = ['task', 'user'] - queryset = Progresses.objects.all() - -class GetUsersByTask(APIView): - permission_classes = (permissions.AllowAny,) - def get(self, request): - prog = Progresses.objects.all() - tsk = Tasks.objects.all() - ans = dict() - for x in tsk: - row = [] - for y in prog: - if y.task.id == x.id: - row.append(y.user.account_id) - ans[x.id] = row - if len(ans) < 1: - return Response({ - 'error': 'cannot provide a valid view' - }, status=status.HTTP_400_BAD_REQUEST) - return Response(ans) - -class GetTasksByUser(APIView): - permission_classes = (permissions.AllowAny,) - def get(self, request): - prog = Progresses.objects.all() - usr = Users.objects.all() - ans = dict() - for x in usr: - row = [] - for y in prog: - if y.user.id == x.id: - row.append(y.task.id) - ans[x.id] = row - if len(ans) < 1: - return Response({ - 'error': 'cannot provide a valid view' - }, status=status.HTTP_400_BAD_REQUEST) - return Response(ans) diff --git a/Back-End/Dockers/usertask_service/task_user/task_user/__init__.py b/Back-End/Dockers/usertask_service/task_user/task_user/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Back-End/Dockers/usertask_service/task_user/task_user/asgi.py b/Back-End/Dockers/usertask_service/task_user/task_user/asgi.py deleted file mode 100644 index 15134f87..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_user/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for task_user project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_user.settings') - -application = get_asgi_application() diff --git a/Back-End/Dockers/usertask_service/task_user/task_user/settings.py b/Back-End/Dockers/usertask_service/task_user/task_user/settings.py deleted file mode 100644 index ab685ca4..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_user/settings.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Django settings for task_user project. - -Generated by 'django-admin startproject' using Django 5.1.1. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path -from celery.schedules import crontab -import os - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent -API_KEY = os.getenv('API_KEY', '123') - -def arise(exception): - raise(exception) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-7&!j-$l_bhb60^ce0cwgnp+9=sylmv@sy(fu)d-vylz!b=g!ur' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG', True) - -ALLOWED_HOSTS = [] - -CORS_ALLOWED_ORIGINS = [ - -] -# CORS_ALLOW_ALL_ORIGINS = True -CORS_ALLOW_CREDENTIALS = True - -CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', -] - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'django_filters', - 'oauth2_provider', - 'task_app', - 'user_app', - 'corsheaders', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', -] - -ROOT_URLCONF = 'task_user.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'task_user.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'task_user'), - 'USER': os.getenv('POSTGRES_USER', 'task_user'), - 'PASSWORD' : os.getenv('POSTGRES_PASSWORD', 'task_user'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5432'), - }, - -} - -import secrets - -oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password -} - - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -CELERY_BROKER_URL = 'redis://localhost:6702/0' -CELERY_RESULT_BACKEND = 'redis://localhost:6702/0' -CELERY_BEAT_SCHEDULE = { - 'task_notify': { - 'task': 'task_app.celerity_task.task_notify', - 'schedule': crontab(minute=0, hour=10), - }, -} - - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], - 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], - -} - -MEDIA_ROOT = BASE_DIR / 'media/' -# MEDIA_URL = 'http://localhost:8000/' \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/task_user/urls.py b/Back-End/Dockers/usertask_service/task_user/task_user/urls.py deleted file mode 100644 index b0b3a075..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_user/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -URL configuration for task_user project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include - -urlpatterns = [ - path('admin/', admin.site.urls), - path('task/', include('task_app.urls')), - path('user/', include('user_app.urls')), -] diff --git a/Back-End/Dockers/usertask_service/task_user/task_user/wsgi.py b/Back-End/Dockers/usertask_service/task_user/task_user/wsgi.py deleted file mode 100644 index 1fbba796..00000000 --- a/Back-End/Dockers/usertask_service/task_user/task_user/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for task_user project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_user.settings') - -application = get_wsgi_application() diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/__init__.py b/Back-End/Dockers/usertask_service/task_user/user_app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/admin.py b/Back-End/Dockers/usertask_service/task_user/user_app/admin.py deleted file mode 100644 index 44ea1c77..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin -from django.contrib.auth import get_user_model -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -# Register your models here. - -User = get_user_model() - -@receiver(post_migrate) -def create_superuser(sender, **kwargs): - if not User.objects.filter(email='pasquale@example.com').exists(): - User.objects.create_superuser(username='pasquale', email='pasquale@example.com', password='123') - print('Superuser created successfully.') \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/apps.py b/Back-End/Dockers/usertask_service/task_user/user_app/apps.py deleted file mode 100644 index 6154b5ea..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/apps.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.apps import AppConfig - -# class TaskAppConfig(AppConfig): -# default_auto_field = 'django.db.models.BigAutoField' -# name = 'user_app' - -# def ready(self): -# from .start_tables import CreateUsersSignal -# try: -# pass -# except Exception as e: -# print(str(e)) - -class UserAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'user_app' - - def ready(self): - try: - # from .authentications import register_self , user_register_self - from .admin import create_superuser - from .start_tables import CreateUsersSignal - #user_register_self() - #print("User login successful") - # register_self() - except Exception as e: - # Optionally log the exception - print(f"Error during registration: {e}") - pass \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/authentications.py b/Back-End/Dockers/usertask_service/task_user/user_app/authentications.py deleted file mode 100644 index 0fcdb699..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/authentications.py +++ /dev/null @@ -1,147 +0,0 @@ -import requests -from oauth2_provider.models import AccessToken -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -from django.conf import settings -from datetime import datetime, timedelta -from task_user.settings import oauth2_settings -import json - -def login_self(): - login_url = 'http://localhost:8000/login/login' - data = { - 'email': 'chat@chat.me', - 'password' : 'chat_password', - } - - response = requests.post(login_url, json=data) - response_data = response.json() - print("login resp:", response_data) - - if response.status_code != 200: - raise Exception('Failed to login user') - return response.json() - -def user_register_self(): - register_url = 'http://localhost:8000/login/register' - data = { - 'email': 'chat@chat.me', - 'username': 'my_chat', - 'password': 'chat_password', - } - response = requests.post(register_url, json=data) - print("Response:", response.json()) - print("Response status code:", response.status_code) - - if response.status_code == 400: - login_self() - elif response.status_code != 201: - raise Exception('Failed to register user') - else: - login_self() - - -def register_self(): - csrf_url = 'http://localhost:8000/login/get_csrf_token' - register_url = 'http://localhost:8000/login/Serviceregister' - - data = { - 'name': 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S'), - 'service_password': oauth2_settings['SERVICE_PASSWORD'], - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'redirect_uris': 'http://localhost:8000', - 'client_id': oauth2_settings['CLIENT_ID'], - 'client_secret': oauth2_settings['CLIENT_SECRET'], - } - - session = requests.Session() - csrf_resp = session.get(csrf_url) - print("Response cookies:", json.dumps(dict(csrf_resp.cookies), indent=4)) - print("Response headers:", json.dumps(dict(csrf_resp.headers), indent=4)) - - if 'csrftoken' not in csrf_resp.cookies: - raise Exception('CSRF token not found in response cookies') - - csrf_token = csrf_resp.cookies['csrftoken'] - - access_token = oauth2_settings['TOKEN'] - print("Access token:", access_token) - headers = { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrf_token, - 'Authorization': 'Bearer ' + access_token, - } - - response = session.post(register_url, json=data, headers=headers) - print("Response:", response.json()) - if response.status_code != 200: - raise Exception('Failed to register application') - try: - app_data = response.json() - except json.JSONDecodeError: - raise Exception('Failed to parse JSON response') - oauth2_settings['CLIENT_ID'] = app_data['client_id'] - oauth2_settings['CLIENT_SECRET'] = app_data['client_secret'] - - -#auth classes -import requests -from django.conf import settings -from django.core.cache import cache -from django.contrib.auth.models import AnonymousUser -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed - -from django.contrib.auth import get_user_model - -class TokenAuthentication(BaseAuthentication): - def authenticate(self, request): - # Get token from Authorization header - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - return None # No token provided; DRF will treat it as an anonymous request - - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - - if user is not None: - return (user, token) # Return tuple (user, auth) as required by DRF - else: - raise AuthenticationFailed("Invalid or inactive token") - - def get_user_from_token(self, token): - # Check the cache for user - user = cache.get(token) - if user: - return user - - # Token introspection request - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - try: - response = requests.post(introspection_url, data={ - 'token': token, - 'client_id': client_id, - 'client_secret': client_secret, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - user_id = data.get('user_id') - User = get_user_model() - try: - user = User.objects.get(id=user_id) - # Cache user for token expiry duration (default to 5 min if not provided) - expires_in = data.get('expires_in', 300) - cache.set(token, user, timeout=expires_in) - return user - except User.DoesNotExist: - pass - except requests.RequestException as e: - raise AuthenticationFailed(f"Token introspection failed: {str(e)}") - - return None # Default to None if authentication fails \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/middleware.py b/Back-End/Dockers/usertask_service/task_user/user_app/middleware.py deleted file mode 100644 index b7f5b77f..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/middleware.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf import settings -from rest_framework.permissions import BasePermission - -class APIKeyPermission(BasePermission): - def has_permission(self, request, view): - api_key = request.headers.get('X-API-KEY') - print("API Key:", api_key) - if api_key: - return api_key == settings.API_KEY - return False - diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/models.py b/Back-End/Dockers/usertask_service/task_user/user_app/models.py deleted file mode 100644 index 4dce1f49..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/models.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.db import models -from django.conf import settings - - -class Avatars(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=255) - image = models.URLField(max_length=200, default='https://drive.google.com/file/d/1MDi_OPO_HtWyKTmI_35GQ4KjA7uh0Z9U/view?usp=drive_link') - # image = models.ImageField(upload_to="avatar/", null=True) - -class Users(models.Model): - staff = models.BooleanField(default=False) - user_id = models.IntegerField(unique=True, primary_key=True) - first_name = models.CharField(max_length=255, null=True, default="") - last_name = models.CharField(max_length=255, null=True, default="") - birth_date = models.DateField(null=True, default=None, blank=True) - bio = models.TextField(default="", null=True, blank=True) - exp = models.IntegerField(default=0) - level = models.IntegerField(default=0) - avatar = models.ForeignKey(Avatars, default=1, on_delete=models.SET(1), related_name="picture") - last_modified = models.DateTimeField(auto_now=True) - -class Friendships(models.Model): - id = models.AutoField(primary_key=True) - user_1 = models.ForeignKey(Users, on_delete=models.CASCADE, related_name='u1') - user_2 = models.ForeignKey(Users, on_delete=models.CASCADE, related_name='u2') - accepted = models.BooleanField(default=False) - last_modified = models.DateTimeField(auto_now=True) \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/notification.py b/Back-End/Dockers/usertask_service/task_user/user_app/notification.py deleted file mode 100644 index 61443e19..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/notification.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.db import models -import os, aiohttp - -Microservices = { - 'Login': os.getenv('loginservice', 'http://localhost:8000'), - 'Chat': os.getenv('chatservice', 'http://localhost:8001'), - 'Users': os.getenv('userService', 'http://localhost:8002'), - 'Notifications': os.getenv('notificationService', 'http://localhost:8003'), - 'Personal' : "Self", -} - -Types = { - 'IM' : 'Immediate', - 'QU' : 'Queued', - 'SC' : 'Scheduled', - 'SE' : 'Sent', -} - -class BaseNotification(models.Model): - id = models.AutoField(primary_key=True) - Sender = models.CharField(max_length=20, choices=Microservices) - message = models.TextField() - - class Meta: - abstract = True - -class UserNotification(BaseNotification): - user_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class GroupNotification(BaseNotification): - group_id = models.IntegerField(default=None) - - class Meta: - abstract = True - -class ImmediateNotification(UserNotification, GroupNotification): - pass - -class ScheduledNotification(UserNotification, GroupNotification): - pass - -async def SendNotification(Notification): - noification_url = Microservices['Notifications'] - async with aiohttp.ClientSession() as session: - async with session.post(noification_url, data=Notification) as response: - return response.status \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/serializer.py b/Back-End/Dockers/usertask_service/task_user/user_app/serializer.py deleted file mode 100644 index 90ea90d1..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/serializer.py +++ /dev/null @@ -1,38 +0,0 @@ -from rest_framework import serializers -from .models import Users, Avatars, Friendships -import datetime - -class AvatarsSerializer(serializers.ModelSerializer): - name = serializers.CharField(max_length=255) - image = serializers.ImageField() - - class Meta: - model = Avatars - fields = '__all__' - -class UsersSerializer(serializers.ModelSerializer): - staff = serializers.BooleanField(default=False) - first_name = serializers.CharField(max_length=255, default="") - last_name = serializers.CharField(max_length=255, default="") - birth_date = serializers.DateField(default=None) - bio = serializers.CharField(default="") - exp = serializers.IntegerField(default=0) - level = serializers.IntegerField(default=0) - # avatar = serializers.PrimaryKeyRelatedField(queryset=Avatars.objects.all(), many=False, default=Avatars.objects.get(id=1)) - #TODO avatar = serializers.PrimaryKeyRelatedField(queryset=Avatars.objects.all(), many=False, default=Avatars.objects.get(id=1)) - last_modified = serializers.DateTimeField(read_only=True) - - class Meta: - model = Users - fields = '__all__' - - -class FriendshipsSerializer(serializers.ModelSerializer): - user_1 = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - user_2 = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - accepted = serializers.BooleanField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - - class Meta: - model = Friendships - fields = '__all__' diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/start_tables.py b/Back-End/Dockers/usertask_service/task_user/user_app/start_tables.py deleted file mode 100644 index 9afb33e9..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/start_tables.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -# def CreateTasks(**kwargs): -# if not Tasks.objects.all(): -# stoca = Tasks.objects.create( -# author_id = 1, -# name = 'stoca', -# description = 'ciao', -# duration = '0 10:10:00', -# exp = 100, -# category = 'SP' -# ) -# stoca.save() - - -@receiver(post_migrate) -def CreateUsersSignal(sender, **kwargs): - # CreateTasks() - print('done') \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/tests.py b/Back-End/Dockers/usertask_service/task_user/user_app/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/urls.py b/Back-End/Dockers/usertask_service/task_user/user_app/urls.py deleted file mode 100644 index f5bec11c..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('avatar', views.AvatarGen.as_view(), name='avatar_gen'), - path('avatar//', views.AvatarManage.as_view(), name='avatar_manage'), - path('user', views.UserGen.as_view(), name='user_gen'), - path('user//', views.UserManage.as_view(), name='user_manage'), - path('friend', views.FriendList.as_view(), name='friend_list'), - path('addfriend', views.AddFriend.as_view(), name='add_friend'), -] \ No newline at end of file diff --git a/Back-End/Dockers/usertask_service/task_user/user_app/views.py b/Back-End/Dockers/usertask_service/task_user/user_app/views.py deleted file mode 100644 index 43e870a9..00000000 --- a/Back-End/Dockers/usertask_service/task_user/user_app/views.py +++ /dev/null @@ -1,125 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from rest_framework.views import APIView -from rest_framework import permissions, status, generics, filters -from rest_framework.response import Response -from .models import * -from .serializer import * -from user_app.models import Users -from .notification import ImmediateNotification, ScheduledNotification , SendNotification -import asyncio -from .middleware import APIKeyPermission -class MultipleFieldLookupMixin: - """ - Apply this mixin to any view or viewset to get multiple field filtering - based on a `lookup_fields` attribute, instead of the default single field filtering. - """ - def get_object(self): - queryset = self.get_queryset() # Get the base queryset - queryset = self.filter_queryset(queryset) # Apply any filter backends - filter = {} - for field in self.lookup_fields: - if self.kwargs.get(field): # Ignore empty fields. - filter[field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter) # Lookup the object - self.check_object_permissions(self.request, obj) - return obj - -class AvatarGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = AvatarsSerializer - queryset = Avatars.objects.all() - -class AvatarManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = AvatarsSerializer - lookup_url_kwarg = 'user_id' - queryset = Avatars.objects.all() - -class UserGen(generics.ListCreateAPIView): - serializer_class = UsersSerializer - filter_backends = [filters.SearchFilter] - search_fields = ['user_id'] - queryset = Users.objects.all() - - def get_permissions(self): - if self.request.method == 'POST': - print("POST here") - self.permission_classes = [APIKeyPermission] - else: - self.permission_classes = (permissions.AllowAny,) - return super().get_permissions() - -class UserManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = UsersSerializer - lookup_url_kwarg = 'user_id' - queryset = Users.objects.all() - -class FriendList(generics.ListAPIView): - serializer_class = FriendshipsSerializer - filter_backends = [filters.SearchFilter] - search_fields = ['user_1__user_id', 'user_2__user_id', 'accepted'] - queryset = Friendships.objects.all() - # def get_queryset(self): - # return Friendships.objects.filter(accepted=True) - -class AddFriend(APIView): - permission_classes = (permissions.AllowAny,) - - def post(self, request): - serializer = FriendshipsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - u1 = Users.objects.get(id=serializer.data['user_1']) - u2 = Users.objects.get(id=serializer.data['user_2']) - if Friendships.objects.filter(user_1=u1, user_2=u2) or Friendships.objects.filter(user_1=u2, user_2=u1): - if Friendships.objects.get(user_1=u1, user_2=u2).accepted: - return Response({ - 'info': 'users are already friends' - }, status=status.HTTP_200_OK) - return Response({ - 'info': 'friend request is pending' - }, status=status.HTTP_200_OK) - fs = Friendships.objects.create( - user_1 = u1, - user_2 = u2 - ) - fs.save() - notifi = ImmediateNotification.objects.create( - Sender = u1, - message = f'Friend request from {u1.first_name} {u1.last_name}', - user_id = u2.id, - group_id = None, - ) - asyncio.create_task(SendNotification(notifi)) - return Response({ - 'info': 'friend request sent' - }, status=status.HTTP_200_OK) - - def patch(self,request): - try: - serializer = FriendshipsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - u1 = Users.objects.get(id=serializer.data['user_1']) - u2 = Users.objects.get(id=serializer.data['user_2']) - if Friendships.objects.filter(user_1=u1, user_2=u2): - fs = Friendships.objects.get(user_1=u1, user_2=u2) - fs.accepted = True - fs.save() - notifi = ImmediateNotification.objects.create( - Sender = u2, - message = f'{u2.first_name} {u2.last_name} accepted your friend request', - user_id = u1.id, - group_id = None, - ) - asyncio.create_task(SendNotification(notifi)) - return Response({ - 'info': 'friend request accepted' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'error': 'friend request not found' - }, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - return Response({ - 'error': e - }, status=status.HTTP_400_BAD_REQUEST) diff --git a/Back-End/Notifications/Dockerfile b/Back-End/Notifications/Dockerfile index e6a2350b..4841a6cb 100644 --- a/Back-End/Notifications/Dockerfile +++ b/Back-End/Notifications/Dockerfile @@ -1,5 +1,5 @@ # Use a stable Python version -FROM python:3.11-alpine AS base +FROM python:3.13-alpine AS base RUN apk add --no-cache postgresql-libs bash @@ -16,6 +16,9 @@ COPY --from=builder /usr/local /usr/local COPY . /app WORKDIR /app +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static + COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/Back-End/Notifications/Notifications/asgi.py b/Back-End/Notifications/Notifications/asgi.py index 607ccefc..c75a62de 100644 --- a/Back-End/Notifications/Notifications/asgi.py +++ b/Back-End/Notifications/Notifications/asgi.py @@ -15,12 +15,13 @@ django.setup() from my_notifications.routing import websocket_urlpatterns -from my_notifications.middleware import JWTAuthMiddlewareStack - +from my_notifications.middleware import JWTAuthMiddlewareStack, DebugMiddleware application = ProtocolTypeRouter({ 'http': get_asgi_application(), - 'websocket': JWTAuthMiddlewareStack( - URLRouter(websocket_urlpatterns) + 'websocket': DebugMiddleware( + JWTAuthMiddlewareStack( + URLRouter(websocket_urlpatterns) + ) ), }) diff --git a/Back-End/Notifications/Notifications/settings.py b/Back-End/Notifications/Notifications/settings.py index 23f6f149..daa70a38 100644 --- a/Back-End/Notifications/Notifications/settings.py +++ b/Back-End/Notifications/Notifications/settings.py @@ -15,7 +15,6 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -23,104 +22,137 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-kp7qs)0l1ie$%muo93+829po%pe9*gz8z8ah6dy0)cskj-5l*c') API_KEY = os.getenv('API_KEY', '123') -# Aggiungi queste impostazioni -FORCE_SCRIPT_NAME = '/api/notifications' - def arise(exception): - raise(exception) - -def ensure_scheme(url): - if url and not url.startswith(('http://', 'https://')): - return f"http://{url}" - return url + raise(exception) +# Simple microservices definition Microservices = { - 'Login': ensure_scheme(os.getenv('LOGIN_SERVICE', 'http://localhost:8000')), - 'Chat': ensure_scheme(os.getenv('CHAT_SERVICE', 'http://localhost:8001')), - 'Users': ensure_scheme(os.getenv('USERS_SERVICE', 'http://localhost:8002')), - 'Notifications': ensure_scheme(os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003')), - 'Pong': ensure_scheme(os.getenv('PONG_SERVICE', 'http://localhost:8004')), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), } - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG', True) - -ALLOWED_HOSTS = [ - 'localhost', - 'localhost:3000', - '127.0.0.1', - '[::1]', - 'trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] - -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] -# CORS_ALLOW_ALL_ORIGINS = True +K8S_ALLOWED_HOSTS = os.environ.get('K8S_ALLOWED_HOSTS', '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16').split(',') + +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN + +ALLOWED_HOSTS = ['*'] +# 'localhost', +# 'localhost:3000', +# '127.0.0.1', +# '[::1]', +# 'trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS + +# CORS_ALLOWED_ORIGINS = [ +# 'http://localhost:3000', +# 'http://localhost', +# 'http://127.0.0.1', +# 'http://[::1]', +# 'https://trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS_WITH_SCHEME +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + 'http://localhost:3000', + 'http://localhost', + 'http://127.0.0.1', + 'http://[::1]', + 'https://trascendence.42firenze.it', + # Aggiungi IP privati per HTTPS + 'https://10.0.2.15', # L'IP specifico della tua VM + # O meglio ancora, patterns per tutti gli IP privati: + 'http://10.*', + 'https://10.*', + 'http://192.168.*', + 'https://192.168.*', + 'http://172.16.*', + 'https://172.16.*', + 'http://172.17.*', + 'https://172.17.*', + 'http://172.18.*', + 'https://172.18.*', + # Continua per tutti i range 172.16.x.x fino a 172.31.x.x + 'http://localhost:8443', + 'http://127.0.0.1:8443', + 'http://10.0.2.15:8443', + 'http://10.0.2.15.xip.io:8443', + 'http://10.11.*.xip.io:8443', + Microservices['Login'], + Microservices['Chat'], + Microservices['Users'], + Microservices['Notifications'], + Microservices['Pong'], +] + K8S_SERVICE_HOSTS_WITH_SCHEME CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', ] # Application definition INSTALLED_APPS = [ - 'daphne', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'oauth2_provider', - 'corsheaders', - 'channels', - 'celery', - 'django_redis', - 'django_filters', - 'my_notifications', - 'rest_framework', - 'redis', + 'django_prometheus', + 'daphne', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'oauth2_provider', + 'corsheaders', + 'channels', + 'celery', + 'django_redis', + 'django_filters', + 'drf_yasg', + 'my_notifications', + 'rest_framework', + 'redis', ] MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', # Add at the top + 'my_notifications.middleware.HealthCheckMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -128,11 +160,12 @@ def ensure_scheme(url): 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', # Add at the end ] REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] } ROOT_URLCONF = 'Notifications.urls' @@ -153,78 +186,83 @@ def ensure_scheme(url): }, ] -WSGI_APPLICATION = 'Notifications.wsgi.application' +# WSGI_APPLICATION = 'Notifications.wsgi.application' ASGI_APPLICATION = 'Notifications.asgi.application' -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'notification_db'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5439'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), - } -} +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'notifications_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5440'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password + 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', + 'CLIENT_ID': secrets.token_urlsafe(32), + 'CLIENT_SECRET': secrets.token_urlsafe(64), + 'TOKEN': '', + 'REFRESH_TOKEN': '', + 'EXPIRES': '', + 'token_type': '', + 'scope': '', + 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password } REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'my_notifications.middleware.JWTAuth', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', - ], - 'DEFAULT_FILTER_BACKENDS': [ - 'django_filters.rest_framework.DjangoFilterBackend' - ] + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'my_notifications.middleware.JWTAuth', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_FILTER_BACKENDS': [ + 'django_filters.rest_framework.DjangoFilterBackend' + ] } - -REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') -REDIS_PORT = os.getenv('REDIS_PORT', '6700') -REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '3') -REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '4') +REDIS_HOST = os.getenv('REDIS_HOST', 'my-umbrella-redis-chart-service.redis-namespace.svc.cluster.local') +REDIS_PORT = os.getenv('REDIS_PORT', '6379') +REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '2') # Notifications: DB 2 +REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '9') # Notifications: Channel DB 9 CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'KEY_PREFIX': 'notifications:', # Service prefix for data isolation + } + } } - - CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], - 'prefix': 'notifications', - }, - } + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], + 'prefix': 'notifications', # Channel prefix + }, + } } # CELERY_BROKER_URL = 'redis://localhost:6701/0' @@ -234,7 +272,6 @@ def ensure_scheme(url): # CELERY_RESULT_SERIALIZER = 'json' # CELERY_TIMEZONE = 'UTC' - # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -253,7 +290,6 @@ def ensure_scheme(url): }, ] - # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -265,11 +301,16 @@ def ensure_scheme(url): USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Changed from '/static/' + +# Add this to define where Django should look for static files +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -278,14 +319,214 @@ def ensure_scheme(url): # Microservices Types = { - 'IM' : 'Immediate', - 'QU' : 'Queued', - 'SC' : 'Scheduled', - 'SE' : 'Sent', + 'IM' : 'Immediate', + 'QU' : 'Queued', + 'SC' : 'Scheduled', + 'SE' : 'Sent', } ADMIN = { - 'username': os.getenv('ADMIN_USERNAME', 'admin'), - 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), - 'password': os.getenv('ADMIN_PASSWORD', 'admin'), -} \ No newline at end of file + 'username': os.getenv('ADMIN_USERNAME', 'admin'), + 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), + 'password': os.getenv('ADMIN_PASSWORD', 'admin'), +} + +# ELK-Optimized Logging Configuration for notifications service +# This configuration provides standardized logging for the ELK Stack + +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "notifications"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/Notifications/Notifications/urls.py b/Back-End/Notifications/Notifications/urls.py index b1835ed9..3c3f4607 100644 --- a/Back-End/Notifications/Notifications/urls.py +++ b/Back-End/Notifications/Notifications/urls.py @@ -15,9 +15,130 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path , include +from django.urls import path, include, re_path +from django.http import JsonResponse, HttpResponse +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Notifications API", + default_version='v1', + description="API for notification management and real-time messaging", + contact=openapi.Contact(email="admin@trascendence.local"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +# Custom view that serves Swagger UI with JWT authentication +def custom_swagger_view(request): + html = """ + + + Notifications API Documentation + + + + + + +
+ + + + +""" + return HttpResponse(html) urlpatterns = [ path('admin/', admin.site.urls), path('notification/', include('my_notifications.urls')), + path('', include('django_prometheus.urls')), # Add metrics endpoint + + # Swagger/API documentation with JWT support + path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', custom_swagger_view, name='schema-swagger-ui'), + re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/schema/', schema_view.without_ui(cache_timeout=0), name='schema-json-api'), + path('health/', lambda request: JsonResponse({'status': 'healthy'}), name='health'), ] diff --git a/Back-End/Notifications/entrypoint.sh b/Back-End/Notifications/entrypoint.sh index 1b69fbcb..e77ffdc2 100644 --- a/Back-End/Notifications/entrypoint.sh +++ b/Back-End/Notifications/entrypoint.sh @@ -4,6 +4,8 @@ set -e export DJANGO_SETTINGS_MODULE=Notifications.settings +# collect static files +python manage.py collectstatic --noinput # migrations at startup python manage.py makemigrations @@ -15,5 +17,5 @@ if [ "$DEBUG" = True ]; then python manage.py runserver 0.0.0.0:8000 else # For production use Daphne (ASGI server) - daphne -b 0.0.0.0 -p 8000 Notifications.asgi:application || { echo "Daphne failed to start"; exit 1; } + daphne -v 3 --access-log - -b 0.0.0.0 -p 8000 Notifications.asgi:application || { echo "Daphne failed to start"; exit 1; } fi \ No newline at end of file diff --git a/Back-End/Notifications/manage.py b/Back-End/Notifications/manage.py old mode 100755 new mode 100644 diff --git a/Back-End/Notifications/my_notifications/apps.py b/Back-End/Notifications/my_notifications/apps.py index 4c98f6ba..a9163dc9 100644 --- a/Back-End/Notifications/my_notifications/apps.py +++ b/Back-End/Notifications/my_notifications/apps.py @@ -7,6 +7,8 @@ class MyNotificationsConfig(AppConfig): def ready(self): from my_notifications.signals import send_notification, schedule_notification, create_user_profile + from .friend_status_sub import FriendStatusListener + FriendStatusListener().start_listener() print('Signals registered') # from my_notifications.admin import register_self # register_self() \ No newline at end of file diff --git a/Back-End/Notifications/my_notifications/consumers.py b/Back-End/Notifications/my_notifications/consumers.py index 3b876e5a..63ecfe3c 100644 --- a/Back-End/Notifications/my_notifications/consumers.py +++ b/Back-End/Notifications/my_notifications/consumers.py @@ -1,13 +1,34 @@ +import asyncio +import json +import logging from channels.layers import get_channel_layer from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async -import json +from .online_status import online_status_service + +logger = logging.getLogger('django') class UserNotificationConsumer(AsyncWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.heartbeat_task = None + async def connect(self): try: + logger.info(f"Notification WebSocket connection attempt for user: {self.scope.get('user', 'Unknown')}") + self.user = self.scope['user'] + + # Check if user is authenticated + if not hasattr(self.user, 'user_id'): + logger.warning(f"Unauthenticated user attempted to connect: {self.user}") + await self.close(code=4001) + return + self.group_name = f'user_notifications_{self.user.user_id}' + + # Test Redis connection + await self.log_redis_connection() await self.channel_layer.group_add( self.group_name, @@ -16,68 +37,146 @@ async def connect(self): await self.accept() await self.set_user_online() - print(f"User {self.user.user_id} connected to WebSocket") + + # Send queued notifications + await self.send_queued_notifications() + + # Start heartbeat monitoring + self.heartbeat_task = asyncio.create_task(self.heartbeat_monitor()) + + logger.info(f"User {self.user.user_id} successfully connected to notifications WebSocket") except Exception as e: - print(f"Error in connect: {e}") - # queued_notifications = await self.get_queued_notifications() - # for notification in queued_notifications: - # await self.send(text_data=json.dumps(notification.message)) - # await self.mark_notification_as_sent(notification) + logger.error(f"Error in notifications connect: {e}") + await self.close(code=4000) async def disconnect(self, close_code): try: + logger.info(f"User {getattr(self, 'user', 'Unknown')} disconnecting from notifications WebSocket with code {close_code}") + + # Cancel heartbeat task + if self.heartbeat_task: + self.heartbeat_task.cancel() + await self.set_user_offline() - await self.channel_layer.group_discard( - self.group_name, - self.channel_name - ) + if hasattr(self, 'group_name'): + await self.channel_layer.group_discard( + self.group_name, + self.channel_name + ) + logger.info(f"User {getattr(self, 'user', 'Unknown')} successfully disconnected") except Exception as e: - print(f"Error in disconnect: {e}") + logger.error(f"Error in disconnect: {e}") async def receive(self, text_data): try: - pass + data = json.loads(text_data) + message_type = data.get('type') + + if message_type == 'heartbeat': + # Refresh user's online status + await self.set_user_online() + await self.send(text_data=json.dumps({'type': 'heartbeat_ack'})) + + elif message_type == 'check_friends_online': + # Future: implement friend online status check + await self.send(text_data=json.dumps({ + 'type': 'friends_online_status', + 'friends': {} + })) + + logger.debug(f"Received message from user {self.user.user_id}: {message_type}") except Exception as e: - print(f"Error in receive: {e}") + logger.error(f"Error in receive: {e}") async def send_notification(self, event): try: - await self.send(text_data=json.dumps(event['message'])) - print(f"Sent notification: {event['message']}") + message = event['message'] + + # Handle friend status updates specifically + if isinstance(message, dict) and message.get('type') == 'friend_status_update': + await self.send(text_data=json.dumps(message)) + logger.info(f"Sent friend status update: {message}") + else: + # Handle regular notifications + await self.send(text_data=json.dumps(message)) + logger.info(f"Sent notification: {message}") except Exception as e: - print(f"Error in send_notification: {e}") + logger.error(f"Error in send_notification: {e}") async def check_online(self, event): try: await self.send(text_data=json.dumps({'is_online': True})) except Exception as e: - print(f"Error in check_online: {e}") + logger.error(f"Error in check_online: {e}") + + async def friend_status_update(self, event): + """Handle friend coming online/offline - DEPRECATED: Use send_notification instead""" + try: + await self.send(text_data=json.dumps({ + 'type': 'friend_status_update', + 'user_id': event['user_id'], + 'is_online': event['is_online'], + 'timestamp': event.get('timestamp') + })) + logger.debug(f"Sent friend status update: user {event['user_id']} is {'online' if event['is_online'] else 'offline'}") + except Exception as e: + logger.error(f"Error in friend_status_update: {e}") @database_sync_to_async def set_user_online(self): try: - from .models import UserProfile - UserProfile.objects.filter(user_id=self.user.user_id).update(is_online=True) + online_status_service.set_user_online(self.user.user_id) + logger.info(f"User {self.user.user_id} set to online") except Exception as e: - print(f"Error in set_user_online: {e}") + logger.error(f"Error in set_user_online: {e}") + + async def heartbeat_monitor(self): + """Send heartbeat request every 30 seconds""" + while True: + try: + await asyncio.sleep(30) + await self.send(text_data=json.dumps({'type': 'heartbeat_request'})) + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Error in heartbeat monitor: {e}") + break @database_sync_to_async def set_user_offline(self): try: - from .models import UserProfile - UserProfile.objects.filter(user_id=self.user.user_id).update(is_online=False) + online_status_service.set_user_offline(self.user.user_id) + logger.info(f"User {self.user.user_id} set to offline") + except Exception as e: + logger.error(f"Error in set_user_offline: {e}") + + async def send_queued_notifications(self): + """Send all queued notifications to the user when they connect""" + try: + queued_notifications = await self.get_queued_notifications() + for notification in queued_notifications: + await self.send(text_data=json.dumps(notification.message)) + await self.mark_notification_as_sent(notification) + + if queued_notifications: + logger.info(f"Sent {len(queued_notifications)} queued notifications to user {self.user.user_id}") except Exception as e: - print(f"Error in set_user_offline: {e}") + logger.error(f"Error sending queued notifications: {e}") @database_sync_to_async def get_queued_notifications(self): try: from .models import QueuedNotification - return QueuedNotification.objects.filter(user_id=self.user.user_id, is_sent=False) + notifications = list(QueuedNotification.objects.filter( + user_id=self.user.user_id, + is_sent=False + ).order_by('created_at')) + logger.info(f"Retrieved {len(notifications)} queued notifications for user {self.user.user_id}") + return notifications except Exception as e: - print(f"Error in get_queued_notifications: {e}") + logger.error(f"Error in get_queued_notifications: {e}") return [] @database_sync_to_async @@ -85,8 +184,26 @@ def mark_notification_as_sent(self, notification): try: notification.is_sent = True notification.save() + logger.info(f"Marked notification {notification.id} as sent") except Exception as e: - print(f"Error in mark_notification_as_sent: {e}") + logger.error(f"Error in mark_notification_as_sent: {e}") + + async def log_redis_connection(self): + try: + # Test della connessione Redis + await self.channel_layer.group_add("test_connection", self.channel_name) + await self.channel_layer.group_discard("test_connection", self.channel_name) + + # Log con informazioni disponibili + if hasattr(self.channel_layer, 'hosts'): + logger.info(f"Redis connection info: {self.channel_layer.hosts}") + else: + config = getattr(self.channel_layer, 'config', {}) + logger.info(f"Redis layer config: {config}") + logger.info("Redis connection test successful") + except Exception as e: + logger.error(f"Error checking Redis connection: {str(e)}") + class GroupNotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_id = self.scope['url_route']['kwargs']['group_id'] @@ -144,4 +261,3 @@ async def check_online(self, event): await self.send(text_data=json.dumps({'is_online': True})) - \ No newline at end of file diff --git a/Back-End/Notifications/my_notifications/friend_status_sub.py b/Back-End/Notifications/my_notifications/friend_status_sub.py new file mode 100644 index 00000000..8a8ffc6f --- /dev/null +++ b/Back-End/Notifications/my_notifications/friend_status_sub.py @@ -0,0 +1,93 @@ +import redis +import json +import threading +import logging +from django.conf import settings +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync +import os + +logger = logging.getLogger('django') + +class FriendStatusListener: + def __init__(self): + self.redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=int(os.getenv('REDIS_ONLINE_STATUS_DB', '0')), + decode_responses=True + ) + self.pubsub = self.redis_client.pubsub() + self.running = False + self.listener_thread = None + self.channel_layer = get_channel_layer() + + def start_listener(self): + """Start listening for friend status updates""" + if not self.running: + self.running = True + self.listener_thread = threading.Thread( + target=self._listen_for_friend_updates, + daemon=True + ) + self.listener_thread.start() + logger.info("๐Ÿ”” Friend status listener started in notification service") + + def stop_listener(self): + """Stop the listener""" + self.running = False + if self.pubsub: + self.pubsub.close() + logger.info("๐Ÿ”• Friend status listener stopped") + + def _listen_for_friend_updates(self): + """Listen for friend status updates and send via WebSocket""" + try: + # Subscribe to all friend status channels + self.pubsub.psubscribe('friend_status_*') + logger.info("๐Ÿ“ก Listening for friend status updates to send via WebSocket") + + for message in self.pubsub.listen(): + if not self.running: + break + + if message['type'] == 'pmessage': + try: + # Extract user_id from channel name + channel = message['channel'] + user_id = channel.replace('friend_status_', '') + + # Parse the message data + data = json.loads(message['data']) + + # Send via WebSocket to the specific user + self._send_to_user_websocket(user_id, data) + + except Exception as e: + logger.error(f"Error processing friend status message: {e}") + + except Exception as e: + logger.error(f"Error in friend status listener: {e}") + finally: + self.pubsub.close() + + def _send_to_user_websocket(self, user_id, data): + """Send friend status update to user via WebSocket""" + try: + if self.channel_layer: + async_to_sync(self.channel_layer.group_send)( + f"user_notifications_{user_id}", + { + 'type': 'send_notification', + 'message': data + } + ) + logger.info(f"๐Ÿ“ค Sent friend status update to user {user_id} via WebSocket") + else: + logger.warning("Channel layer not available") + + except Exception as e: + logger.error(f"Error sending friend status to WebSocket: {e}") + +# Global instance +friend_status_listener = FriendStatusListener() \ No newline at end of file diff --git a/Back-End/Notifications/my_notifications/middleware.py b/Back-End/Notifications/my_notifications/middleware.py index aa70a64f..736c1cef 100644 --- a/Back-End/Notifications/my_notifications/middleware.py +++ b/Back-End/Notifications/my_notifications/middleware.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import AnonymousUser from channels.auth import AuthMiddlewareStack from django.core.cache import cache +from django.db import OperationalError, connections +from django.http import JsonResponse from rest_framework_simplejwt.tokens import AccessToken from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.permissions import BasePermission @@ -64,7 +66,7 @@ def get_user_from_token(token): # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) + user = UserProfile.objects.get(user_id=user_id) # Cache the user with a timeout (e.g., 5 minutes) cache.set(token, user, timeout=300) return user @@ -78,6 +80,7 @@ class JWTAuth(JWTAuthentication): """ Enhanced JWT authentication class with caching support and improved error handling. """ + user_id_claim = 'user_id' def authenticate(self, request): # Skip authentication for specific paths if needed if request.path.endswith('/chat/new_user/') and request.method == 'POST': @@ -117,10 +120,34 @@ def authenticate(self, request): except Exception as e: # Log the error but don't cache exceptions logger.warning(f"JWT authentication failed: {str(e)}") + logger.warning(f"Token details: {token[:10]}...{token[-10:]}") + try: + decoded = AccessToken(token) + user_id = decoded.get('user_id') + logger.warning(f"User ID from token: {user_id}, exists: {UserProfile.objects.filter(user_id=user_id).exists()}") + except Exception as inner_e: + logger.warning(f"Token decode failed: {str(inner_e)}") return None # No auth header or not a Bearer token return super().authenticate(request) + def get_user(self, validated_token): + """ + Sovrascrive il metodo get_user per utilizzare UserProfile invece del modello User standard + """ + try: + user_id = validated_token[self.user_id_claim] + + # Cerca l'utente nel modello UserProfile + user = UserProfile.objects.get(user_id=user_id) + + return user + except UserProfile.DoesNotExist: + logger.warning(f"User ID {user_id} from token not found in UserProfile") + return None + except Exception as e: + logger.error(f"Error getting user from token: {str(e)}") + return None class JWTAuthMiddleware(BaseMiddleware): """ @@ -176,7 +203,7 @@ def get_user_from_jwt(self, token): # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) + user = UserProfile.objects.get(user_id=user_id) # Cache the user with a timeout (e.g., 5 minutes) cache.set(f"jwt_ws_user_{token}", user, timeout=300) return user @@ -186,113 +213,56 @@ def get_user_from_jwt(self, token): logger.error(f"JWT token validation failed: {str(e)}") return AnonymousUser() +class DebugMiddleware(BaseMiddleware): + """ + Debug middleware to log WebSocket connection attempts and details + """ + async def __call__(self, scope, receive, send): + logger.info(f"=== WebSocket Debug Info ===") + logger.info(f"Path: {scope.get('path')}") + logger.info(f"Query string: {scope.get('query_string', b'').decode()}") + logger.info(f"Headers: {dict(scope.get('headers', []))}") + logger.info(f"User: {scope.get('user', 'Not set')}") + logger.info(f"User type: {type(scope.get('user', 'Not set'))}") + if hasattr(scope.get('user'), 'user_id'): + logger.info(f"User ID: {scope['user'].user_id}") + logger.info(f"=== End Debug Info ===") + + return await super().__call__(scope, receive, send) + def JWTAuthMiddlewareStack(inner): """ Helper function that returns a JWT auth middleware wrapped with AuthMiddlewareStack. """ return JWTAuthMiddleware(AuthMiddlewareStack(inner)) -# class TokenAuthMiddleware(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# query_string = parse_qs(scope["query_string"].decode()) -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) -# try: -# token = query_string.get("token") -# if token: -# scope["user"] = await get_user_from_token(token[0]) -# else: -# scope["user"] = AnonymousUser() -# except Exception as e: -# scope["user"] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# def TokenAuthMiddlewareStack(inner): -# return TokenAuthMiddleware(AuthMiddlewareStack(inner)) - -# class TokenAuthMiddlewareHTTP(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# headers = dict(scope['headers']) -# token = headers.get(b'authorization') -# if token: -# token = token.decode().replace("Bearer ", "") -# scope['user'] = await self.get_user_from_token(token) -# else: -# scope['user'] = AnonymousUser() - -# return await super().__call__(scope, receive, send) - -# @database_sync_to_async -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] - -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user - -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() +class HealthCheckMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Allow health check endpoint regardless of host + if request.path.endswith('/health'): # Liveness probe + return JsonResponse({'status': 'ok'}) + elif request.path.endswith('/ready'): # Readiness probe + db_ready = False + try: + # Check the default database connection + db_conn = connections['default'] + db_conn.cursor() # Attempt to get a cursor to check connectivity + # Optionally, you could execute a very simple query: + # with db_conn.cursor() as cursor: + # cursor.execute("SELECT 1") + # cursor.fetchone() + db_ready = True + except OperationalError: + logger.error("Readiness probe: Database connection failed.") + # db_ready remains False -# class TokenAuthPermission(BasePermission): -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] + if db_ready: + return JsonResponse({'status': 'ready', 'database': 'ok'}) + else: + return JsonResponse({'status': 'unready', 'database': 'unavailable'}, status=503) -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user - -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() - -# def has_permission(self, request, view): -# if request.path == '/chat/new_user/' and request.method == 'POST': -# request.user = AnonymousUser() -# return True -# token = request.META.get('HTTP_AUTHORIZATION') -# if token: -# token = token.replace("Bearer ", "") -# user = self.get_user_from_token(token) -# if user and not isinstance(user, AnonymousUser): -# request.user = user -# return True -# return False - -# from django.conf import settings -# from rest_framework.permissions import BasePermission - -# class APIKeyPermission(BasePermission): -# def has_permission(self, request, view): -# api_key = request.headers.get('X-API-KEY') -# if api_key: -# return api_key == settings.API_KEY -# return False \ No newline at end of file + return self.get_response(request) \ No newline at end of file diff --git a/Back-End/Notifications/my_notifications/models.py b/Back-End/Notifications/my_notifications/models.py index b5c631c3..9fcf4c85 100644 --- a/Back-End/Notifications/my_notifications/models.py +++ b/Back-End/Notifications/my_notifications/models.py @@ -6,7 +6,7 @@ class BaseNotification(models.Model): id = models.AutoField(primary_key=True) Sender = models.CharField(max_length=200, choices=[(key, key) for key in Microservices.keys()]) - message = models.TextField() + message = models.JSONField() is_sent = models.BooleanField(default=False) class Meta: abstract = True @@ -27,11 +27,11 @@ class SentNotification(models.Model): id = models.IntegerField(primary_key=True) user_id = models.IntegerField() group_id = models.IntegerField(null=True, blank=True) - message = models.TextField() + message = models.JSONField() is_sent = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f'Sent at {self.send_time} to {self.user_id} or {self.group_id}' + return f'Sent at {self.created_at} to {self.user_id} or {self.group_id}' class ImmediateNotification(UserNotification, GroupNotification): pass @@ -43,8 +43,7 @@ def save(self, *args, **kwargs): user_id=self.user_id, group_id=self.group_id, message=self.message, - is_read=self.is_read, - creation_time=self.creation_time + is_sent=True ) self.delete() else: @@ -60,8 +59,7 @@ def on_create(self): user_id=self.user_id, group_id=self.group_id, message=self.message, - is_read=self.is_read, - creation_time=self.creation_time + Sender=self.Sender ) self.delete() else: diff --git a/Back-End/Notifications/my_notifications/online_status.py b/Back-End/Notifications/my_notifications/online_status.py new file mode 100644 index 00000000..86e407da --- /dev/null +++ b/Back-End/Notifications/my_notifications/online_status.py @@ -0,0 +1,101 @@ +import json +import time +import redis +import os +import asyncio +from django.conf import settings +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync +import logging + +logger = logging.getLogger('django') + +class OnlineStatusService: + def __init__(self): + # Use shared Redis DB for online status + self.redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=int(os.getenv('REDIS_ONLINE_STATUS_DB', '0')), # Shared DB + decode_responses=True + ) + + def is_user_online(self, user_id): + """Fast Redis lookup for user online status""" + try: + return bool(self.redis_client.exists(f"online:{user_id}")) + except Exception: + return False + + def get_multiple_users_online_status(self, user_ids): + """Batch check online status for multiple users""" + try: + online_status = {} + if not user_ids: + return online_status + + pipe = self.redis_client.pipeline() + for user_id in user_ids: + pipe.exists(f"online:{user_id}") + results = pipe.execute() + + for user_id, is_online in zip(user_ids, results): + online_status[user_id] = bool(is_online) + return online_status + except Exception: + return {user_id: False for user_id in user_ids} + + def get_online_users_count(self): + """Get total count of online users""" + try: + return self.redis_client.scard("online_users") + except Exception: + return 0 + + def set_user_online(self, user_id): + """Set user as online in Redis""" + try: + # Check if user was already online + was_online = self.is_user_online(user_id) + + self.redis_client.setex(f"online:{user_id}", 60, "1") # Set with expiration + self.redis_client.sadd("online_users", user_id) + + # Only publish global status change (no friend logic here) + if not was_online: + self.publish_global_status_change(user_id, True) + except Exception as e: + logger.error(f"Error setting user {user_id} online: {e}") + + def set_user_offline(self, user_id): + """Set user as offline in Redis""" + try: + # Check if user was online + was_online = self.is_user_online(user_id) + + self.redis_client.delete(f"online:{user_id}") + self.redis_client.srem("online_users", user_id) + + # Only publish global status change (no friend logic here) + if was_online: + self.publish_global_status_change(user_id, False) + except Exception as e: + logger.error(f"Error setting user {user_id} offline: {e}") + + def publish_global_status_change(self, user_id, is_online): + """Publish global user status change - no friend logic here""" + try: + self.redis_client.publish( + 'user_status_global', + json.dumps({ + 'user_id': user_id, + 'is_online': is_online, + 'timestamp': time.time() + }) + ) + logger.info(f"๐Ÿ“ก Published global status change for user {user_id}: {'online' if is_online else 'offline'}") + except Exception as e: + logger.error(f"Error publishing global status change: {e}") + +# Create a singleton instance +online_status_service = OnlineStatusService() \ No newline at end of file diff --git a/Back-End/Notifications/my_notifications/tests.py b/Back-End/Notifications/my_notifications/tests.py index 7ce503c2..efc786d8 100644 --- a/Back-End/Notifications/my_notifications/tests.py +++ b/Back-End/Notifications/my_notifications/tests.py @@ -1,3 +1,22 @@ -from django.test import TestCase -# Create your tests here. +from django.test import TestCase, Client + +class MyNotificationsURLTests(TestCase): + def setUp(self): + self.client = Client() + + def test_add_user_to_group_endpoint(self): + response = self.client.get('/notification/groups/1/add_user/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_add_user_endpoint(self): + response = self.client.get('/notification/add_user') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_notification_history_endpoint(self): + response = self.client.get('/notification/notification_history') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_new_notification_endpoint(self): + response = self.client.get('/notification/new/') + self.assertIn(response.status_code, [200, 401, 403]) diff --git a/Back-End/Notifications/my_notifications/views.py b/Back-End/Notifications/my_notifications/views.py index 9bea41b5..2ba1491a 100644 --- a/Back-End/Notifications/my_notifications/views.py +++ b/Back-End/Notifications/my_notifications/views.py @@ -34,39 +34,49 @@ async def send_user_notification(user_id, notification): serialized_notification = UniversalNotificationSerializer(notification, model=model).data print(f'Sending notification to user {user_id}') - - try: - await channel_layer.group_send( - f'user_notifications_{user_id}', - { - 'type': 'send_notification', - 'message': serialized_notification, - } - ) - print(f"Notification sent to user {user_id}") - - # Creazione della SentNotification - await sync_to_async(SentNotification.objects.create)( - id=notification.id, - user_id=user_id, - group_id=None, - message=notification.message, - is_sent=True - ) - - # Cancellazione della vecchia ImmediateNotification - await sync_to_async(notification.delete)() - return True - except Exception as e: - print(f"Error sending notification: {e}") - - # Creazione della QueuedNotification + + # Check if user is online + from .online_status import online_status_service + is_user_online = online_status_service.is_user_online(user_id) + + if is_user_online: + try: + await channel_layer.group_send( + f'user_notifications_{user_id}', + { + 'type': 'send_notification', + 'message': serialized_notification, + } + ) + print(f"Notification sent to user {user_id}") + + # Creazione della SentNotification + await sync_to_async(SentNotification.objects.create)( + id=notification.id, + user_id=user_id, + group_id=None, + message=notification.message, + is_sent=True + ) + + # Cancellazione della vecchia ImmediateNotification + await sync_to_async(notification.delete)() + return True + except Exception as e: + print(f"Error sending notification: {e}") + is_user_online = False # Treat as offline if send fails + + if not is_user_online: + print(f"User {user_id} is offline, queuing notification") + # Create QueuedNotification await sync_to_async(QueuedNotification.objects.create)( user_id=user_id, group_id=notification.group_id, message=serialized_notification, is_sent=False ) + # Delete the original notification + await sync_to_async(notification.delete)() return False class NewUser(generics.CreateAPIView): diff --git a/Back-End/Notifications/requirements.txt b/Back-End/Notifications/requirements.txt index 8ad2ef9f..756d3633 100644 --- a/Back-End/Notifications/requirements.txt +++ b/Back-End/Notifications/requirements.txt @@ -3,19 +3,19 @@ asgiref==3.8.1 async-timeout==5.0.1 billiard==4.2.1 celery==5.4.0 -certifi==2024.8.30 +certifi==2025.1.31 cffi==1.17.1 channels-redis==4.2.1 -charset-normalizer==3.4.0 -click==8.1.7 +charset-normalizer==3.4.1 +click==8.1.8 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 -cryptography==44.0.0 +cryptography==44.0.2 daphne==4.1.2 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 +django==5.2.4 +django-cors-headers==4.7.0 +django-filter==25.1 django-oauth-toolkit==3.0.1 django-redis==5.4.0 idna==3.10 @@ -23,20 +23,21 @@ jwcrypto==1.5.6 kombu==5.4.2 oauthlib==3.2.2 prompt_toolkit==3.0.48 -psycopg==3.2.5 -psycopg-binary==3.2.5 +psycopg==3.2.9 +psycopg-binary==3.2.9 pycparser==2.22 python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 -six==1.16.0 -sqlparse==0.5.2 +redis==5.2.1 +requests>=2.32.4 +six==1.17.0 +sqlparse==0.5.3 typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 +tzdata==2025.1 +urllib3>=2.5.0 vine==5.1.0 wcwidth==0.2.13 -djangorestframework-simplejwt==5.4.0 +djangorestframework-simplejwt==5.5.0 # via -r Back-End/requirements.in -drf-yasg==1.21.8 - # via -r Back-End/requirements.in \ No newline at end of file +drf-yasg==1.21.9 +django-prometheus==2.3.1 +prometheus-client==0.21.1 diff --git a/Back-End/api-docs/Dockerfile b/Back-End/api-docs/Dockerfile new file mode 100644 index 00000000..fb171d34 --- /dev/null +++ b/Back-End/api-docs/Dockerfile @@ -0,0 +1,36 @@ +# Use a stable Python version +FROM python:3.13-alpine AS base + +RUN apk add --no-cache postgresql-libs bash + +FROM base AS builder + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +FROM base + +COPY --from=builder /usr/local /usr/local + +COPY . /app +WORKDIR /app + +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static + +COPY ./entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Create non-root user +RUN adduser -D -s /bin/bash docs +RUN chown -R docs:docs /app +USER docs + +EXPOSE 8005 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8005/health/', timeout=5)" || exit 1 + +CMD [ "/bin/bash","/entrypoint.sh" ] diff --git a/Back-End/Dockers/loginservice/login/login/__init__.py b/Back-End/api-docs/api_docs/__init__.py similarity index 100% rename from Back-End/Dockers/loginservice/login/login/__init__.py rename to Back-End/api-docs/api_docs/__init__.py diff --git a/Back-End/api-docs/api_docs/settings.py b/Back-End/api-docs/api_docs/settings.py new file mode 100644 index 00000000..2c86e7d9 --- /dev/null +++ b/Back-End/api-docs/api_docs/settings.py @@ -0,0 +1,315 @@ +""" +Django settings for API Documentation service +""" + +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv('SECRET_KEY', 'api-docs-secret-key-change-in-production') + +DEBUG = os.getenv('DEBUG', 'True').lower() == 'true' + +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*').split(',') if os.getenv('ALLOWED_HOSTS', '*') != '*' else ['*'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'drf_yasg', + 'corsheaders', + 'docs', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'api_docs.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'api_docs.wsgi.application' + + +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'apidocs_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5439'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } + +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', +} + +# CORS settings +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True + +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +# Microservices endpoints +MICROSERVICES = { + 'login': os.getenv('MICROSERVICES_LOGIN', 'http://login:8000'), + 'chat': os.getenv('MICROSERVICES_CHAT', 'http://chat:8001'), + 'user': os.getenv('MICROSERVICES_USER', 'http://user:8002'), + 'notifications': os.getenv('MICROSERVICES_NOTIFICATIONS', 'http://notifications:8003'), + 'pong': os.getenv('MICROSERVICES_PONG', 'http://pong:8004'), +} + +# For local development, use localhost +if DEBUG: + MICROSERVICES = { + 'login': os.getenv('MICROSERVICES_LOGIN', 'http://localhost:8000'), + 'chat': os.getenv('MICROSERVICES_CHAT', 'http://localhost:8001'), + 'user': os.getenv('MICROSERVICES_USER', 'http://localhost:8002'), + 'notifications': os.getenv('MICROSERVICES_NOTIFICATIONS', 'http://localhost:8003'), + 'pong': os.getenv('MICROSERVICES_PONG', 'http://localhost:8004'), + } + +# ELK-Optimized Logging Configuration for api-docs service +# This configuration provides standardized logging for the ELK Stack + +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "api-docs"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'api-docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_api-docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/api-docs/api_docs/urls.py b/Back-End/api-docs/api_docs/urls.py new file mode 100644 index 00000000..8f1c60a6 --- /dev/null +++ b/Back-End/api-docs/api_docs/urls.py @@ -0,0 +1,16 @@ +""" +URL configuration for API Documentation aggregator +""" +from django.contrib import admin +from django.urls import path, include +from django.http import JsonResponse + +def health_check(request): + """Simple health check endpoint""" + return JsonResponse({'status': 'healthy'}) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('health/', health_check, name='health'), + path('', include('docs.urls')), +] diff --git a/Back-End/api-docs/api_docs/wsgi.py b/Back-End/api-docs/api_docs/wsgi.py new file mode 100644 index 00000000..dbc76417 --- /dev/null +++ b/Back-End/api-docs/api_docs/wsgi.py @@ -0,0 +1,10 @@ +""" +WSGI config for api_docs project. +""" + +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_docs.settings') + +application = get_wsgi_application() diff --git a/Back-End/api-docs/docs/__init__.py b/Back-End/api-docs/docs/__init__.py new file mode 100644 index 00000000..df668c9d --- /dev/null +++ b/Back-End/api-docs/docs/__init__.py @@ -0,0 +1,3 @@ +""" +Documentation aggregator app +""" diff --git a/Back-End/Dockers/notification_service/Notifications/my_notifications/apps.py b/Back-End/api-docs/docs/apps.py similarity index 57% rename from Back-End/Dockers/notification_service/Notifications/my_notifications/apps.py rename to Back-End/api-docs/docs/apps.py index e2ce7523..a5939977 100644 --- a/Back-End/Dockers/notification_service/Notifications/my_notifications/apps.py +++ b/Back-End/api-docs/docs/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class MyNotificationsConfig(AppConfig): +class DocsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'my_notifications' + name = 'docs' diff --git a/Back-End/api-docs/docs/templates/docs/index.html b/Back-End/api-docs/docs/templates/docs/index.html new file mode 100644 index 00000000..662efbf3 --- /dev/null +++ b/Back-End/api-docs/docs/templates/docs/index.html @@ -0,0 +1,162 @@ + + + + + + Trascendence API Documentation + + + +
+

๐Ÿš€ Trascendence API Documentation

+ +
+ {% for service in services %} +
+

{{ service|title }} Service

+
+ + Checking... +
+

API documentation for the {{ service }} microservice

+ View Documentation +
+ {% endfor %} +
+ +
+

๐Ÿ“š Combined Documentation

+

View all microservices APIs in a single unified documentation

+ View Combined Docs +
+
+ + + + diff --git a/Back-End/api-docs/docs/templates/docs/swagger.html b/Back-End/api-docs/docs/templates/docs/swagger.html new file mode 100644 index 00000000..620ecd75 --- /dev/null +++ b/Back-End/api-docs/docs/templates/docs/swagger.html @@ -0,0 +1,90 @@ + + + + + + {{ service_name|title }} API Documentation + + + + + โ† Back to Services + +
+ + + + + + diff --git a/Back-End/api-docs/docs/urls.py b/Back-End/api-docs/docs/urls.py new file mode 100644 index 00000000..72427d39 --- /dev/null +++ b/Back-End/api-docs/docs/urls.py @@ -0,0 +1,18 @@ +""" +URLs for documentation app +""" +from django.urls import path, re_path +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('status/', views.service_status, name='service_status'), + path('docs//', views.service_docs, name='service_docs'), + path('api//swagger.json', views.swagger_json, name='swagger_json'), + path('api/combined/swagger.json', views.combined_docs, name='combined_docs'), + path('docs/combined/', views.service_docs, {'service_name': 'combined'}, name='combined_docs_ui'), + + # Proxy endpoints for API testing + re_path(r'^proxy/(?P\w+)/(?P.*)$', views.proxy_api, name='proxy_api'), + path('proxy//', views.proxy_api, {'path': ''}, name='proxy_api_root'), +] diff --git a/Back-End/api-docs/docs/views.py b/Back-End/api-docs/docs/views.py new file mode 100644 index 00000000..8148ed2d --- /dev/null +++ b/Back-End/api-docs/docs/views.py @@ -0,0 +1,196 @@ +""" +Views for API documentation aggregator +""" +import requests +import json +from django.shortcuts import render +from django.http import JsonResponse, HttpResponse +from django.conf import settings +from rest_framework.decorators import api_view +from rest_framework.response import Response + + +def index(request): + """ + Homepage showing all available API documentation + """ + services = list(settings.MICROSERVICES.keys()) + return render(request, 'docs/index.html', {'services': services}) + + +def service_docs(request, service_name): + """ + Display Swagger UI for a specific service + """ + if service_name not in settings.MICROSERVICES: + return HttpResponse('Service not found', status=404) + + service_url = settings.MICROSERVICES[service_name] + return render(request, 'docs/swagger.html', { + 'service_name': service_name, + 'service_url': service_url, + 'swagger_json_url': f'/api/{service_name}/swagger.json' + }) + + +@api_view(['GET']) +def swagger_json(request, service_name): + """ + Proxy swagger.json from microservice + """ + if service_name not in settings.MICROSERVICES: + return Response({'error': 'Service not found'}, status=404) + + service_url = settings.MICROSERVICES[service_name] + + try: + # Try multiple common swagger endpoints + swagger_endpoints = [ + f'{service_url}/swagger.json', + f'{service_url}/api/swagger.json', + f'{service_url}/swagger/?format=openapi', + f'{service_url}/api/schema/', + f'{service_url}/docs/?format=openapi' + ] + + for endpoint in swagger_endpoints: + try: + response = requests.get(endpoint, timeout=5) + if response.status_code == 200: + swagger_data = response.json() + + # Update the host and schemes in swagger data + if 'host' in swagger_data: + swagger_data['host'] = request.get_host() + if 'schemes' in swagger_data: + swagger_data['schemes'] = ['https' if request.is_secure() else 'http'] + + # Update server URLs for OpenAPI 3.0 + if 'servers' in swagger_data: + base_url = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}" + swagger_data['servers'] = [{'url': f"{base_url}/proxy/{service_name}"}] + + return Response(swagger_data) + except (requests.RequestException, ValueError): + continue + + return Response({ + 'error': 'Swagger documentation not available', + 'message': f'Tried endpoints: {swagger_endpoints}' + }, status=404) + + except Exception as e: + return Response({'error': f'Service unavailable: {str(e)}'}, status=503) + + +@api_view(['GET']) +def combined_docs(request): + """ + Return combined API documentation for all services + """ + combined_swagger = { + "swagger": "2.0", + "info": { + "title": "Trascendence API Documentation", + "description": "Combined API documentation for all microservices", + "version": "1.0.0" + }, + "host": request.get_host(), + "schemes": ["https" if request.is_secure() else "http"], + "basePath": "/", + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": {}, + "definitions": {} + } + + for service_name, service_url in settings.MICROSERVICES.items(): + try: + response = requests.get(f'{service_url}/swagger.json', timeout=5) + if response.status_code == 200: + service_swagger = response.json() + + # Add service prefix to paths + for path, methods in service_swagger.get('paths', {}).items(): + prefixed_path = f'/api/{service_name}{path}' + combined_swagger['paths'][prefixed_path] = methods + + # Merge definitions + for def_name, definition in service_swagger.get('definitions', {}).items(): + combined_swagger['definitions'][f'{service_name}_{def_name}'] = definition + + except requests.RequestException: + # Service is unavailable, skip it + continue + + return Response(combined_swagger) + + +@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) +def proxy_api(request, service_name, path=''): + """ + Proxy API requests to the appropriate microservice + This allows testing APIs directly from the Swagger UI + """ + if service_name not in settings.MICROSERVICES: + return Response({'error': 'Service not found'}, status=404) + + service_url = settings.MICROSERVICES[service_name] + target_url = f"{service_url}/{path.lstrip('/')}" + + try: + # Forward the request with the same method, headers, and data + headers = {} + for key, value in request.META.items(): + if key.startswith('HTTP_'): + # Convert Django META headers back to normal headers + header_name = key[5:].replace('_', '-').title() + if header_name not in ['Host', 'Content-Length']: + headers[header_name] = value + + # Add content type if present + if hasattr(request, 'content_type') and request.content_type: + headers['Content-Type'] = request.content_type + + response = requests.request( + method=request.method, + url=target_url, + headers=headers, + params=request.GET, + data=request.body if request.method in ['POST', 'PUT', 'PATCH'] else None, + timeout=30 + ) + + # Return the response from the microservice + return HttpResponse( + response.content, + status=response.status_code, + content_type=response.headers.get('content-type', 'application/json') + ) + + except requests.RequestException as e: + return Response({'error': f'Service unavailable: {str(e)}'}, status=503) + + +def service_status(request): + """ + Check status of all microservices + """ + services_status = {} + + for service_name, service_url in settings.MICROSERVICES.items(): + try: + response = requests.get(f'{service_url}/health/', timeout=5) + services_status[service_name] = { + 'status': 'healthy' if response.status_code == 200 else 'unhealthy', + 'url': service_url, + 'response_time': response.elapsed.total_seconds() + } + except requests.RequestException: + services_status[service_name] = { + 'status': 'unreachable', + 'url': service_url, + 'response_time': None + } + + return JsonResponse({'services': services_status}) diff --git a/Back-End/api-docs/entrypoint.sh b/Back-End/api-docs/entrypoint.sh new file mode 100644 index 00000000..582763ba --- /dev/null +++ b/Back-End/api-docs/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +set -e + +# collect static files +python manage.py collectstatic --noinput + +# migrations at startup (for the lightweight SQLite DB) +python manage.py migrate --noinput + +# For development +if [ "$DEBUG" = "True" ]; then + python manage.py runserver 0.0.0.0:8005 +else + # For production use Gunicorn (WSGI server) + gunicorn --bind 0.0.0.0:8005 api_docs.wsgi:application \ + --log-level=info \ + --capture-output \ + --access-logfile - \ + --error-logfile - || { echo "Gunicorn failed to start"; exit 1; } +fi diff --git a/Back-End/api-docs/manage.py b/Back-End/api-docs/manage.py new file mode 100644 index 00000000..f669f474 --- /dev/null +++ b/Back-End/api-docs/manage.py @@ -0,0 +1,10 @@ +""" +API Documentation Aggregator +Collects and displays Swagger docs from all microservices +""" +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_docs.settings') + +application = get_wsgi_application() diff --git a/Back-End/api-docs/requirements.txt b/Back-End/api-docs/requirements.txt new file mode 100644 index 00000000..fcc9d854 --- /dev/null +++ b/Back-End/api-docs/requirements.txt @@ -0,0 +1,8 @@ +django==5.2.4 +djangorestframework==3.14.0 +drf-yasg==1.21.7 +django-cors-headers==4.3.1 +requests==2.32.4 +pyyaml==6.0.1 +gunicorn==23.0.0 +setuptools>=65.0.0 diff --git a/Back-End/build_push.sh b/Back-End/build_push.sh new file mode 100755 index 00000000..0d4b1fd7 --- /dev/null +++ b/Back-End/build_push.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Script to build and push Docker images +set -e +# Build the Docker image +USERNAME=bombatomica + +docker build -t $USERNAME/user:latest ./task_user/ +docker build -t $USERNAME/login:latest ./login/ +docker build -t $USERNAME/chat:latest ./chat/ +docker build -t $USERNAME/notifications:latest ./Notifications/ +docker build -t $USERNAME/pong:latest ./pongProject/ + +docker push $USERNAME/user:latest +docker push $USERNAME/login:latest +docker push $USERNAME/chat:latest +docker push $USERNAME/notifications:latest +docker push $USERNAME/pong:latest diff --git a/Back-End/chat/Dockerfile b/Back-End/chat/Dockerfile index e6a2350b..f1153dc9 100644 --- a/Back-End/chat/Dockerfile +++ b/Back-End/chat/Dockerfile @@ -1,5 +1,5 @@ # Use a stable Python version -FROM python:3.11-alpine AS base +FROM python:3.13-alpine AS base RUN apk add --no-cache postgresql-libs bash @@ -16,6 +16,10 @@ COPY --from=builder /usr/local /usr/local COPY . /app WORKDIR /app +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static +RUN mkdir -p /app/media + COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/Back-End/chat/chat/asgi.py b/Back-End/chat/chat/asgi.py index 7343ddb6..2ee56c4a 100644 --- a/Back-End/chat/chat/asgi.py +++ b/Back-End/chat/chat/asgi.py @@ -1,26 +1,39 @@ import os, django +from pathlib import Path os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.security.websocket import AllowedHostsOriginValidator from django.urls import path +from django.conf import settings +from chat.settings import BASE_DIR + django.setup() +BASE_DIR = Path('/app') + from my_chat.middleware import JWTAuthMiddleware -from my_chat import consumers -from .routing import websocket_urlpatterns +from my_chat import routing +# import shutil +import logging + + +logger = logging.getLogger('django') +print("ASGI APPLICATION LOADED") -print("ASGI APPLICATION LOADED") +from my_chat.middleware import DebugMiddleware application = ProtocolTypeRouter({ - "http": get_asgi_application(), - "websocket": AllowedHostsOriginValidator( - JWTAuthMiddleware( - URLRouter(websocket_urlpatterns) - ) - ), + "http": get_asgi_application(), + "websocket": AllowedHostsOriginValidator( + ( + JWTAuthMiddleware( + URLRouter(routing.websocket_urlpatterns) + ) + ) + ), }) #path('ws/chat//', consumers.ChatConsumer.as_asgi()), diff --git a/Back-End/chat/chat/routing.py b/Back-End/chat/chat/routing.py deleted file mode 100644 index fa91f5c7..00000000 --- a/Back-End/chat/chat/routing.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import re_path -from my_chat import consumers - -websocket_urlpatterns = [ - re_path(r'ws/chat/(?P\d+)/$', consumers.ChatConsumer.as_asgi()), -] \ No newline at end of file diff --git a/Back-End/chat/chat/settings.py b/Back-End/chat/chat/settings.py index 5e3cebf3..12f08852 100644 --- a/Back-End/chat/chat/settings.py +++ b/Back-End/chat/chat/settings.py @@ -10,17 +10,13 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path import secrets , os -# Aggiungi queste impostazioni -FORCE_SCRIPT_NAME = '/api/chat' - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -30,115 +26,246 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', True) -ALLOWED_HOSTS = ['*'] +# Simple microservices definition +Microservices = { + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), +} + +K8S_ALLOWED_HOSTS = os.environ.get('K8S_ALLOWED_HOSTS', '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16').split(',') + +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN +ALLOWED_HOSTS = ['*'] +# 'localhost', +# 'localhost:3000', +# '127.0.0.1', +# '[::1]', +# 'trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS + +# CORS_ALLOWED_ORIGINS = [ +# 'http://localhost:3000', +# 'http://localhost', +# 'http://127.0.0.1', +# 'http://[::1]', +# 'https://trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS_WITH_SCHEME +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True + +CSRF_TRUSTED_ORIGINS = [ + 'http://localhost:3000', + 'http://localhost', + 'http://127.0.0.1', + 'http://[::1]', + 'https://trascendence.42firenze.it', + # Aggiungi IP privati per HTTPS + 'https://10.0.2.15', # L'IP specifico della tua VM + # O meglio ancora, patterns per tutti gli IP privati: + 'http://10.*', + 'https://10.*', + 'http://192.168.*', + 'https://192.168.*', + 'http://172.16.*', + 'https://172.16.*', + 'http://172.17.*', + 'https://172.17.*', + 'http://172.18.*', + 'https://172.18.*', + # Continua per tutti i range 172.16.x.x fino a 172.31.x.x + 'http://localhost:8443', + 'http://127.0.0.1:8443', + 'http://10.0.2.15:8443', + 'http://10.0.2.15.xip.io:8443', + 'http://10.11.*.xip.io:8443', + Microservices['Login'], + Microservices['Chat'], + Microservices['Users'], + Microservices['Notifications'], + Microservices['Pong'], +] + K8S_SERVICE_HOSTS_WITH_SCHEME + +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] # Application definition INSTALLED_APPS = [ - 'daphne', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'rest_framework_simplejwt', - 'my_chat', - 'oauth2_provider', - 'django_filters', - 'corsheaders', - 'channels', - 'celery', - 'redis', - 'drf_yasg', + 'django_prometheus', + 'daphne', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'my_chat', + 'oauth2_provider', + 'django_filters', + 'corsheaders', + 'channels', + 'celery', + 'redis', + 'drf_yasg', ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - # 'my_chat.middleware.TokenAuthMiddlewareHTTP', + 'django_prometheus.middleware.PrometheusBeforeMiddleware', # Add at the top + 'my_chat.middleware.HealthCheckMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'my_chat.middleware.ErrorLoggingMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', # Add at the end ] - # 'oauth2_provider.middleware.OAuth2TokenMiddleware', + # 'oauth2_provider.middleware.OAuth2TokenMiddleware', ROOT_URLCONF = 'chat.urls' TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] -WSGI_APPLICATION = 'chat.wsgi.application' +# WSGI_APPLICATION = 'chat.wsgi.application' ASGI_APPLICATION = 'chat.asgi.application' oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': os.getenv('SERVICE_PASSWORD', '123'), -} - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'chat_db'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5436'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), - } + 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', + 'CLIENT_ID': secrets.token_urlsafe(32), + 'CLIENT_SECRET': secrets.token_urlsafe(64), + 'TOKEN': '', + 'REFRESH_TOKEN': '', + 'EXPIRES': '', + 'token_type': '', + 'scope': '', + 'SERVICE_PASSWORD': os.getenv('SERVICE_PASSWORD', '123'), } +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'chat_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5438'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, ] - # BUFET_URL = os.getenv('bufet_url', 'http://localhost:8003/task/bufet') # Internationalization @@ -152,11 +279,27 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ +# Update your static files configuration STATIC_URL = '/static/' -STATIC_ROOT = '/home/lollo/Documents/challenge_fides/Back-End/login/staticfiles' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] + +# Media files (user uploads) +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# File upload settings +FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB +DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB + +# Security settings for file uploads +FILE_UPLOAD_PERMISSIONS = 0o644 +FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -164,118 +307,244 @@ CORS_ORIGIN_ALLOW_ALL = True -REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') -REDIS_PORT = os.getenv('REDIS_PORT', '6700') -REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '0') -REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '1') +REDIS_HOST = os.getenv('REDIS_HOST', 'my-umbrella-redis-chart-service.redis-namespace.svc.cluster.local') +REDIS_PORT = os.getenv('REDIS_PORT', '6379') +REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '0') # Chat: DB 0 +REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '8') # Chat: Channel DB 8 CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'KEY_PREFIX': 'chat:', # Service prefix for data isolation + } + } } - - CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], - 'prefix': 'chat', - }, - } + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], + 'prefix': 'chat', # Channel prefix + }, + } } - REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'my_chat.middleware.JWTAuth', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'my_chat.Permissions.IsAuthenticated', - ], - 'DEFAULT_FILTER_BACKENDS': [ - 'django_filters.rest_framework.DjangoFilterBackend', - ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'my_chat.middleware.JWTAuth', + ], + 'DEFAULT_FILTER_BACKENDS': [ + 'django_filters.rest_framework.DjangoFilterBackend', + ], } -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'loki': { - 'class': 'logging.StreamHandler', - 'formatter': 'detailed', - 'stream': 'ext://sys.stdout', # Sends logs to stdout for Loki - }, - }, - 'loggers': { - 'django': { - 'handlers': ['loki'], - 'level': 'INFO', - 'propagate': True, - }, - }, - 'formatters': { - 'detailed': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', - }, - }, -} +# Get environment specifics +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') -OAUTH2_APP_NAME = 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S') -Microservices = { - 'Login': os.getenv('LOGIN_SERVICE', 'http://localhost:8000'), - 'Chat': os.getenv('CHAT_SERVICE', 'http://localhost:8001'), - 'Users': os.getenv('USERS_SERVICE', 'http://localhost:8002'), - 'Notifications': os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003'), - 'Personal' : "Self", -} +# Environment-based logging configuration -# filepath: /home/lollo/Documents/Fides/Back-End/chat/chat/settings.py - -SWAGGER_SETTINGS = { - 'USE_SESSION_AUTH': False, - 'SECURITY_DEFINITIONS': { - 'Your App API - Swagger': { - 'type': 'oauth2', - 'authorizationUrl': f"{Microservices['Login']}/o/authorize", - 'tokenUrl': f"{Microservices['Login']}/o/token/", - 'flow': 'accessCode', - 'scopes': { - 'read:groups': 'read groups', - } - } - }, - 'OAUTH2_CONFIG': { - 'clientId': f'{oauth2_settings["CLIENT_ID"]}', - 'clientSecret': f'{oauth2_settings["CLIENT_SECRET"]}', - 'appName': f'{OAUTH2_APP_NAME}', - }, -} +# Logging configuration optimized for ELK Stack -ADMIN = { - 'username': os.getenv('ADMIN_USERNAME', 'admin'), - 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), - 'password': os.getenv('ADMIN_PASSWORD', 'admin'), -} +# ELK-Optimized Logging Configuration for chat service +# This configuration provides standardized logging for the ELK Stack -Microservices = { - 'Login': os.getenv('LOGIN_SERVICE', 'http://localhost:8000'), - 'Chat': os.getenv('CHAT_SERVICE', 'http://localhost:8001'), - 'Users': os.getenv('USERS_SERVICE', 'http://localhost:8002'), - 'Notifications': os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003'), - 'Pong': os.getenv('PONG_SERVICE', 'http://localhost:8004'), - 'Personal' : "Self", -} +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None -CSRF_LOGIN_URL = Microservices['Login'] + '/login/get_csrf_token' -REGISTER_URL = Microservices['Login'] + '/login/Serviceregister' -# OAUTH2_REDIRECT_URL = f"{Microservices['Login']}/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html" -# OAUTH2_REDIRECT_URL = 'http://localhost:8001/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html' +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "chat"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'chat': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_chat': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/chat/chat/urls.py b/Back-End/chat/chat/urls.py index 6dd872f1..683af49a 100644 --- a/Back-End/chat/chat/urls.py +++ b/Back-End/chat/chat/urls.py @@ -15,31 +15,137 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path , include, re_path +from django.urls import path, include, re_path +from django.conf import settings +from django.conf.urls.static import static from rest_framework import permissions from drf_yasg.views import get_schema_view from drf_yasg import openapi -from my_chat.authentications import TokenAuthentication +from django.http import HttpResponse +# Schema view for JSON schema_view = get_schema_view( - openapi.Info( - title="Snippets API for Chat", - default_version='v1', - description="Chat API", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="contact@snippets.local"), - license=openapi.License(name="BSD License"), - ), - public=True, - permission_classes=(permissions.AllowAny,), - authentication_classes=[], + openapi.Info( + title="Snippets API for Chat", + default_version='v1', + description="Chat API", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), + authentication_classes=[], ) +# Custom view that serves Swagger UI with JWT authentication +def custom_swagger_view(request): + html = """ + + + Chat API Documentation + + + + + + +
+ + + + +""" + return HttpResponse(html) + urlpatterns = [ - path('admin/', admin.site.urls), - path('chat/', include('my_chat.urls')), - path('swagger/', schema_view.without_ui(cache_timeout=0), name='schema-json'), - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('admin/', admin.site.urls), + path('chat/', include('my_chat.urls')), + path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', custom_swagger_view, name='schema-swagger-ui'), # Use custom view + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('', include('django_prometheus.urls')), # Add metrics endpoint ] + +# Serve media files during development +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/Back-End/chat/entrypoint.sh b/Back-End/chat/entrypoint.sh index a63f1fd8..7af545f0 100644 --- a/Back-End/chat/entrypoint.sh +++ b/Back-End/chat/entrypoint.sh @@ -2,6 +2,9 @@ export DJANGO_SETTINGS_MODULE=chat.settings +# collect static files +python manage.py collectstatic --noinput + # Run migrations at startup python manage.py makemigrations python manage.py makemigrations my_chat @@ -12,5 +15,5 @@ if [ "$DEBUG" = True ]; then python manage.py runserver 0.0.0.0:8000 || { echo "Runserver failed to start"; exit 1; } else # For production use Daphne (ASGI server) - /usr/local/bin/daphne -b 0.0.0.0 -p 8000 chat.asgi:application || { echo "Daphne failed to start"; exit 1; } + /usr/local/bin/daphne -v 3 --access-log - -b 0.0.0.0 -p 8000 chat.asgi:application || { echo "Daphne failed to start"; exit 1; } fi \ No newline at end of file diff --git a/Back-End/chat/manage.py b/Back-End/chat/manage.py old mode 100755 new mode 100644 diff --git a/Back-End/chat/my_chat/Permissions.py b/Back-End/chat/my_chat/Permissions.py index c32ea61e..1331d280 100644 --- a/Back-End/chat/my_chat/Permissions.py +++ b/Back-End/chat/my_chat/Permissions.py @@ -2,88 +2,82 @@ from rest_framework.permissions import BasePermission from django.core.cache import cache from .models import ChatRoom, UserProfile, ChatMember +from django.contrib.auth.models import AnonymousUser logger = logging.getLogger('django') -class IsAuthenticated(BasePermission): - """ - Basic permission that requires the user to be authenticated. - """ - def has_permission(self, request, view): - return request.user and request.user.is_authenticated - class IsChatMember(BasePermission): - """ - Permission that checks if the user is a member of the requested chat. - Uses caching to reduce database queries. - """ - def has_permission(self, request, view): - if not request.user or not request.user.is_authenticated: - return False - - # For list endpoints or actions not tied to a specific chat - if getattr(view, 'action', None) in ['list', 'create']: - return True - - room_id = view.kwargs.get('room_id') or request.data.get('room_id') or request.query_params.get('room_id') - if not room_id: - return False - - # Check cache first - cache_key = f"chat_membership_{request.user.id}_{room_id}" - is_member = cache.get(cache_key) - - if is_member is not None: - return is_member - - # Check database - try: - is_member = ChatMember.objects.filter( - user_id=request.user.id, - chat_room_id=room_id, - is_active=True - ).exists() - - # Cache the result for 5 minutes - cache.set(cache_key, is_member, timeout=300) - return is_member - except Exception as e: - logger.error(f"Error checking chat membership: {str(e)}") - return False - - def has_object_permission(self, request, view, obj): - # For objects with a direct reference to a chat room - if hasattr(obj, 'chat_room_id'): - room_id = obj.chat_room_id - elif hasattr(obj, 'room_id'): - room_id = obj.room_id - else: - # If it's a chat room itself - room_id = getattr(obj, 'room_id', None) - - if not room_id: - return False - - cache_key = f"chat_membership_{request.user.id}_{room_id}" - is_member = cache.get(cache_key) - - if is_member is not None: - return is_member - - # Check database - try: - is_member = ChatMember.objects.filter( - user_id=request.user.id, - chat_room_id=room_id, - is_active=True - ).exists() - - # Cache the result for 5 minutes - cache.set(cache_key, is_member, timeout=300) - return is_member - except Exception as e: - logger.error(f"Error checking chat object permission: {str(e)}") - return False + """ + Permission that checks if the user is a member of the requested chat. + Uses caching to reduce database queries. + """ + def has_permission(self, request, view): + if not request.user or not request.user.is_authenticated: + return False + + # For list endpoints or actions not tied to a specific chat + if getattr(view, 'action', None) in ['list', 'create']: + return True + + room_id = view.kwargs.get('room_id') or request.data.get('room_id') or request.query_params.get('room_id') + if not room_id: + return False + + # Check cache first + cache_key = f"chat_membership_{request.user.id}_{room_id}" + is_member = cache.get(cache_key) + + if is_member is not None: + return is_member + + # Check database + try: + is_member = ChatMember.objects.filter( + user_id=request.user.user_id, + chat_room_id=room_id, + is_active=True + ).exists() + + # Cache the result for 5 minutes + cache.set(cache_key, is_member, timeout=300) + return is_member + except Exception as e: + logger.error(f"Error checking chat membership: {str(e)}") + return False + + def has_object_permission(self, request, view, obj): + # For objects with a direct reference to a chat room + if hasattr(obj, 'chat_room_id'): + room_id = obj.chat_room_id + elif hasattr(obj, 'room_id'): + room_id = obj.room_id + else: + # If it's a chat room itself + room_id = getattr(obj, 'room_id', None) + + if not room_id: + return False + + cache_key = f"chat_membership_{request.user.id}_{room_id}" + is_member = cache.get(cache_key) + + if is_member is not None: + return is_member + + # Check database + try: + is_member = ChatMember.objects.filter( + user_id=request.user.id, + chat_room_id=room_id, + is_active=True + ).exists() + + # Cache the result for 5 minutes + cache.set(cache_key, is_member, timeout=300) + return is_member + except Exception as e: + logger.error(f"Error checking chat object permission: {str(e)}") + return False class IsChatAdmin(BasePermission): """ @@ -155,41 +149,41 @@ def has_object_permission(self, request, view, obj): logger.error(f"Error checking chat admin object permission: {str(e)}") return False class CanSendMessage(BasePermission): - """ - Permission to check if a user can send messages in a chat. - Considers both membership and possible muting status. - """ - def has_permission(self, request, view): - if not request.user or not request.user.is_authenticated: - return False - - room_id = view.kwargs.get('room_id') or request.data.get('room_id') - if not room_id: - return False - - # Check cache first - cache_key = f"can_send_message_{request.user.id}_{room_id}" - can_send = cache.get(cache_key) - - if can_send is not None: - return can_send - - # Check database - verify membership and not muted - try: - member = ChatMember.objects.get( - user_id=request.user.id, - chat_room_id=room_id - ) - can_send = member.is_active and not member.is_muted - - # Cache the result for a shorter period (1 minute) as mute status may change - cache.set(cache_key, can_send, timeout=60) - return can_send - except ChatMember.DoesNotExist: - return False - except Exception as e: - logger.error(f"Error checking message sending permission: {str(e)}") - return False + """ + Permission to check if a user can send messages in a chat. + Considers both membership and possible muting status. + """ + def has_permission(self, request, view): + if not request.user or not request.user.is_authenticated: + return False + + room_id = view.kwargs.get('room_id') or request.data.get('room_id') + if not room_id: + return False + + # Check cache first + cache_key = f"can_send_message_{request.user.id}_{room_id}" + can_send = cache.get(cache_key) + + if can_send is not None: + return can_send + + # Check database - verify membership and not muted + try: + member = ChatMember.objects.get( + user_id=request.user.id, + chat_room_id=room_id + ) + can_send = member.is_active and not member.is_muted + + # Cache the result for a shorter period (1 minute) as mute status may change + cache.set(cache_key, can_send, timeout=60) + return can_send + except ChatMember.DoesNotExist: + return False + except Exception as e: + logger.error(f"Error checking message sending permission: {str(e)}") + return False class ServiceAuthentication(BasePermission): """ Permission for inter-service communication. @@ -230,7 +224,7 @@ def has_permission(self, request, view): # Allow service-to-service calls if ServiceAuthentication().has_permission(request, view): return True - + # New user endpoint is public if request.path.endswith('/chat/new_user/') and request.method == 'POST': return True @@ -267,4 +261,34 @@ def has_object_permission(self, request, view, obj): if action in ['GET', 'HEAD']: return IsChatMember().has_object_permission(request, view, obj) - return False \ No newline at end of file + return False + +class IsAuthenticatedUserProfile(BasePermission): + """ + Permesso personalizzato per il modello UserProfile. + """ + def has_permission(self, request, view): + return (request.user is not None and + not isinstance(request.user, AnonymousUser) and + hasattr(request.user, 'user_id')) +class IsOwnUserProfile(BasePermission): + """ + Permesso che verifica se l'utente sta accedendo ai propri dati. + Da usare per le richieste che manipolano dati utente. + """ + def has_permission(self, request, view): + # Verifica prima se l'utente รจ autenticato + if not IsAuthenticatedUserProfile().has_permission(request, view): + return False + + # Per le viste che usano l'ID utente nell'URL + user_id = view.kwargs.get('user_id') + if user_id and str(request.user.user_id) == str(user_id): + return True + + # Per le richieste che usano l'ID utente nei parametri query + user_id_param = request.query_params.get('user_id') + if user_id_param and str(request.user.user_id) == str(user_id_param): + return True + + return False diff --git a/Back-End/chat/my_chat/admin.py b/Back-End/chat/my_chat/admin.py index 3215a442..51f62e32 100644 --- a/Back-End/chat/my_chat/admin.py +++ b/Back-End/chat/my_chat/admin.py @@ -2,13 +2,5 @@ from django.contrib.auth import get_user_model from django.db.models.signals import post_migrate from django.dispatch import receiver -from chat.settings import ADMIN # Register your models here. -User = get_user_model() - -@receiver(post_migrate) -def create_superuser(sender, **kwargs): - if not User.objects.filter(email=ADMIN['email']).exists(): - User.objects.create_superuser(email=ADMIN['email'], password=ADMIN['password'], username=ADMIN['username']) - print('Superuser created successfully.') \ No newline at end of file diff --git a/Back-End/chat/my_chat/apps.py b/Back-End/chat/my_chat/apps.py index 12f1c51f..3412ea43 100644 --- a/Back-End/chat/my_chat/apps.py +++ b/Back-End/chat/my_chat/apps.py @@ -6,13 +6,12 @@ class MyChatConfig(AppConfig): def ready(self): try: - from .authentications import register_self - from .admin import create_superuser - from .signals import chat_room_created + # from .authentications import register_self + import my_chat.signals # Import signals to ensure they are registered #user_register_self() #print("User login successful") - register_self() + # register_self() except Exception as e: - # Optionally log the exception + # Optionally log the exception print(f"Error during registration: {e}") pass \ No newline at end of file diff --git a/Back-End/chat/my_chat/authentications.py b/Back-End/chat/my_chat/authentications.py deleted file mode 100644 index b3b5da5b..00000000 --- a/Back-End/chat/my_chat/authentications.py +++ /dev/null @@ -1,194 +0,0 @@ -import requests -from oauth2_provider.models import AccessToken -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -from django.conf import settings -from datetime import datetime, timedelta -from chat.settings import oauth2_settings , OAUTH2_APP_NAME -import json -from chat.settings import Microservices , CSRF_LOGIN_URL, REGISTER_URL, oauth2_settings - -# def login_self(): -# login_url = f'{Microservices['Login']}/login/login' -# data = { -# 'email': 'chat@chat.me', -# 'password' : 'chat_password', -# } - -# response = requests.post(login_url, json=data) - -# if response.status_code != 200: -# raise Exception('Failed to login user') -# return response.json() - -# def user_register_self(): -# register_url = 'http://localhost:8000/login/register' -# data = { -# 'email': 'chat@chat.me', -# 'username': 'my_chat', -# 'password': 'chat_password', -# } -# response = requests.post(register_url, json=data) - -# if response.status_code == 400: -# login_self() -# elif response.status_code != 201: -# raise Exception('Failed to register user') -# else: -# login_self() - - -def register_self(): - csrf_url = CSRF_LOGIN_URL - register_url = REGISTER_URL - - data = { - 'name': OAUTH2_APP_NAME, - 'service_password': oauth2_settings['SERVICE_PASSWORD'], - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'redirect_uris': 'http://localhost:8000', - 'client_id': oauth2_settings['CLIENT_ID'], - 'client_secret': oauth2_settings['CLIENT_SECRET'], - } - - session = requests.Session() - csrf_resp = session.get(csrf_url) - - if 'csrftoken' not in csrf_resp.cookies: - raise Exception('CSRF token not found in response cookies') - - csrf_token = csrf_resp.cookies['csrftoken'] - - access_token = oauth2_settings['TOKEN'] - print("Access token:", access_token) - headers = { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrf_token, - 'Authorization': 'Bearer ' + access_token, - } - - response = session.post(register_url, headers=headers, json=data) - print("Response status code:", response.status_code) - print("Response text:", response.json()) - if response.status_code != 200 and response.status_code != 201: - raise Exception('Failed to register application') - try: - app_data = response.json() - except json.JSONDecodeError: - raise Exception('Failed to parse JSON response') - - -#auth classes -import requests -from django.conf import settings -from django.core.cache import cache -from django.contrib.auth.models import AnonymousUser -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -import base64 ,requests -from chat.settings import oauth2_settings -from my_chat.models import UserProfile -from django.contrib.auth import get_user_model - -class TokenAuthentication(BaseAuthentication): - def authenticate(self, request): - if request.path == '/chat/new_user/': - request.user = AnonymousUser() - return None - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - return None - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - - if user is not None: - return (user, token) # Return tuple (user, auth) as required by DRF - else: - raise AuthenticationFailed("Invalid or inactive token") - - def get_user_from_token(self, token): - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - credentials = f"{client_id}:{client_secret}" - encoded_credentials = base64.b64encode(credentials.encode()).decode() - - headers = { - 'Authorization': f'Basic {encoded_credentials}', - 'Content-Type': 'application/x-www-form-urlencoded', - } - try: - response = requests.post(introspection_url, headers=headers,data={ - 'token': token, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - email = data.get('username') - User = UserProfile.objects.get(email=email) - try: - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, User, timeout=300) - return user - except e: - return AnonymousUser() - else: - print('Token introspection failed:' f'{response.status_code}') - print(f"Response: {response}") - return AnonymousUser() - except requests.RequestException as e: - # Log the exception if needed - print(f"Token introspection failed: {e}") - return None # Default to None if authentication fails - - - - - - # def get_user_from_token(self, token): - # user = cache.get(token) - # if user: - # return user - - # # Token introspection request - # introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - # client_id = oauth2_settings['CLIENT_ID'] - # client_secret = oauth2_settings['CLIENT_SECRET'] - - # try: - # response = requests.post(introspection_url, data={ - # 'token': token, - # 'client_id': client_id, - # 'client_secret': client_secret, - # }) - - # try: - # response_json = response.json() - # print(f'\033[96mResponse1 json: {response_json}\033[0m') #DEBUG# # Testo ciano - # except requests.exceptions.JSONDecodeError: - # print('\033[91mFailed to decode JSON response\033[0m') #DEBUG# # Testo rosso - # return AnonymousUser() - - # if response.status_code == 200: - # data = response.json() - # if data.get('active'): - # user_id = data.get('username') - # User = get_user_model() - # try: - # user = User.objects.get(user_id=user_id) - # # Cache user for token expiry duration (default to 5 min if not provided) - # expires_in = data.get('expires_in', 300) - # cache.set(token, user, timeout=expires_in) - # return user - # except User.DoesNotExist: - # pass - # except requests.RequestException as e: - # raise AuthenticationFailed(f"Token introspection failed: {str(e)}") \ No newline at end of file diff --git a/Back-End/chat/my_chat/consumers.py b/Back-End/chat/my_chat/consumers.py index 7c1daeea..c9be58a6 100644 --- a/Back-End/chat/my_chat/consumers.py +++ b/Back-End/chat/my_chat/consumers.py @@ -1,22 +1,88 @@ import json +import logging +from urllib.parse import parse_qs from channels.generic.websocket import AsyncWebsocketConsumer -from my_chat.models import ChatRoom, ChatMessage -from .user_services import register_user +from channels.db import database_sync_to_async +from my_chat.models import ChatRoom, ChatMessage, ChatMember, UserProfile from asgiref.sync import sync_to_async -from my_chat.models import ChatRoom, ChatMessage + +logger = logging.getLogger('django') class ChatConsumer(AsyncWebsocketConsumer): - async def connect(self): - self.room_id = self.scope['url_route']['kwargs']['room_id'] + async def connect(self): + # Estrai room_id dai parametri query + query_string = parse_qs(self.scope["query_string"].decode()) + if 'room_id' not in query_string: + logger.error("WebSocket connection attempt without room_id") + await self.close(code=4000) + return + + self.room_id = query_string['room_id'][0] self.room_group_name = f'chat_{self.room_id}' + + # Controlla connessione Redis + await self.log_redis_connection() + # Join room group await self.channel_layer.group_add( self.room_group_name, self.channel_name ) - + + logger.info(f"User {self.scope['user']} connected to chat {self.room_id}") await self.accept() + @database_sync_to_async + def is_chat_member(self): + try: + user = self.scope['user'] + # Se l'utente รจ AnonymousUser, non รจ membro + if not hasattr(user, 'user_id'): + return False + + # Verifica nel database + return ChatMember.objects.filter( + user_id=user.user_id, + chat_room_id=self.room_id, + is_active=True + ).exists() + except Exception as e: + logger.error(f"Error checking chat membership: {str(e)}") + return False + + @database_sync_to_async + def is_user_blocked(self, sender_username): + """Verifica se l'utente corrente ha bloccato il sender""" + try: + current_user = self.scope['user'] + if not hasattr(current_user, 'user_id'): + return False + + sender_profile = UserProfile.objects.get(username=sender_username) + current_user_profile = UserProfile.objects.get(user_id=current_user.user_id) + + return current_user_profile.is_blocked_by(sender_profile) + except Exception as e: + logger.error(f"Error checking if user is blocked: {str(e)}") + return False + + async def log_redis_connection(self): + try: + # Test della connessione Redis senza accedere a connection_pool + # Prova un'operazione semplice per verificare che Redis funzioni + await self.channel_layer.group_add("test_connection", self.channel_name) + await self.channel_layer.group_discard("test_connection", self.channel_name) + + # Log con informazioni disponibili + if hasattr(self.channel_layer, 'hosts'): + logger.info(f"Redis connection info: {self.channel_layer.hosts}") + else: + config = getattr(self.channel_layer, 'config', {}) + logger.info(f"Redis layer config: {config}") + logger.info("Redis connection test successful") + except Exception as e: + logger.error(f"Error checking Redis connection: {str(e)}") + async def disconnect(self, close_code): # Leave room group await self.channel_layer.group_discard( @@ -24,36 +90,67 @@ async def disconnect(self, close_code): self.channel_name ) - async def save_message(self, room_id, message, sender, timestamp): + async def save_message(self, room_id, message, sender_username, timestamp, message_type='text'): try: room = await sync_to_async(ChatRoom.objects.get)(room_id=room_id) + + sender_profile = await sync_to_async(UserProfile.objects.get)(username=sender_username) + + await sync_to_async(ChatMessage.objects.create)( + room=room, + message=message, + sender=sender_profile, + timestamp=timestamp, + message_type=message_type + ) + logger.info(f"Message saved: {message_type} message from {sender_username} in room {room_id}") + return None except ChatRoom.DoesNotExist: return {'error': 'Room does not exist'} -# # Retrieve the chat room asynchronously -# try: -# room = await sync_to_async(ChatRoom.objects.get)(id=room_id) -# except ChatRoom.DoesNotExist: -# # Send error back if room does not exist -# await self.send(text_data=json.dumps({ -# 'error': 'Room does not exist' -# })) -# return - - # Save message to the database - await sync_to_async(ChatMessage.objects.create)( - room=room, - message=message, - sender=sender, - timestamp=timestamp - ) - return None + except UserProfile.DoesNotExist: + logger.error(f"User profile not found for username: {sender_username}") + if hasattr(self.scope['user'], 'user_id'): + try: + sender_profile = await sync_to_async(UserProfile.objects.get)(user_id=self.scope['user'].user_id) + await sync_to_async(ChatMessage.objects.create)( + room=room, + message=message, + sender=sender_profile, + timestamp=timestamp, + message_type=message_type + ) + return None + except Exception as e: + logger.error(f"Fallback failed: {str(e)}") + + return {'error': 'User profile not found'} + except Exception as e: + logger.error(f"Error saving message: {str(e)}") + return {'error': f'Error saving message: {str(e)}'} async def receive(self, text_data): - text_data_json = json.loads(text_data) - message = text_data_json['message'] - room_id = text_data_json['room_id'] - sender = text_data_json['sender'] - timestamp = text_data_json.get('timestamp') + try: + data = json.loads(text_data) + message_type = data.get('type', 'text') + + if message_type == 'text': + await self.handle_text_message(data) + elif message_type == 'game_invitation': + await self.handle_game_invitation(data) + else: + logger.warning(f"Unknown message type: {message_type}") + + except json.JSONDecodeError: + logger.error("Invalid JSON received") + except Exception as e: + logger.error(f"Error processing message: {str(e)}") + + async def handle_text_message(self, data): + """Handle regular text messages""" + message = data['message'] + room_id = data['room_id'] + sender = data['sender'] + timestamp = data.get('timestamp') await self.channel_layer.group_send( self.room_group_name, @@ -62,25 +159,59 @@ async def receive(self, text_data): 'message': message, 'room_id': room_id, 'timestamp': timestamp, - 'sender': sender + 'sender': sender, + 'sender_id': self.scope['user'].user_id if hasattr(self.scope['user'], 'user_id') else None } ) - error = await self.save_message(room_id, message, sender, timestamp) + error = await self.save_message(room_id, message, sender, timestamp, 'text') + if error: + await self.send(text_data=json.dumps(error)) + + async def handle_game_invitation(self, data): + """Handle game invitation messages""" + sender = data.get('sender') + message = data.get('message', f"{sender} invited you to play a game!") + room_id = data.get('room_id') + timestamp = data.get('timestamp') + + # Broadcast game invitation to all users in the room + await self.channel_layer.group_send( + self.room_group_name, + { + 'type': 'game_invitation_message', + 'message': message, + 'room_id': room_id, + 'sender': sender, + 'timestamp': timestamp, + 'sender_id': self.scope['user'].user_id if hasattr(self.scope['user'], 'user_id') else None + } + ) + + # Save to database as game_invitation type + error = await self.save_message(room_id, message, sender, timestamp, 'game_invitation') if error: await self.send(text_data=json.dumps(error)) - async def chat_message(self, event): - message = event['message'] - room_id = event['room_id'] - sender = event['sender'] - timestamp = event['timestamp'] async def chat_message(self, event): # Send message to WebSocket await self.send(text_data=json.dumps({ + 'type': 'text', + 'message': event['message'], + 'room_id': event['room_id'], + 'sender': event['sender'], + 'timestamp': event['timestamp'], + 'sender_id': event['sender_id'] + })) + + async def game_invitation_message(self, event): + """Send game invitation to WebSocket""" + await self.send(text_data=json.dumps({ + 'type': 'game_invitation', 'message': event['message'], 'room_id': event['room_id'], 'sender': event['sender'], - 'timestamp': event['timestamp'] + 'timestamp': event['timestamp'], + 'sender_id': event['sender_id'] })) diff --git a/Back-End/chat/my_chat/dicts.py b/Back-End/chat/my_chat/dicts.py deleted file mode 100644 index c547fa21..00000000 --- a/Back-End/chat/my_chat/dicts.py +++ /dev/null @@ -1,42 +0,0 @@ -room_names = [ - "General", - "Random", - "TechTalk", - "Gaming", - "Music", - "Movies", - "Books", - "Travel", - "Food", - "Fitness", - "Health", - "Education", - "Science", - "History", - "Art", - "Photography", - "Programming", - "Startups", - "Business", - "Finance", - "Investing", - "Crypto", - "AI", - "MachineLearning", - "DataScience", - "Sports", - "Soccer", - "Basketball", - "Baseball", - "Tennis", - "Running", - "Yoga", - "Meditation", - "Pets", - "Parenting", - "Relationships", - "Advice", - "Support", - "Announcements", - "Events" -] \ No newline at end of file diff --git a/Back-End/chat/my_chat/middleware.py b/Back-End/chat/my_chat/middleware.py index aa70a64f..49f7ff02 100644 --- a/Back-End/chat/my_chat/middleware.py +++ b/Back-End/chat/my_chat/middleware.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import AnonymousUser from channels.auth import AuthMiddlewareStack from django.core.cache import cache +from django.db import OperationalError, connections +from django.http import JsonResponse from rest_framework_simplejwt.tokens import AccessToken from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.permissions import BasePermission @@ -14,6 +16,37 @@ from django.conf import settings logger = logging.getLogger('django') +class HealthCheckMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Allow health check endpoint regardless of host + if request.path.endswith('/health'): # Liveness probe + return JsonResponse({'status': 'ok'}) + elif request.path.endswith('/ready'): # Readiness probe + db_ready = False + try: + # Check the default database connection + db_conn = connections['default'] + db_conn.cursor() # Attempt to get a cursor to check connectivity + # Optionally, you could execute a very simple query: + # with db_conn.cursor() as cursor: + # cursor.execute("SELECT 1") + # cursor.fetchone() + db_ready = True + except OperationalError: + logger.error("Readiness probe: Database connection failed.") + # db_ready remains False + + if db_ready: + return JsonResponse({'status': 'ready', 'database': 'ok'}) + else: + return JsonResponse({'status': 'unready', 'database': 'unavailable'}, status=503) + + return self.get_response(request) + + class ServiceAuthentication(authentication.BaseAuthentication): """ Authentication class specifically for inter-service communication. @@ -64,7 +97,7 @@ def get_user_from_token(token): # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) + user = UserProfile.objects.get(user_id=user_id) # Cache the user with a timeout (e.g., 5 minutes) cache.set(token, user, timeout=300) return user @@ -78,49 +111,92 @@ class JWTAuth(JWTAuthentication): """ Enhanced JWT authentication class with caching support and improved error handling. """ + user_id_claim = 'user_id' + def authenticate(self, request): # Skip authentication for specific paths if needed if request.path.endswith('/chat/new_user/') and request.method == 'POST': logger.debug(f"Skipping JWT auth for {request.path}") return (AnonymousUser(), None) - + logger.debug(f"Headers in request: {request.META}") # Check for cached authentication result first auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if auth_header and auth_header.startswith('Bearer '): - token = auth_header.replace('Bearer ', '') + if not auth_header or not auth_header.startswith('Bearer '): + return None - # Try to get from cache first - cache_key = f"jwt_auth_{token}" - cached_result = cache.get(cache_key) + token = auth_header.replace('Bearer ', '') + if not token: + return None + + # Try to get from cache first + cache_key = f"jwt_auth_{token}" + cached_result = cache.get(cache_key) + + if cached_result is not None: + logger.debug("Using cached JWT authentication result") + if cached_result == "anonymous": + return None + return cached_result - if cached_result is not None: - logger.debug("Using cached JWT authentication result") - if cached_result == "anonymous": - return None - return cached_result + # If not in cache, validate the token using the more robust method from WebSocket middleware + try: + # Directly try to decode the JWT token first (like in WebSocket middleware) + access_token = AccessToken(token) + user_id = access_token.get('user_id') + + if not user_id: + logger.warning("No user_id claim found in token") + cache.set(cache_key, "anonymous", timeout=60) + return None + # Check if user exists in database try: - # Call parent class to validate token - auth_result = super().authenticate(request) - - # Cache the result (None results don't need caching) - if auth_result: - # Cache successful authentications for 5 minutes (adjust as needed) - cache.set(cache_key, auth_result, timeout=300) - logger.debug(f"JWT auth successful for user {auth_result[0]}") - return auth_result - else: - # Cache negative results for a shorter time (1 minute) - cache.set(cache_key, "anonymous", timeout=60) - return None - - except Exception as e: - # Log the error but don't cache exceptions - logger.warning(f"JWT authentication failed: {str(e)}") + user = UserProfile.objects.get(user_id=user_id) + # Successfully authenticated + auth_result = (user, token) + cache.set(cache_key, auth_result, timeout=300) + logger.debug(f"JWT auth successful for user {user}") + return auth_result + except UserProfile.DoesNotExist: + logger.warning(f"User ID {user_id} from token not found in UserProfile") + cache.set(cache_key, "anonymous", timeout=60) return None + + except Exception as e: + # Log the error but don't cache exceptions + logger.warning(f"JWT authentication failed: {str(e)}") + logger.warning(f"Token details: {token[:10]}...{token[-10:] if len(token) > 20 else ''}") + try: + # Try to extract some debug info without validation + decoded = AccessToken(token, verify=False) + user_id = decoded.get('user_id') + exp = decoded.get('exp', 'unknown') + iat = decoded.get('iat', 'unknown') + logger.warning(f"Token debug info - User ID: {user_id}, Expires: {exp}, Issued: {iat}") + if user_id: + logger.warning(f"User exists: {UserProfile.objects.filter(user_id=user_id).exists()}") + except Exception as inner_e: + logger.warning(f"Token decode failed: {str(inner_e)}") + + return None - # No auth header or not a Bearer token - return super().authenticate(request) + def get_user(self, validated_token): + """ + Returns user from validated token for DRF compatibility + """ + try: + user_id = validated_token[self.user_id_claim] + + # Cerca l'utente nel modello UserProfile + user = UserProfile.objects.get(user_id=user_id) + + return user + except UserProfile.DoesNotExist: + logger.warning(f"User ID {user_id} from token not found in UserProfile") + return None + except Exception as e: + logger.error(f"Error getting user from token: {str(e)}") + return None class JWTAuthMiddleware(BaseMiddleware): """ @@ -147,7 +223,7 @@ async def __call__(self, scope, receive, send): if auth_header: token_str = auth_header.decode() if token_str.startswith('Bearer '): - token = token_str[7:] # Remove 'Bearer ' prefix + token = token_str[7:] if token: # Get user from token @@ -176,7 +252,7 @@ def get_user_from_jwt(self, token): # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) + user = UserProfile.objects.get(user_id=user_id) # Cache the user with a timeout (e.g., 5 minutes) cache.set(f"jwt_ws_user_{token}", user, timeout=300) return user @@ -192,6 +268,37 @@ def JWTAuthMiddlewareStack(inner): """ return JWTAuthMiddleware(AuthMiddlewareStack(inner)) + +import logging +from channels.middleware import BaseMiddleware + +logger = logging.getLogger('django') + +class DebugMiddleware(BaseMiddleware): + async def __call__(self, scope, receive, send): + # Log dettagli della richiesta + if scope['type'] == 'websocket': + logger.info(f"โญ WEBSOCKET REQUEST โญ") + logger.info(f"PATH: {scope['path']}") + logger.info(f"HEADERS: {scope.get('headers', [])}") + logger.info(f"QUERY_STRING: {scope.get('query_string', b'').decode()}") + + # Passa al middleware successivo + return await super().__call__(scope, receive, send) + +class ErrorLoggingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + try: + response = self.get_response(request) + return response + except Exception as e: + import traceback + logger.error(f"Uncaught exception: {str(e)}\n{traceback.format_exc()}") + raise + # class TokenAuthMiddleware(BaseMiddleware): # async def __call__(self, scope, receive, send): # query_string = parse_qs(scope["query_string"].decode()) @@ -241,7 +348,7 @@ def JWTAuthMiddlewareStack(inner): # # Retrieve the user from the database # try: -# user = UserProfile.objects.get(id=user_id) +# user = UserProfile.objects.get(user_id=user_id) # # Cache the user with a timeout (e.g., 5 minutes) # cache.set(token, user, timeout=300) # return user @@ -265,7 +372,7 @@ def JWTAuthMiddlewareStack(inner): # # Retrieve the user from the database # try: -# user = UserProfile.objects.get(id=user_id) +# user = UserProfile.objects.get(user_id=user_id) # # Cache the user with a timeout (e.g., 5 minutes) # cache.set(token, user, timeout=300) # return user diff --git a/Back-End/chat/my_chat/models.py b/Back-End/chat/my_chat/models.py index 2a680624..0b8b8d37 100644 --- a/Back-End/chat/my_chat/models.py +++ b/Back-End/chat/my_chat/models.py @@ -1,63 +1,191 @@ from django.db import models +import uuid +from django.utils import timezone # Create your models here. +# on block user, it should block all the incoming messages from the blocked user class UserProfile(models.Model): - user_id = models.IntegerField(primary_key=True) - username = models.CharField(max_length=255, null=True) - email = models.EmailField(max_length=255, unique=True, null=True) - is_staff = models.BooleanField(default=False) + user_id = models.IntegerField(primary_key=True) + username = models.CharField(max_length=255, null=True) + email = models.EmailField(max_length=255, unique=True, null=True) + is_staff = models.BooleanField(default=False) - def __str__(self): - return self.username or f"User {self.user_id}" + @property + def is_authenticated(self): + return True + + @property + def is_anonymous(self): + return False + + @property + def id(self): + """Provides compatibility with code that uses 'id' instead of 'user_id'""" + return self.user_id + + def __str__(self): + return self.username or f"User {self.user_id}" + + def block_user(self, user_to_block): + """Block a user""" + blocked_user, created = BlockedUser.objects.get_or_create( + blocker=self, + blocked=user_to_block + ) + return blocked_user + + def unblock_user(self, user_to_unblock): + """Unblock a user""" + try: + blocked_user = BlockedUser.objects.get( + blocker=self, + blocked=user_to_unblock + ) + blocked_user.delete() + return True + except BlockedUser.DoesNotExist: + return False + + def is_blocked_by(self, user): + """Check if this user is blocked by another user""" + return BlockedUser.objects.filter(blocker=user, blocked=self).exists() + + def has_blocked(self, user): + """Check if this user has blocked another user""" + return BlockedUser.objects.filter(blocker=self, blocked=user).exists() + + def get_blocked_users(self): + """Get all users blocked by this user""" + return UserProfile.objects.filter( + user_id__in=BlockedUser.objects.filter(blocker=self).values_list('blocked_id', flat=True) + ) + + def get_users_who_blocked_me(self): + """Get all users who blocked this user""" + return UserProfile.objects.filter( + user_id__in=BlockedUser.objects.filter(blocked=self).values_list('blocker_id', flat=True) + ) + + +class BlockedUser(models.Model): + """Model to handle user blocking relationships with additional metadata""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + blocker = models.ForeignKey( + UserProfile, + on_delete=models.CASCADE, + related_name='blocked_users' + ) + blocked = models.ForeignKey( + UserProfile, + on_delete=models.CASCADE, + related_name='blocked_by_users' + ) + blocked_at = models.DateTimeField(default=timezone.now) + reason = models.TextField(blank=True, null=True) # Optional reason for blocking + + class Meta: + unique_together = ('blocker', 'blocked') + indexes = [ + models.Index(fields=['blocker']), + models.Index(fields=['blocked']), + models.Index(fields=['blocked_at']), + ] + + def __str__(self): + return f"{self.blocker} blocked {self.blocked} at {self.blocked_at}" + + def clean(self): + """Prevent users from blocking themselves""" + from django.core.exceptions import ValidationError + if self.blocker == self.blocked: + raise ValidationError("Users cannot block themselves") + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) class ChatRoom(models.Model): - room_id = models.AutoField(primary_key=True) - room_name = models.CharField(max_length=100) - room_description = models.TextField() - users = models.ManyToManyField(UserProfile, through='ChatMember', related_name='chat_rooms') - creation_time = models.DateTimeField(auto_now_add=True) - creator = models.ForeignKey(UserProfile, related_name='creator', on_delete=models.SET_NULL, null=True) - - def get_user_number(self): - return self.users.count() - - def __str__(self): - return self.room_name + room_id = models.AutoField(primary_key=True) + room_name = models.CharField(max_length=100) + room_description = models.TextField() + users = models.ManyToManyField(UserProfile, through='ChatMember', related_name='chat_rooms') + creation_time = models.DateTimeField(auto_now_add=True) + creator = models.ForeignKey(UserProfile, related_name='creator', on_delete=models.SET_NULL, null=True) + + def get_user_number(self): + return self.users.count() + + def __str__(self): + return self.room_name # model to handle the many-to-many relationship with roles class ChatMember(models.Model): - # Roles - ROLES = [ - ('admin', 'Administrator'), - ('moderator', 'Moderator'), - ('member', 'Regular Member'), - ('guest', 'Guest'), - ] - - user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) - chat_room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE) - role = models.CharField(max_length=20, choices=ROLES, default='member') - joined_at = models.DateTimeField(auto_now_add=True) - is_active = models.BooleanField(default=True) - is_muted = models.BooleanField(default=False) - muted_until = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = ('user', 'chat_room') - - def __str__(self): - return f"{self.user} in {self.chat_room} as {self.get_role_display()}" + # Roles + ROLES = [ + ('admin', 'Administrator'), + ('moderator', 'Moderator'), + ('member', 'Regular Member'), + ('guest', 'Guest'), + ] + + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + chat_room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE) + role = models.CharField(max_length=20, choices=ROLES, default='member') + joined_at = models.DateTimeField(auto_now_add=True) + is_active = models.BooleanField(default=True) + is_muted = models.BooleanField(default=False) + muted_until = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ('user', 'chat_room') + + def __str__(self): + return f"{self.user} in {self.chat_room} as {self.get_role_display()}" class ChatMessage(models.Model): + MESSAGE_TYPES = [ + ('text', 'Text Message'), + ('game_invitation', 'Game Invitation'), + ('image', 'Image'), + ('video', 'Video'), + ('file', 'File'), + ('system', 'System Message'), + ] + message_id = models.AutoField(primary_key=True) room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE) - message = models.TextField() sender = models.ForeignKey(UserProfile, on_delete=models.SET_DEFAULT, default=1) timestamp = models.DateTimeField(auto_now_add=True) + # Message type and content + message_type = models.CharField(max_length=20, choices=MESSAGE_TYPES, default='text') + message = models.TextField(blank=True) # For text messages and game invitation messages + + # Media fields (for future use) + image = models.ImageField(upload_to='chat_images/', null=True, blank=True) + video = models.FileField(upload_to='chat_videos/', null=True, blank=True) + file = models.FileField(upload_to='chat_files/', null=True, blank=True) + + # Metadata for any message type (flexible JSON field for future extensions) + metadata = models.JSONField(default=dict, blank=True) + def __str__(self): - return f"Message {self.message_id} in {self.room}" - + return f"{self.get_message_type_display()} {self.message_id} in {self.room}" + + @property + def content(self): + """Return appropriate content based on message type""" + if self.message_type == 'text': + return self.message + elif self.message_type == 'game_invitation': + return self.message + elif self.message_type == 'image': + return self.image.url if self.image else None + elif self.message_type == 'video': + return self.video.url if self.video else None + elif self.message_type == 'file': + return self.file.url if self.file else None + return self.message def get_user_model(user_id): - return UserProfile.objects.get(user_id=user_id) \ No newline at end of file + return UserProfile.objects.get(user_id=user_id) \ No newline at end of file diff --git a/Back-End/chat/my_chat/notification.py b/Back-End/chat/my_chat/notification.py index b2fe782e..6f89beae 100644 --- a/Back-End/chat/my_chat/notification.py +++ b/Back-End/chat/my_chat/notification.py @@ -1,7 +1,7 @@ from django.db import models import os import asyncio -import aiohttp +# import aiohttp from chat.settings import Microservices Types = { @@ -14,7 +14,7 @@ class BaseNotification(models.Model): id = models.AutoField(primary_key=True) Sender = models.CharField(max_length=200, choices=[(key, key) for key in Microservices.keys()]) - message = models.TextField() + message = models.JSONField() class Meta: abstract = True @@ -37,11 +37,11 @@ class ImmediateNotification(UserNotification, GroupNotification): class ScheduledNotification(UserNotification, GroupNotification): pass -async def SendNotification(Notification): - noification_url = Microservices['Notifications'] - async with aiohttp.ClientSession() as session: - async with session.post(noification_url, data=Notification) as response: - return response.status +# async def SendNotification(Notification): +# noification_url = Microservices['Notifications'] +# async with aiohttp.ClientSession() as session: +# async with session.post(noification_url, data=Notification) as response: +# return response.status import requests from rest_framework import serializers diff --git a/Back-End/chat/my_chat/routing.py b/Back-End/chat/my_chat/routing.py new file mode 100644 index 00000000..39fa8404 --- /dev/null +++ b/Back-End/chat/my_chat/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + # Usa un pattern semplice senza room_id nell'URL + re_path(r'^chat/?$', consumers.ChatConsumer.as_asgi()), +] \ No newline at end of file diff --git a/Back-End/chat/my_chat/serializers.py b/Back-End/chat/my_chat/serializers.py index d749f668..46517dac 100644 --- a/Back-End/chat/my_chat/serializers.py +++ b/Back-End/chat/my_chat/serializers.py @@ -1,23 +1,127 @@ +import logging from rest_framework import serializers -from .models import ChatRoom, ChatMessage, UserProfile +from .models import ChatMember, ChatRoom, ChatMessage, UserProfile + +logger = logging.getLogger('django') + class userSerializer(serializers.ModelSerializer): class Meta: model = UserProfile fields = '__all__' + +class userCreateSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ['user_id', 'username', 'email'] +class userBlockedSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ['user_id', 'username'] class chat_roomSerializer(serializers.ModelSerializer): - class Meta: - model = ChatRoom - fields = ['room_id', 'room_name', 'room_description', 'creator', 'users'] + creator = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()) + users = serializers.ListField( + child=serializers.IntegerField(), + write_only=True, + required=False + ) + + class Meta: + model = ChatRoom + fields = ['room_id', 'room_name', 'room_description', 'creator', 'users'] + + def create(self, validated_data): + # Estrai lista di user_ids + user_ids = validated_data.pop('users', []) + + # Crea ChatRoom + chat_room = ChatRoom.objects.create(**validated_data) + + # Aggiungi gli utenti alla chat + if user_ids: + try: + # Trova i profili utente + user_profiles = UserProfile.objects.filter(user_id__in=user_ids) + + # Additional blocking check for users being added + creator = chat_room.creator + filtered_profiles = [] + + for profile in user_profiles: + # Check if creator has blocked this user + if creator and creator.has_blocked(profile): + logging.warning(f"Creator {creator.user_id} has blocked user {profile.user_id}, skipping") + continue + + # Check if this user has blocked the creator + if creator and profile.has_blocked(profile): + logging.warning(f"User {profile.user_id} has blocked creator {creator.user_id}, skipping") + continue + + filtered_profiles.append(profile) + + # Aggiungi membri alla chat con ruoli predefiniti + for profile in filtered_profiles: + # Il creatore รจ admin, gli altri sono normal_user + role = 'admin' if profile.user_id == chat_room.creator.user_id else 'normal_user' + ChatMember.objects.create( + chat_room=chat_room, + user=profile, + role=role + ) + + logging.info(f"Created chat room {chat_room.room_id} with {len(filtered_profiles)} members (filtered from {len(user_profiles)} due to blocking)") + except Exception as e: + logging.error(f"Error adding users to chat: {str(e)}") + # Se la creazione dei membri fallisce, elimina la chat + chat_room.delete() + raise + + return chat_room + + def to_representation(self, instance): + # Per le operazioni di lettura, includi gli utenti come elenco di id + representation = super().to_representation(instance) + representation['users'] = list(instance.users.values_list('user_id', flat=True)) + return representation class chat_messageSerializer(serializers.ModelSerializer): - message_id = serializers.IntegerField() - room_id = serializers.PrimaryKeyRelatedField(queryset=ChatRoom.objects.all()) - message = serializers.CharField() - sender = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()) - timestamp = serializers.DateTimeField() - class Meta: - model = ChatMessage - fields = '__all__' + # Aggiungi campi espliciti per evitare problemi di serializzazione + sender_name = serializers.SerializerMethodField() + + class Meta: + model = ChatMessage + fields = ['message_id', 'room', 'message', 'sender', 'sender_name', 'timestamp', 'message_type'] + + def get_sender_name(self, obj): + try: + if hasattr(obj.sender, 'username') and obj.sender.username: + return obj.sender.username + return str(obj.sender) + except Exception as e: + logger.error(f"Error getting sender_name: {str(e)}") + return "Unknown" + + def to_representation(self, instance): + try: + data = { + 'message_id': instance.message_id, + 'message': instance.message, + 'timestamp': instance.timestamp, + 'sender': getattr(instance.sender, 'username', 'Unknown User'), + 'sender_id': instance.sender.user_id if instance.sender else None, + 'message_type': instance.message_type + } + return data + except Exception as e: + logger.error(f"Serialization error for message {getattr(instance, 'message_id', 'unknown')}: {str(e)}") + # Return minimal safe representation + return { + 'message_id': getattr(instance, 'message_id', 0), + 'message': getattr(instance, 'message', 'Error retrieving message'), + 'timestamp': getattr(instance, 'timestamp', ''), + 'sender': 'Unknown User', + 'message_type': 'unknown' + } \ No newline at end of file diff --git a/Back-End/chat/my_chat/signals.py b/Back-End/chat/my_chat/signals.py index b49c854a..e407336e 100644 --- a/Back-End/chat/my_chat/signals.py +++ b/Back-End/chat/my_chat/signals.py @@ -1,39 +1,113 @@ import signal from django.dispatch import receiver from django.db.models.signals import post_save -from .models import UserProfile, ChatRoom, ChatMessage +from .models import ChatMember, UserProfile, ChatRoom, ChatMessage from .notification import ImmediateNotification, SendNotificationSync +from django.db.models.signals import m2m_changed +import logging -# @receiver(post_save, sender=ChatRoom) -# def chat_room_created(sender, instance, created, **kwargs): -# if created: -# print(f'ChatRoom object: {instance}') -# print(f'ChatRoom object: {instance.users.count()}') -# if instance.users.count() > 0: -# for user in instance.users.all(): -# print('passed') -# notification = ImmediateNotification( -# Sender='Chat', -# message=f'Chat Room {instance.room_id} created', -# user_id=user.user_id -# ) -# SendNotificationSync(notification) -# print(f'Notification Sent to User {user.user_id}') +logger = logging.getLogger('django') -from django.db.models.signals import m2m_changed -@receiver(m2m_changed, sender=ChatRoom.users.through) -def chat_room_created(sender, instance, action, **kwargs): - if action == 'post_add': - print(f'ChatRoom object: {instance}') - print(f'ChatRoom object: {instance.users.count()}') - if instance.users.count() > 0: - for user in instance.users.all(): - print('passed') - notification = ImmediateNotification( - Sender='Chat', - message=f'Chat Room {instance.room_id} created', - user_id=user.user_id - ) - SendNotificationSync(notification) - print(f'Notification Sent to User {user.user_id}') \ No newline at end of file +@receiver(post_save, sender=ChatMember) +def chat_member_added(sender, instance, created, **kwargs): + """ + Signal triggered when a ChatMember is created (user added to chat room) + """ + if not created: + return # Solo per nuovi ChatMember, non per aggiornamenti + + chat_room = instance.chat_room + user = instance.user + if user is chat_room.creator: + logger.info(f'๐Ÿšจ User {user.user_id} is the creator of ChatRoom {chat_room.room_id}') + return + logger.info(f'๐Ÿšจ User {user.user_id} added to ChatRoom {chat_room.room_id}') + try: + # Determina se รจ una nuova chat o aggiunta a chat esistente + total_members = chat_room.users.count() + + # Se c'รจ solo 1 membro, รจ probabilmente il creatore (nuova chat) + # Se ci sono 2+ membri, qualcuno รจ stato aggiunto a chat esistente + is_new_chat = total_members <= 2 # Adatta questa logica se necessario + + + notification_type = 'chat_room_joined' + message_text = f'You have been added to chat room "{chat_room.room_name}"' + + # Crea la notifica + notification = ImmediateNotification( + Sender='Chat', + message={ + 'type': notification_type, + 'data': { + 'room_id': chat_room.room_id, + 'room_name': chat_room.room_name, + 'room_description': chat_room.room_description, + 'creator_id': chat_room.creator.user_id if chat_room.creator else None, + 'creator_name': chat_room.creator.username if chat_room.creator else 'Unknown', + 'user_role': instance.role, + 'total_members': total_members + } + }, + user_id=user.user_id + ) + + # Invia la notifica + SendNotificationSync(notification) + logger.info(f'โœ… Notification sent to user {user.user_id} for chat room {chat_room.room_id}') + + except Exception as e: + logger.error(f'โŒ Error sending notification to user {user.user_id}: {str(e)}') + print(f"โŒ Error in chat_member_added signal: {str(e)}") + + +# @receiver(m2m_changed, sender=ChatRoom.users.through) +# def chat_room_users_changed(sender, instance, action, pk_set, **kwargs): +# """ +# Signal to handle when users are added to or removed from chat rooms. +# Sends notifications for both chat creation and when users are added to existing chats. +# """ +# logger.debug(f'ChatRoom users changed: {instance.room_id}, action: {action}, pk_set: {pk_set}') +# if action == 'post_add' and pk_set: +# logger.info(f'Users added to ChatRoom {instance.room_id}: {pk_set}') + +# # Get the users that were just added +# added_users = UserProfile.objects.filter(user_id__in=pk_set) + +# # Determine if this is a new chat creation or adding users to existing chat +# total_users = instance.users.count() +# is_new_chat = total_users == len(pk_set) # If all users are the ones just added, it's a new chat + +# for user in added_users: +# try: +# if is_new_chat: +# # This is a new chat creation +# notification = ImmediateNotification( +# Sender='Chat', +# message={ +# 'type': 'chat_room_created', +# 'room_id': instance.room_id, +# 'room_name': instance.room_name, +# 'message': f'You have been added to new chat room "{instance.room_name}"' +# }, +# user_id=user.user_id +# ) +# else: +# # This is adding users to an existing chat +# notification = ImmediateNotification( +# Sender='Chat', +# message={ +# 'type': 'chat_room_joined', +# 'room_id': instance.room_id, +# 'room_name': instance.room_name, +# 'message': f'You have been added to chat room "{instance.room_name}"' +# }, +# user_id=user.user_id +# ) + +# SendNotificationSync(notification) +# logger.info(f'Notification sent to User {user.user_id} for chat room {instance.room_id}') + +# except Exception as e: +# logger.error(f'Error sending notification to user {user.user_id}: {str(e)}') \ No newline at end of file diff --git a/Back-End/chat/my_chat/tests.py b/Back-End/chat/my_chat/tests.py index abfe6a56..33a4c6c4 100644 --- a/Back-End/chat/my_chat/tests.py +++ b/Back-End/chat/my_chat/tests.py @@ -1,29 +1,59 @@ -from channels.testing import WebsocketCommunicator from django.contrib.auth import get_user_model from django.test import TransactionTestCase from chat.asgi import application from asgiref.sync import sync_to_async -class ChatTests(TransactionTestCase): - async def test_websocket_connection(self): - # Create a mock user using sync_to_async - User = get_user_model() - user = await sync_to_async(User.objects.create_user)(username='testuser', password='password') +# ...existing code... +from django.test import TestCase, Client - # Define the headers - headers = [(b'origin', b'http://localhost')] +class MyChatURLTests(TestCase): + def setUp(self): + self.client = Client() - # Create the WebsocketCommunicator with the URL path and headers - communicator = WebsocketCommunicator(application, "/ws/chat/room_1/", headers=headers) + def test_get_chat_message(self): + response = self.client.get('/chat/chat_rooms/1/get_message/') + self.assertIn(response.status_code, [200, 401, 403, 404]) - # Set the user in the scope - communicator.scope['user'] = user + def test_get_chat_info(self): + response = self.client.get('/chat/chat_rooms/1/') + self.assertIn(response.status_code, [200, 401, 403, 404]) - connected, subprotocol = await communicator.connect() - self.assertTrue(connected) + def test_new_user(self): + response = self.client.get('/chat/new_user/') + self.assertIn(response.status_code, [200, 401, 403]) - await communicator.send_json_to({'message': 'Hello, WebSocket!', 'room_id': 'room_1', 'sender': 'testuser', 'timestamp': '2021-01-01T00:00:00Z'}) - response = await communicator.receive_json_from() - self.assertEqual(response['message'], 'Hello, WebSocket!') + def test_create_chat(self): + response = self.client.get('/chat/chat_rooms/create/') + self.assertIn(response.status_code, [200, 401, 403]) - await communicator.disconnect() \ No newline at end of file + def test_get_chats(self): + response = self.client.get('/chat/chat_rooms/getchat/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_download_chat_data(self): + response = self.client.get('/chat/chat_data/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_download_similarities_data(self): + response = self.client.get('/chat/user_similarities/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_add_users_to_chat(self): + response = self.client.get('/chat/chat_rooms/1/add_user/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_block_user(self): + response = self.client.get('/chat/block_user/1') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_all_blocked_users(self): + response = self.client.get('/chat/blocked_users/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_chat_media_upload(self): + response = self.client.get('/chat/media/upload/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_chat_media_manager(self): + response = self.client.get('/chat/media/manage/') + self.assertIn(response.status_code, [200, 401, 403]) \ No newline at end of file diff --git a/Back-End/chat/my_chat/urls.py b/Back-End/chat/my_chat/urls.py index dbad3797..4f7c0e63 100644 --- a/Back-End/chat/my_chat/urls.py +++ b/Back-End/chat/my_chat/urls.py @@ -7,10 +7,13 @@ path('chat_rooms//get_message/', views.GetChatMessage.as_view()), # GET localhost:8000/chat_rooms/1/get_message/:20 path('chat_rooms//', views.GetChatInfo.as_view()), path('new_user/', views.new_user.as_view()), - path('create_channel_group/', views.CreateChannelGroupView.as_view()), path('chat_rooms/create/', views.CreateChat.as_view()), path('chat_rooms/getchat/', views.GetChats.as_view(), name='get_chats'), path('chat_data/', views.DownloadChatRoomData.as_view()), path('user_similarities/', views.DownloadSimilaritiesData.as_view()), path('chat_rooms//add_user/', views.AddUsersToChat.as_view()), + path('block_user/', views.BlockUser.as_view()), + path('blocked_users/', views.allBlockedUsers.as_view()), + path('media/upload/', views.ChatMediaUpload.as_view(), name='chat_media_upload'), + path('media/manage/', views.ChatMediaManager.as_view(), name='chat_media_manager'), ] \ No newline at end of file diff --git a/Back-End/chat/my_chat/views.py b/Back-End/chat/my_chat/views.py index d5f25be2..9e8d7a71 100644 --- a/Back-End/chat/my_chat/views.py +++ b/Back-End/chat/my_chat/views.py @@ -1,5 +1,5 @@ +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions -from rest_framework.permissions import IsAuthenticated from django_filters.rest_framework import DjangoFilterBackend from django.http import HttpResponse, JsonResponse from rest_framework import status @@ -7,17 +7,31 @@ from rest_framework.views import APIView from channels.layers import get_channel_layer from asgiref.sync import async_to_sync -from .models import ChatRoom, UserProfile, ChatMessage +from .models import ChatRoom, UserProfile, ChatMessage, ChatMember, BlockedUser from django.contrib.auth.models import AnonymousUser -from .serializers import chat_roomSerializer, chat_messageSerializer, userSerializer +from .serializers import chat_roomSerializer, chat_messageSerializer, userSerializer, userBlockedSerializer, userCreateSerializer from .middleware import ServiceAuthentication, JWTAuthMiddleware , JWTAuth -from .authentications import TokenAuthentication from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from rest_framework_simplejwt.authentication import JWTAuthentication import logging -from .Permissions import ChatRoomPermissions +from .Permissions import ChatRoomPermissions , IsAuthenticatedUserProfile , IsOwnUserProfile from django.views.decorators.csrf import csrf_exempt +from functools import wraps +from rest_framework.parsers import MultiPartParser, FormParser +from django.core.files.storage import default_storage +import os + +logger = logging.getLogger('django') + + +def post_api_view(func): + @wraps(func) + def wrapper(self, request, *args, **kwargs): + logger.info(f"POST API called: {func.__name__} by user {getattr(request.user, 'username', 'Anonymous')}") + return func(self, request, *args, **kwargs) + return wrapper + class GetChatMessage(generics.ListAPIView): """ API endpoint that allows users to be viewed or edited. @@ -35,22 +49,69 @@ class GetChatMessage(generics.ListAPIView): serializer_class = chat_messageSerializer lookup_url_kwarg = 'room_id' authentication_classes = [JWTAuth] - permission_classes = [ChatRoomPermissions] + # permission_classes = [ChatRoomPermissions] TODO: samu fai le permissioni + permission_classes = (IsAuthenticatedUserProfile,) - @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter('Authorization', openapi.IN_HEADER, description="OAuth2 Token", type=openapi.TYPE_STRING) - ] - ) def get_queryset(self): - room_id = self.kwargs.get(self.lookup_url_kwarg) - return ChatMessage.objects.filter(room_id=room_id) - + try: + room_id = self.kwargs.get(self.lookup_url_kwarg) + logger.info(f"GetChatMessage: Attempting to get messages for room_id={room_id}") + if not room_id: + logger.error("Missing room_id parameter in request") + return ChatMessage.objects.none() + + user = self.request.user + + # First verify the user is a member of this room + if isinstance(user, AnonymousUser) or not hasattr(user, 'user_id'): + logger.warning("Unauthenticated user attempted to access chat messages") + return ChatMessage.objects.none() + + # Check membership directly in the view + room_exists = ChatRoom.objects.filter( + room_id=room_id, + users__user_id=user.user_id + ).exists() + + if not room_exists: + logger.warning(f"User {user} attempted to access messages in room {room_id} but is not a member") + return ChatMessage.objects.none() + + # Get user's blocked users using the new BlockedUser model + try: + user_profile = UserProfile.objects.get(user_id=user.user_id) + # Get all users blocked by this user + blocked_relationships = BlockedUser.objects.filter(blocker=user_profile).select_related('blocked') + blocked_user_ids = [relationship.blocked.user_id for relationship in blocked_relationships] + except UserProfile.DoesNotExist: + blocked_user_ids = [] + + # User is a member, return messages excluding blocked users + messages = ChatMessage.objects.filter(room__room_id=room_id).exclude(sender__user_id__in=blocked_user_ids) + logger.info(f"Found {messages.count()} messages in room {room_id} (excluding blocked users)") + return messages + + except Exception as e: + import traceback + logger.error(f"ERROR in get_queryset: {str(e)}\n{traceback.format_exc()}") + return ChatMessage.objects.none() + + # def list(self, request, *args, **kwargs): + # try: + # # Use parent implementation but catch any exceptions + # return super().list(request, *args, **kwargs) + # except Exception as e: + # logger.error(f"Error listing chat messages: {str(e)}") + # # Return empty list on error + # return Response([], status=status.HTTP_200_OK) -class GetChatInfo(generics.RetrieveAPIView): +class GetChatInfo(generics.RetrieveUpdateDestroyAPIView): serializer_class = chat_roomSerializer lookup_url_kwarg = 'room_id' + lookup_field = 'room_id' + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) def get_queryset(self): room_id = self.kwargs.get(self.lookup_url_kwarg) @@ -65,82 +126,174 @@ def get_queryset(self): return ChatRoom.objects.filter(room_id=room_id) class new_user(generics.ListCreateAPIView): - serializer_class = userSerializer - queryset = UserProfile.objects.all() - - def get_permissions(self): - if self.request.method == 'POST': - self.permission_classes = [] # No need for permissions as authentication will handle it - self.authentication_classes = [ServiceAuthentication] - else: - self.permission_classes = (permissions.IsAuthenticated,) - self.authentication_classes = (JWTAuthentication,) - return super().get_permissions() - - def perform_create(self, serializer): - # You can add additional logging for audit purposes - # logger.info(f"Creating new user from service: {self.request.headers.get('User-Agent')}") - return serializer.save() + serializer_class = userCreateSerializer + queryset = UserProfile.objects.all() + def get_permissions(self): + if self.request.method == 'POST': + self.permission_classes = [] # No need for permissions as authentication will handle it + self.authentication_classes = [ServiceAuthentication] + else: + self.permission_classes = (IsAuthenticatedUserProfile,) + self.authentication_classes = (JWTAuthentication,) + return super().get_permissions() + + def perform_create(self, serializer): + # You can add additional logging for audit purposes + # logger.info(f"Creating new user from service: {self.request.headers.get('User-Agent')}") + return serializer.save() class CreateChat(generics.ListCreateAPIView): + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) serializer_class = chat_roomSerializer queryset = ChatRoom.objects.all() + + def perform_create(self, serializer): + """Override to add bidirectional blocking checks when creating chats""" + creator = self.request.user + user_ids = self.request.data.get('users', []) + + # Get creator's user profile + try: + creator_profile = UserProfile.objects.get(user_id=creator.user_id) + # Get users blocked by creator using new BlockedUser model + blocked_relationships = BlockedUser.objects.filter(blocker=creator_profile).select_related('blocked') + blocked_user_ids = [relationship.blocked.user_id for relationship in blocked_relationships] + + # Filter out users that the creator has blocked + filtered_user_ids = [uid for uid in user_ids if uid not in blocked_user_ids] + + if len(filtered_user_ids) != len(user_ids): + logger.warning(f"User {creator.user_id} attempted to create chat with blocked users. Filtered {len(user_ids) - len(filtered_user_ids)} blocked users.") + + # Also check if any of the remaining users have blocked the creator + users_to_add = UserProfile.objects.filter(user_id__in=filtered_user_ids) + final_user_ids = [] + + for user_to_add in users_to_add: + # Check if user_to_add has blocked the creator using new model + if not BlockedUser.objects.filter(blocker=user_to_add, blocked=creator_profile).exists(): + final_user_ids.append(user_to_add.user_id) + else: + logger.warning(f"User {user_to_add.user_id} has blocked creator {creator.user_id}, cannot add to chat") + + # Update the serializer data with final filtered users + mutable_data = self.request.data.copy() + mutable_data['users'] = final_user_ids + serializer.validated_data['users'] = final_user_ids + + except UserProfile.DoesNotExist: + logger.error(f"Creator profile not found for user {creator.user_id}") + except Exception as e: + logger.error(f"Error checking blocked users in CreateChat: {str(e)}") + + return serializer.save() class GetChats(generics.ListAPIView): - serializer_class = chat_roomSerializer - permission_classes = [IsAuthenticated] - filter_backends = [DjangoFilterBackend] - filterset_fields = ['users__user_id'] - queryset = ChatRoom.objects.all() + serializer_class = chat_roomSerializer + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + + def get_queryset(self): + user = self.request.user + + # Gestione sicura di utenti non autenticati + if isinstance(user, AnonymousUser) or not hasattr(user, 'user_id'): + logger.warning("Unauthenticated user attempted to access chats") + return ChatRoom.objects.none() + + # Log per debugging + logger.info(f"Retrieving chats for user: {user.user_id} ({getattr(user, 'username', 'unknown')})") + + try: + # Get user's blocked users using new BlockedUser model + try: + user_profile = UserProfile.objects.get(user_id=user.user_id) + blocked_relationships = BlockedUser.objects.filter(blocker=user_profile).select_related('blocked') + blocked_user_ids = [relationship.blocked.user_id for relationship in blocked_relationships] + except UserProfile.DoesNotExist: + blocked_user_ids = [] + + # Get chats where user is a member + user_chats = ChatRoom.objects.filter(users__user_id=user.user_id) + + # Filter out chats that contain only blocked users (excluding the current user) + filtered_chats = [] + for chat in user_chats: + # Get all users in this chat except the current user + other_users = chat.users.exclude(user_id=user.user_id) + # Check if there are any non-blocked users in the chat + non_blocked_users = other_users.exclude(user_id__in=blocked_user_ids) + + # Include the chat if: + # 1. It has non-blocked users, OR + # 2. It's a single-user chat (only the current user) + if non_blocked_users.exists() or not other_users.exists(): + filtered_chats.append(chat.room_id) + + return ChatRoom.objects.filter(room_id__in=filtered_chats) + + except Exception as e: + logger.error(f"Error retrieving chats for user {user.user_id}: {str(e)}") + return ChatRoom.objects.none() class AddUsersToChat(generics.UpdateAPIView): - serializer_class = chat_roomSerializer - lookup_url_kwarg = 'room_id' - - def get_queryset(self): - room_id = self.kwargs.get(self.lookup_url_kwarg) - user = self.request.user - - if isinstance(user, AnonymousUser): - raise ValueError('User is not authenticated') - - if not ChatRoom.objects.filter(room_id=room_id, users=user).exists(): - raise ValueError('User is not in the room') - return ChatRoom.objects.none() - - return ChatRoom.objects.filter(room_id=room_id) + serializer_class = chat_roomSerializer + lookup_url_kwarg = 'room_id' -class CreateChannelGroupView(APIView): - def post(self, request, *args, **kwargs): - room_id = request.data.get('room_id') - user_ids = request.data.get('user_ids', []) + def get_queryset(self): + room_id = self.kwargs.get(self.lookup_url_kwarg) + user = self.request.user - if not room_id: - return Response({'error': 'Room name is required'}, status=status.HTTP_400_BAD_REQUEST) + if isinstance(user, AnonymousUser): + raise ValueError('User is not authenticated') - try: - room = ChatRoom.objects.get(room_id=room_id) - except ChatRoom.DoesNotExist: - return Response({'error': 'Room does not exist'}, status=status.HTTP_404_NOT_FOUND) + if not ChatRoom.objects.filter(room_id=room_id, users=user).exists(): + raise ValueError('User is not in the room') + return ChatRoom.objects.none() - users = UserProfile.objects.filter(user_id__in=user_ids) - if not users.exists(): - return Response({'error': 'No valid users found with user_id: ' + user_ids}, status=status.HTTP_400_BAD_REQUEST) + return ChatRoom.objects.filter(room_id=room_id) - room.users.add(*users) - room.save() - - channel_layer = get_channel_layer() - group_name = f'chat_{room.room_name}' - - for user in users: - async_to_sync(channel_layer.group_add)( - group_name, - f'user_{user.user_id}' - ) - - return Response({'message': 'Users added to group'}, status=status.HTTP_200_OK) + def perform_update(self, serializer): + """Override to add blocking checks when adding users to existing chats""" + user = self.request.user + user_ids = self.request.data.get('users', []) + + # Get current user's blocked users using new BlockedUser model + try: + user_profile = UserProfile.objects.get(user_id=user.user_id) + blocked_relationships = BlockedUser.objects.filter(blocker=user_profile).select_related('blocked') + blocked_user_ids = [relationship.blocked.user_id for relationship in blocked_relationships] + + # Filter out blocked users from the user_ids list + filtered_user_ids = [uid for uid in user_ids if uid not in blocked_user_ids] + + if len(filtered_user_ids) != len(user_ids): + logger.warning(f"User {user.user_id} attempted to add blocked users to chat. Filtered {len(user_ids) - len(filtered_user_ids)} blocked users.") + + # Also check if any of the users to be added have blocked the current user + users_to_add = UserProfile.objects.filter(user_id__in=filtered_user_ids) + final_user_ids = [] + + for user_to_add in users_to_add: + # Check if user_to_add has blocked the current user using new model + if not BlockedUser.objects.filter(blocker=user_to_add, blocked=user_profile).exists(): + final_user_ids.append(user_to_add.user_id) + else: + logger.warning(f"User {user_to_add.user_id} has blocked {user.user_id}, cannot add to chat") + + # Update the serializer data with final filtered users + mutable_data = self.request.data.copy() + mutable_data['users'] = final_user_ids + serializer.validated_data['users'] = final_user_ids + + except UserProfile.DoesNotExist: + logger.error(f"User profile not found for user {user.user_id}") + except Exception as e: + logger.error(f"Error checking blocked users in AddUsersToChat: {str(e)}") + + return serializer.save() class DownloadChatRoomData(APIView): def get(self, request): @@ -168,6 +321,437 @@ def get(self, request): response = HttpResponse(data, headers) return Response(response, status=status.HTTP_200_OK) -@csrf_exempt -def health_check(request): - return JsonResponse({'status': 'ok'}) \ No newline at end of file +class BlockUser(APIView): + """ + API endpoint to block a user + + post: + Blocks a user + + Parameters: + - user_id: the id of the user to block + + Response: + - 200 OK: if the request is successful + - 400 Bad Request: if the request is invalid + delete: + Unblocks a user + + Parameters: + - user_id: the id of the user to unblock + + Response: + - 200 OK: if the request is successful + - 400 Bad Request: if the request is invalid + """ + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) + def post(self, request, *args, **kwargs): + try: + # Get the user id from url + user_id = self.kwargs.get('user_id') + + if not user_id: + return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + toBlock = get_object_or_404(UserProfile, user_id=user_id) + user = request.user + + # Check if user is trying to block themselves + if user.user_id == user_id: + return Response({'error': 'You cannot block yourself'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if user is already blocked + if BlockedUser.objects.filter(blocker=user, blocked=toBlock).exists(): + return Response({'error': 'User is already blocked'}, status=status.HTTP_400_BAD_REQUEST) + + # Create the blocking relationship using the new model + blocked_relationship = BlockedUser.objects.create( + blocker=user, + blocked=toBlock, + reason=request.data.get('reason', '') # Optional reason + ) + + logger.info(f"User {user.user_id} blocked user {toBlock.user_id}") + return Response({ + 'message': f'User {toBlock.username} blocked successfully', + 'blocked_at': blocked_relationship.blocked_at.isoformat(), + 'block_id': str(blocked_relationship.id) + }, status=status.HTTP_200_OK) + + except UserProfile.DoesNotExist: + return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error blocking user: {str(e)}") + return Response({'error': 'An error occurred while blocking the user'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def delete(self, request, *args, **kwargs): + try: + # Get the user id from url + user_id = self.kwargs.get('user_id') + + if not user_id: + return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + toUnblock = get_object_or_404(UserProfile, user_id=user_id) + user = request.user + + # Check if user is trying to unblock themselves + if user.user_id == user_id: + return Response({'error': 'You cannot unblock yourself'}, status=status.HTTP_400_BAD_REQUEST) + + # Find and delete the blocking relationship + blocked_relationship = BlockedUser.objects.filter(blocker=user, blocked=toUnblock).first() + if not blocked_relationship: + return Response({'error': 'User is not blocked'}, status=status.HTTP_400_BAD_REQUEST) + + blocked_relationship.delete() + + logger.info(f"User {user.user_id} unblocked user {toUnblock.user_id}") + return Response({ + 'message': f'User {toUnblock.username} unblocked successfully' + }, status=status.HTTP_200_OK) + + except UserProfile.DoesNotExist: + return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error unblocking user: {str(e)}") + return Response({'error': 'An error occurred while unblocking the user'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get(self, request): + user = request.user + if not user.is_authenticated: + return Response({'error': 'User is not authenticated'}, status=status.HTTP_401_UNAUTHORIZED) + + # Get all users that this user has blocked using the new BlockedUser model + blocked_relationships = BlockedUser.objects.filter(blocker=user).select_related('blocked') + + if not blocked_relationships.exists(): + return Response({'message': 'No blocked users found'}, status=status.HTTP_404_NOT_FOUND) + + # Extract the blocked users from the relationships + blocked_users = [relationship.blocked for relationship in blocked_relationships] + blocked_users_data = userBlockedSerializer(blocked_users, many=True).data + + # Return the list of blocked users with additional metadata + return Response({ + 'blocked_users': blocked_users_data, + 'count': len(blocked_users_data) + }, status=status.HTTP_200_OK) + + +class allBlockedUsers(generics.ListAPIView): + """ + API endpoint to get all blocked users + + get: + Retrieves a list of all blocked users for the authenticated user + + Response: + - 200 OK: if the request is successful + - 401 Unauthorized: if the user is not authenticated + """ + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) + serializer_class = userBlockedSerializer + + def get_queryset(self): + user = self.request.user + if not user.is_authenticated: + return UserProfile.objects.none() + + # Get all users blocked by the current user using the new BlockedUser model + blocked_relationships = BlockedUser.objects.filter(blocker=user).select_related('blocked') + blocked_user_ids = [relationship.blocked.user_id for relationship in blocked_relationships] + + return UserProfile.objects.filter(user_id__in=blocked_user_ids) + +class ChatMediaUpload(APIView): + """ + Upload media files (images, videos, files) for chat messages. + Similar to AvatarManager but for chat media uploads. + + POST: Upload a media file and create a chat message + Parameters: + - room_id (int): The ID of the chat room + - media_type (str): Type of media ('image', 'video', 'file') + - file: The media file to upload + - message (str, optional): Additional text message + + Returns: + 200 OK: Media uploaded successfully with message details + 400 Bad Request: Invalid request data or file type + 403 Forbidden: User not authorized to post in this room + 404 Not Found: Chat room not found + """ + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) + parser_classes = (MultiPartParser, FormParser) + + def post(self, request): + try: + room_id = request.data.get('room_id') + media_type = request.data.get('media_type') + uploaded_file = request.FILES.get('file') + message_text = request.data.get('message', '') + + # Validate required fields + if not room_id: + return Response({'error': 'room_id is required'}, status=status.HTTP_400_BAD_REQUEST) + if not media_type: + return Response({'error': 'media_type is required'}, status=status.HTTP_400_BAD_REQUEST) + if not uploaded_file: + return Response({'error': 'file is required'}, status=status.HTTP_400_BAD_REQUEST) + if media_type not in ['image', 'video', 'file']: + return Response({'error': 'media_type must be image, video, or file'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if user is member of the chat room + user = request.user + try: + chat_room = ChatRoom.objects.get(room_id=room_id) + if not chat_room.users.filter(user_id=user.user_id).exists(): + return Response({'error': 'You are not a member of this chat room'}, status=status.HTTP_403_FORBIDDEN) + except ChatRoom.DoesNotExist: + return Response({'error': 'Chat room not found'}, status=status.HTTP_404_NOT_FOUND) + + # Validate file type and size based on media_type + file_size = uploaded_file.size + max_size = 10 * 1024 * 1024 # 10MB default + + if media_type == 'image': + valid_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] + if uploaded_file.content_type not in valid_types: + return Response({'error': 'Invalid image type. Allowed: JPEG, PNG, GIF, WebP'}, status=status.HTTP_400_BAD_REQUEST) + max_size = 5 * 1024 * 1024 # 5MB for images + elif media_type == 'video': + valid_types = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'] + if uploaded_file.content_type not in valid_types: + return Response({'error': 'Invalid video type. Allowed: MP4, WebM, OGG, MOV'}, status=status.HTTP_400_BAD_REQUEST) + max_size = 50 * 1024 * 1024 # 50MB for videos + elif media_type == 'file': + # Allow most common file types but exclude potentially dangerous ones + blocked_types = ['application/x-executable', 'application/x-msdownload', 'application/x-dosexec'] + if uploaded_file.content_type in blocked_types: + return Response({'error': 'File type not allowed for security reasons'}, status=status.HTTP_400_BAD_REQUEST) + max_size = 20 * 1024 * 1024 # 20MB for general files + + if file_size > max_size: + return Response({ + 'error': f'File size too large. Maximum allowed: {max_size // (1024 * 1024)}MB' + }, status=status.HTTP_400_BAD_REQUEST) + + # Create the chat message with the uploaded file + chat_message = ChatMessage( + room=chat_room, + sender=user, + message_type=media_type, + message=message_text + ) + + # Set the appropriate field based on media type + if media_type == 'image': + chat_message.image = uploaded_file + elif media_type == 'video': + chat_message.video = uploaded_file + elif media_type == 'file': + chat_message.file = uploaded_file + + chat_message.save() + + # Send WebSocket notification to chat room members + channel_layer = get_channel_layer() + message_data = { + 'message_id': chat_message.message_id, + 'room_id': room_id, + 'sender_id': user.user_id, + 'sender_username': user.username, + 'message_type': media_type, + 'message': message_text, + 'content': chat_message.content, + 'timestamp': chat_message.timestamp.isoformat(), + } + + async_to_sync(channel_layer.group_send)( + f'chat_{room_id}', + { + 'type': 'chat_message', + 'message': message_data + } + ) + + return Response({ + 'message': f'{media_type.capitalize()} uploaded successfully', + 'message_id': chat_message.message_id, + 'content_url': chat_message.content, + 'message_type': media_type + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"Error uploading chat media: {str(e)}") + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get(self, request): + """ + Get media file by message_id for download/viewing + """ + try: + message_id = request.query_params.get('message_id') + if not message_id: + return Response({'error': 'message_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + # Get the message and verify user has access + try: + chat_message = ChatMessage.objects.get(message_id=message_id) + + # Check if user is member of the chat room + user = request.user + if not chat_message.room.users.filter(user_id=user.user_id).exists(): + return Response({'error': 'You do not have access to this message'}, status=status.HTTP_403_FORBIDDEN) + + # Return the file URL + file_url = chat_message.content + if file_url: + return Response({ + 'message_id': message_id, + 'file_url': file_url, + 'message_type': chat_message.message_type, + 'filename': os.path.basename(file_url) if file_url else None + }, status=status.HTTP_200_OK) + else: + return Response({'error': 'No media file found for this message'}, status=status.HTTP_404_NOT_FOUND) + + except ChatMessage.DoesNotExist: + return Response({'error': 'Message not found'}, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + logger.error(f"Error retrieving chat media: {str(e)}") + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class ChatMediaManager(APIView): + """ + Media file management for chat messages - similar to AvatarManager pattern. + + GET: Retrieve media file information by message_id + DELETE: Delete a media message (only by sender or room admin) + """ + authentication_classes = [JWTAuth] + permission_classes = (IsAuthenticatedUserProfile,) + + def get(self, request): + """Get media file information and URL""" + try: + message_id = request.query_params.get('message_id') + if not message_id: + return Response({'error': 'message_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + chat_message = ChatMessage.objects.get(message_id=message_id) + + # Check if user has access to this message + user = request.user + if not chat_message.room.users.filter(user_id=user.user_id).exists(): + return Response({'error': 'You do not have access to this message'}, status=status.HTTP_403_FORBIDDEN) + + # Check if this is a media message + if chat_message.message_type not in ['image', 'video', 'file']: + return Response({'error': 'This message does not contain media'}, status=status.HTTP_400_BAD_REQUEST) + + # Get file info + file_field = None + filename = None + file_size = None + + if chat_message.message_type == 'image' and chat_message.image: + file_field = chat_message.image + elif chat_message.message_type == 'video' and chat_message.video: + file_field = chat_message.video + elif chat_message.message_type == 'file' and chat_message.file: + file_field = chat_message.file + + if file_field: + filename = os.path.basename(file_field.name) + try: + file_size = file_field.size + except: + file_size = None + + return Response({ + 'message_id': message_id, + 'message_type': chat_message.message_type, + 'file_url': chat_message.content, + 'filename': filename, + 'file_size': file_size, + 'sender': chat_message.sender.username, + 'timestamp': chat_message.timestamp.isoformat(), + 'room_id': chat_message.room.room_id, + 'text_message': chat_message.message + }, status=status.HTTP_200_OK) + + except ChatMessage.DoesNotExist: + return Response({'error': 'Message not found'}, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + logger.error(f"Error retrieving chat media info: {str(e)}") + return Response({'error': 'An internal error occurred. Please contact support.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def delete(self, request): + """Delete a media message (only by sender or room admin)""" + try: + message_id = request.data.get('message_id') + if not message_id: + return Response({'error': 'message_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + chat_message = ChatMessage.objects.get(message_id=message_id) + user = request.user + + # Check if user has access to this message + if not chat_message.room.users.filter(user_id=user.user_id).exists(): + return Response({'error': 'You do not have access to this message'}, status=status.HTTP_403_FORBIDDEN) + + # Check if user can delete this message (sender or room admin) + is_sender = chat_message.sender.user_id == user.user_id + is_admin = False + + try: + member = ChatMember.objects.get(user=user, chat_room=chat_message.room) + is_admin = member.role in ['admin', 'moderator'] + except ChatMember.DoesNotExist: + pass + + if not (is_sender or is_admin): + return Response({'error': 'You can only delete your own messages or you must be a room admin'}, status=status.HTTP_403_FORBIDDEN) + + # Store info before deletion + room_id = chat_message.room.room_id + deleted_message_id = chat_message.message_id + + # Delete the message (this will also delete the file due to Django's file field cleanup) + chat_message.delete() + + # Send WebSocket notification about message deletion + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + f'chat_{room_id}', + { + 'type': 'message_deleted', + 'message': { + 'deleted_message_id': deleted_message_id, + 'deleted_by': user.username, + 'room_id': room_id + } + } + ) + + return Response({ + 'message': 'Media message deleted successfully', + 'deleted_message_id': deleted_message_id + }, status=status.HTTP_200_OK) + + except ChatMessage.DoesNotExist: + return Response({'error': 'Message not found'}, status=status.HTTP_404_NOT_FOUND) + + except Exception as e: + logger.error(f"Error deleting chat media: {str(e)}") + return Response({'error': 'An internal error occurred. Please contact support.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/Back-End/chat/requirements.txt b/Back-End/chat/requirements.txt index 9a70232c..a83322ac 100644 --- a/Back-End/chat/requirements.txt +++ b/Back-End/chat/requirements.txt @@ -1,28 +1,28 @@ amqp==5.3.1 asgiref==3.8.1 async-timeout==5.0.1 -attrs==24.2.0 +attrs==25.1.0 autobahn==24.4.2 Automat==24.8.1 billiard==4.2.1 celery==5.4.0 -certifi==2024.8.30 +certifi==2025.1.31 cffi==1.17.1 channels-redis==4.2.1 -charset-normalizer==3.4.0 -click==8.1.7 +charset-normalizer==3.4.1 +click==8.1.8 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 constantly==23.10.4 -cryptography==44.0.0 +cryptography==44.0.2 daphne==4.1.2 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 +django==5.2.4 +django-cors-headers==4.7.0 +django-filter==25.1 django-oauth-toolkit==3.0.1 django-redis==5.4.0 -djangorestframework==3.15.2 +djangorestframework>=3.16.0 hyperlink==21.0.0 idna==3.10 incremental==24.7.2 @@ -30,27 +30,56 @@ jwcrypto==1.5.6 kombu==5.4.2 oauthlib==3.2.2 prompt_toolkit==3.0.48 -psycopg==3.2.5 -psycopg-binary==3.2.5 +psycopg==3.2.9 +psycopg-binary==3.2.9 pyasn1==0.6.1 pyasn1_modules==0.4.1 pycparser==2.22 pyOpenSSL==24.3.0 python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 +redis==5.2.1 +requests>=2.32.4 service-identity==24.2.0 -six==1.16.0 -sqlparse==0.5.2 +six==1.17.0 +sqlparse==0.5.3 Twisted==24.11.0 txaio==23.1.1 typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 +tzdata==2025.1 +urllib3>=2.5.0 vine==5.1.0 wcwidth==0.2.13 -zope.interface==7.2 -djangorestframework-simplejwt==5.4.0 +zope-interface==7.2 +djangorestframework-simplejwt==5.5.0 # via -r Back-End/requirements.in -drf-yasg==1.21.8 - # via -r Back-End/requirements.in \ No newline at end of file +drf-yasg==1.21.9 + # via -r Back-End/requirements.in +inflection==0.5.1 + # via + # -r requirements.in + # drf-yasg +packaging==24.2 + # via + # -r requirements.in + # build + # drf-yasg + # gunicorn + # pip-review +pytz==2025.1 + # via + # -r requirements.in + # django-loki + # drf-yasg +pillow==11.1.0 +pyyaml==6.0.2 + # via + # -r requirements.in + # drf-yasg +uritemplate==4.1.1 + # via + # -r requirements.in + # coreapi + # drf-yasg +whitenoise==6.9.0 +django-prometheus==2.3.1 +prometheus-client==0.21.1 diff --git a/Back-End/Dockers/login_db/init.sql b/Back-End/common_settings.py similarity index 100% rename from Back-End/Dockers/login_db/init.sql rename to Back-End/common_settings.py diff --git a/Back-End/login/Dockerfile b/Back-End/login/Dockerfile index e6a2350b..4841a6cb 100644 --- a/Back-End/login/Dockerfile +++ b/Back-End/login/Dockerfile @@ -1,5 +1,5 @@ # Use a stable Python version -FROM python:3.11-alpine AS base +FROM python:3.13-alpine AS base RUN apk add --no-cache postgresql-libs bash @@ -16,6 +16,9 @@ COPY --from=builder /usr/local /usr/local COPY . /app WORKDIR /app +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static + COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/Back-End/login/entrypoint.sh b/Back-End/login/entrypoint.sh index b3aa9cbf..a1d32d34 100644 --- a/Back-End/login/entrypoint.sh +++ b/Back-End/login/entrypoint.sh @@ -2,8 +2,11 @@ set -e +# collect static files +python manage.py collectstatic --noinput + # migrations at startup -python manage.py makemigrations +python manage.py makemigrations python manage.py makemigrations my_login python manage.py migrate @@ -11,6 +14,10 @@ python manage.py migrate if [ "$DEBUG" = "True" ]; then python manage.py runserver 0.0.0.0:8000 else - # For production use Gunicorn (WSGI server) - gunicorn login.wsgi:application --bind 0.0.0.0:8000 --root-path /api/login || echo "Gunicorn failed to start" + # For production use Gunicorn (WSGI server) with improved logging + gunicorn login.wsgi:application --bind 0.0.0.0:8000 \ + --log-level=debug \ + --capture-output \ + --access-logfile - \ + --error-logfile - || { echo "Gunicorn failed to start"; exit 1; } fi \ No newline at end of file diff --git a/Back-End/login/login/settings.py b/Back-End/login/login/settings.py index 2ad46f05..2593bf43 100644 --- a/Back-End/login/login/settings.py +++ b/Back-End/login/login/settings.py @@ -13,226 +13,268 @@ from datetime import timedelta from pathlib import Path import os +from datetime import datetime import secrets -# Aggiungi queste impostazioni -FORCE_SCRIPT_NAME = '/api/login' - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-kp7qs)0l1ie$%muo93+829po%pe9*gz8z8ah6dy0)cskj-5l*c') +FIELD_ENCRYPTION_KEY = os.getenv('FIELD_ENCRYPTION_KEY', 'Hog-t3arjmkEOl49iagysJdpmvu9eJLkbSgDn-TNNLU=') #'django-insecure-kp7qs)0l1ie$%muo93+829po%pe9*gz8z8ah6dy0)cskj-5l*c' API_KEY = os.getenv('API_KEY', '123') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', True) -def ensure_scheme(url): - if url and not url.startswith(('http://', 'https://')): - return f"http://{url}" - return url - +# Simple microservices definition Microservices = { - 'Login': ensure_scheme(os.getenv('LOGIN_SERVICE', 'http://localhost:8000')), - 'Chat': ensure_scheme(os.getenv('CHAT_SERVICE', 'http://localhost:8001')), - 'Users': ensure_scheme(os.getenv('USERS_SERVICE', 'http://localhost:8002')), - 'Notifications': ensure_scheme(os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003')), - 'Pong': ensure_scheme(os.getenv('PONG_SERVICE', 'http://localhost:8004')), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), } K8S_ALLOWED_HOSTS = os.environ.get('K8S_ALLOWED_HOSTS', '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16').split(',') -ALLOWED_HOSTS = [ - 'localhost', - 'localhost:3000', - '127.0.0.1', - '[::1]', - 'trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + K8S_ALLOWED_HOSTS + [ - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] - - -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] -# CORS_ALLOW_ALL_ORIGINS = True +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN + +# Add additional service hosts from environment if provided +# if os.environ.get('K8S_SERVICE_HOSTS'): +# additional_hosts = os.environ.get('K8S_SERVICE_HOSTS').split(',') +# K8S_SERVICE_HOSTS.extend([host.strip() for host in additional_hosts if host.strip()]) + +ALLOWED_HOSTS = ['*'] +# 'localhost', +# 'localhost:3000', +# '127.0.0.1', +# '[::1]', +# 'trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS + +# CORS_ALLOWED_ORIGINS = [ +# 'http://localhost:3000', +# 'http://localhost', +# 'http://127.0.0.1', +# 'http://[::1]', +# 'https://trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS_WITH_SCHEME +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + 'http://localhost:3000', + 'http://localhost', + 'http://127.0.0.1', + 'http://[::1]', + 'https://trascendence.42firenze.it', + # Aggiungi IP privati per HTTPS + 'https://10.0.2.15', # L'IP specifico della tua VM + # O meglio ancora, patterns per tutti gli IP privati: + 'http://10.*', + 'https://10.*', + 'http://192.168.*', + 'https://192.168.*', + 'http://172.16.*', + 'https://172.16.*', + 'http://172.17.*', + 'https://172.17.*', + 'http://172.18.*', + 'https://172.18.*', + # Continua per tutti i range 172.16.x.x fino a 172.31.x.x + 'http://localhost:8443', + 'http://127.0.0.1:8443', + 'http://10.0.2.15:8443', + 'http://10.0.2.15.xip.io:8443', + 'http://10.11.*.xip.io:8443', + Microservices['Login'], + Microservices['Chat'], + Microservices['Users'], + Microservices['Notifications'], + Microservices['Pong'], +] + K8S_SERVICE_HOSTS_WITH_SCHEME CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', ] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'rest_framework_simplejwt', - 'my_login', - 'corsheaders', - 'oauth2_provider', + 'django_prometheus', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'drf_yasg', + 'my_login', + 'corsheaders', + 'oauth2_provider', ] MIDDLEWARE = [ - 'my_login.middleware.HealthCheckMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django_prometheus.middleware.PrometheusBeforeMiddleware', # Add at the top + 'my_login.middleware.HealthCheckMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +# Add at the end of middleware list +MIDDLEWARE.append('django_prometheus.middleware.PrometheusAfterMiddleware') + ROOT_URLCONF = 'login.urls' TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] WSGI_APPLICATION = 'login.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'login_db'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5435'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), - } -} +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'login_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5437'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } #user model AUTH_USER_MODEL = 'my_login.AppUser' REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), } # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, ] OAUTH2_PROVIDER = { - 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, - 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, - 'REFRESH_TOKEN_EXPIRE_SECONDS': 36000, - 'ROTATE_REFRESH_TOKENS': True, - 'SCOPES': { - 'read': 'Read scope', - 'write': 'Write scope', - 'groups': 'Access to your groups', - }, - # 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', - 'OAUTH2_VALIDATOR_CLASS': 'my_login.validations.CustomOAuth2Validator', + 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, + 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, + 'REFRESH_TOKEN_EXPIRE_SECONDS': 36000, + 'ROTATE_REFRESH_TOKENS': True, + 'SCOPES': { + 'read': 'Read scope', + 'write': 'Write scope', + 'groups': 'Access to your groups', + }, + # 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', + 'OAUTH2_VALIDATOR_CLASS': 'my_login.validations.CustomOAuth2Validator', } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), - 'ROTATE_REFRESH_TOKENS': True, - 'BLACKLIST_AFTER_ROTATION': True, - 'ALGORITHM': 'HS256', - # 'ISSUER': 'login', - 'SIGNING_KEY': SECRET_KEY, - 'VERIFYING_KEY': None, - 'AUTH_HEADER_TYPES': ('Bearer',), - 'USER_ID_FIELD': 'user_id', - 'USER_ID_CLAIM': 'user_id', - 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), - 'TOKEN_TYPE_CLAIM': 'token_type', + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, + 'ALGORITHM': 'HS256', + # 'ISSUER': 'login', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'user_id', + 'USER_ID_CLAIM': 'user_id', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', } # Internationalization @@ -247,16 +289,19 @@ def ensure_scheme(url): USE_TZ = True client = { - 'CLIENT_ID' : os.getenv('CLIENT_ID', ''), - 'CLIENT_SECRET' : os.getenv('CLIENT_SECRET', ''), + 'CLIENT_ID' : os.getenv('CLIENT_ID', ''), + 'CLIENT_SECRET' : os.getenv('CLIENT_SECRET', ''), } - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Changed from '/static/' -STATIC_URL = 'static/' -STATIC_ROOT = '/home/lollo/Documents/challenge_fides/Back-End/login/staticfiles' +# Add this to define where Django should look for static files +#STATICFILES_DIRS = [ +# os.path.join(BASE_DIR, "static"), +#] # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field @@ -264,47 +309,229 @@ def ensure_scheme(url): SERVICE_PASSWORD = os.getenv('SERVICE_PASSWORD','123') # this is the password that the service will use to authenticate itself to the OAuth2 server -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - }, - }, - 'root': { - 'handlers': ['console'], - 'level': 'DEBUG', - }, +ADMIN = { + 'username': os.getenv('ADMIN_USERNAME', 'admin'), + 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), + 'password': os.getenv('ADMIN_PASSWORD', 'admin'), } -ADMIN = { - 'username': os.getenv('ADMIN_USERNAME', 'admin'), - 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), - 'password': os.getenv('ADMIN_PASSWORD', 'admin'), +OAUTH2_PROVIDERS = { + 'GOOGLE' : { + 'redirect_uri': 'http://trascendence.42firenze.it/api/login/login/oauth/callback/google/', + 'authorization_url': "https://accounts.google.com/o/oauth2/v2/auth", + 'token_url': "https://oauth2.googleapis.com/token", + 'scope': "openid email profile", + 'user_info_url': "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", + 'client_id': os.getenv('GOOGLE_CLIENT_ID', 'pippo'), + 'client_secret': os.getenv('GOOGLE_CLIENT_SECRET', 'moltosegreto'), + }, + '42' : { + 'redirect_uri': 'http://trascendence.42firenze.it/api/login/login/oauth/callback/42/', + 'authorization_url': "https://api.intra.42.fr/oauth/authorize", + 'token_url': "https://api.intra.42.fr/oauth/token", + 'scope': "public", + 'user_info_url': "https://api.intra.42.fr/v2/me", + 'client_id': os.getenv('42_CLIENT_ID', 'pippo'), + 'client_secret': os.getenv('42_CLIENT_SECRET', 'moltosegreto'), + }, } -# LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'handlers': { -# 'loki': { -# 'class': 'logging.StreamHandler', -# 'formatter': 'detailed', -# 'stream': 'ext://sys.stdout', # Sends logs to stdout for Loki -# }, -# }, -# 'loggers': { -# 'django': { -# 'handlers': ['loki'], -# 'level': 'INFO', -# 'propagate': True, -# }, -# }, -# 'formatters': { -# 'detailed': { -# 'format': '{levelname} {asctime} {module} {message}', -# 'style': '{', -# }, -# }, -# } +# ELK-Optimized Logging Configuration for login service +# This configuration provides standardized logging for the ELK Stack + +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "login"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'login': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_login': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/login/login/urls.py b/Back-End/login/login/urls.py index 0626fcf1..e42e71c4 100644 --- a/Back-End/login/login/urls.py +++ b/Back-End/login/login/urls.py @@ -1,13 +1,134 @@ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path +from django.http import JsonResponse, HttpResponse +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi from oauth2_provider import urls as oauth2_urls from oauth2_provider import views as oauth2_views from my_login.views import CustomIntrospect +schema_view = get_schema_view( + openapi.Info( + title="Login & Authentication API", + default_version='v1', + description="API for user authentication, OAuth2, and login management", + contact=openapi.Contact(email="admin@trascendence.local"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +# Custom view that serves Swagger UI with JWT authentication +def custom_swagger_view(request): + html = """ + + + Login & Authentication API Documentation + + + + + + +
+ + + + +""" + return HttpResponse(html) + urlpatterns = [ path('admin/', admin.site.urls), path('login/', include('my_login.urls')), path('o/', include(oauth2_urls)), + path('', include('django_prometheus.urls')), # Add metrics endpoint + + # Swagger/API documentation with JWT support + path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', custom_swagger_view, name='schema-swagger-ui'), + re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/schema/', schema_view.without_ui(cache_timeout=0), name='schema-json-api'), + path('health/', lambda request: JsonResponse({'status': 'healthy'}), name='health'), ] oauth2_urls = [ diff --git a/Back-End/login/login/wsgi.py b/Back-End/login/login/wsgi.py index 6683f8fc..c0d4c8af 100644 --- a/Back-End/login/login/wsgi.py +++ b/Back-End/login/login/wsgi.py @@ -13,4 +13,18 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'login.settings') +from django.core.wsgi import get_wsgi_application + +class PrefixMiddleware: + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + path = environ.get('PATH_INFO', '') + if path.startswith(self.prefix): + environ['PATH_INFO'] = path[len(self.prefix):] or '/' + return self.app(environ, start_response) + application = get_wsgi_application() +application = PrefixMiddleware(application, '/api/login') \ No newline at end of file diff --git a/Back-End/login/manage.py b/Back-End/login/manage.py old mode 100755 new mode 100644 diff --git a/Back-End/login/my_login/middleware.py b/Back-End/login/my_login/middleware.py index f111c6de..21bc3c4c 100644 --- a/Back-End/login/my_login/middleware.py +++ b/Back-End/login/my_login/middleware.py @@ -1,27 +1,46 @@ -from django.conf import settings +from math import log from rest_framework.permissions import BasePermission +import logging +from django.conf import settings +from django.db import connections # Added +from django.db.utils import OperationalError # Added +from django.http import JsonResponse -class APIKeyPermission(BasePermission): - def has_permission(self, request, view): - api_key = request.META.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False +logger = logging.getLogger('django') +class APIKeyPermission(BasePermission): def has_permission(self, request, view): api_key = request.META.get('X-API-KEY') if api_key: return api_key == settings.API_KEY return False -from django.http import JsonResponse - class HealthCheckMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Allow health check endpoint regardless of host - if request.path.endswith('/health'): - return JsonResponse({'status': 'ok'}) - return self.get_response(request) \ No newline at end of file + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Allow health check endpoint regardless of host + if request.path.endswith('/health'): # Liveness probe + return JsonResponse({'status': 'ok'}) + elif request.path.endswith('/ready'): # Readiness probe + db_ready = False + try: + # Check the default database connection + db_conn = connections['default'] + db_conn.cursor() # Attempt to get a cursor to check connectivity + # Optionally, you could execute a very simple query: + # with db_conn.cursor() as cursor: + # cursor.execute("SELECT 1") + # cursor.fetchone() + db_ready = True + except OperationalError: + logger.error("Readiness probe: Database connection failed.") + # db_ready remains False + + if db_ready: + return JsonResponse({'status': 'ready', 'database': 'ok'}) + else: + return JsonResponse({'status': 'unready', 'database': 'unavailable'}, status=503) + + return self.get_response(request) diff --git a/Back-End/login/my_login/models.py b/Back-End/login/my_login/models.py index 5c8aa131..c2173232 100644 --- a/Back-End/login/my_login/models.py +++ b/Back-End/login/my_login/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.base_user import BaseUserManager from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from encrypted_model_fields.fields import EncryptedCharField class AppUserManager(BaseUserManager): def create_user(self, email, password=None): @@ -32,6 +33,9 @@ class AppUser(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] is_staff = models.BooleanField(default=False) + has_two_factor_auth = models.BooleanField(default=False) + two_factor_secret = EncryptedCharField(max_length=255, blank=True, null=True) + samu_e_un_coglione = models.BooleanField(default=True) objects = AppUserManager() def __str__(self): return self.username diff --git a/Back-End/login/my_login/requirements.txt b/Back-End/login/my_login/requirements.txt deleted file mode 100644 index 0b19f60c..00000000 --- a/Back-End/login/my_login/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -Django==5.1.3 -djangorestframework==3.15.2 -login==0.0.6 -oauthlib==3.2.2 -Requests==2.32.3 diff --git a/Back-End/login/my_login/tests.py b/Back-End/login/my_login/tests.py index 7ce503c2..34e253f6 100644 --- a/Back-End/login/my_login/tests.py +++ b/Back-End/login/my_login/tests.py @@ -1,3 +1,50 @@ -from django.test import TestCase -# Create your tests here. +from django.test import TestCase, Client + +class MyLoginURLTests(TestCase): + def setUp(self): + self.client = Client() + + def test_register(self): + response = self.client.get('/login/register') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_login(self): + response = self.client.get('/login/login') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_logout(self): + response = self.client.get('/login/logout') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_user(self): + response = self.client.get('/login/user') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_get_csrf_token(self): + response = self.client.get('/login/get_csrf_token') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_serviceregister(self): + response = self.client.get('/login/Serviceregister') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_oauth_login(self): + response = self.client.get('/login/oauth/testprovider/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_oauth_callback(self): + response = self.client.get('/login/oauth/callback/testprovider/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_setup_2fa(self): + response = self.client.get('/login/2fa/setup/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_verify_2fa(self): + response = self.client.get('/login/2fa/verify/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_disable_2fa(self): + response = self.client.get('/login/2fa/disable/') + self.assertIn(response.status_code, [200, 401, 403]) diff --git a/Back-End/login/my_login/urls.py b/Back-End/login/my_login/urls.py index a95107a7..7278b679 100644 --- a/Back-End/login/my_login/urls.py +++ b/Back-End/login/my_login/urls.py @@ -8,4 +8,9 @@ path('user', views.UserView.as_view(), name='user'), path('get_csrf_token', views.get_csrf_token, name='get_csrf_token'), path('Serviceregister', views.ServiceRegister.as_view(), name='Serviceregister'), + path('oauth//', views.OAuthLoginView.as_view(), name='oauth_login'), + path('oauth/callback//', views.OAuthCallbackView.as_view(), name='oauth_callback'), + path('2fa/setup/', views.Setup2FAView.as_view(), name='setup_2fa'), + path('2fa/verify/', views.Verify2FALoginView.as_view(), name='verify_2fa'), + path('2fa/disable/', views.Disable2FAView.as_view(), name='disable_2fa'), ] diff --git a/Back-End/login/my_login/views.py b/Back-End/login/my_login/views.py index f5fa6a03..b62a8ecd 100644 --- a/Back-End/login/my_login/views.py +++ b/Back-End/login/my_login/views.py @@ -1,6 +1,9 @@ +from math import log +import stat from urllib.parse import urlencode from django.contrib.auth import get_user_model, login, logout from django.utils.decorators import method_decorator +from django.shortcuts import redirect from django.utils.decorators import method_decorator import requests from rest_framework.views import APIView @@ -12,7 +15,7 @@ from .errors import error_codes from oauth2_provider.models import AccessToken , Application from oauthlib.common import generate_token -from django.http import HttpRequest, JsonResponse +from django.http import HttpRequest, HttpResponseRedirect, JsonResponse from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.middleware.csrf import get_token from oauth2_provider.views import TokenView @@ -27,32 +30,41 @@ from datetime import timedelta from django.conf import settings from rest_framework_simplejwt.tokens import RefreshToken -import logging +import logging, random, string, hashlib, base64 from django.views.decorators.csrf import csrf_exempt +from django.db import transaction +import pyotp +from django_ratelimit.decorators import ratelimit +import io +import urllib.parse +import hmac +import time +import struct +import os +from django.http import HttpResponse #use rest +import requests +import secrets +import logging +import html +import json + +OAUTH2_PROVIDERS = settings.OAUTH2_PROVIDERS + +logger = logging.getLogger('light_login') def get_jwt_token_for_user(user): refresh = RefreshToken.for_user(user) return str(refresh.access_token) - - -def get_access_token(): - app = Application.objects.get(name='my_login') - - token = AccessToken.objects.create( - user=app.user, - token=generate_token(), - application=app, - scope='read write', - expires=timezone.now() + timedelta(seconds=36000), - ) - return token.token - -def CreateOnOtherServices(user): +# Update the CreateOnOtherServices function to handle errors properly +@transaction.atomic +def CreateOnOtherServices(user, **kwargs): + """Create user in all microservices with proper transaction handling""" Chat_url = Microservices['Chat'] + "/chat/new_user/" Notification_url = Microservices['Notifications'] + "/notification/add_user" User_url = Microservices['Users'] + "/user/user" pong_url = Microservices['Pong'] + "/pong/player" + jwt_token = get_jwt_token_for_user(user) headers = { 'Content-Type': 'application/json', @@ -65,55 +77,702 @@ def CreateOnOtherServices(user): 'username': user.username, 'email': user.email, } - user_response = requests.post(User_url, json=user_data, headers=headers) - if user_response.status_code != 201: - logging.error(user_response.json()) - print(user_response.json()) - raise ValueError('User service failed to create user') - chat_response = requests.post(Chat_url, json=user_data, headers=headers) - if chat_response.status_code != 201: - logging.error(chat_response.json()) - print(chat_response.json()) - raise ValueError('Chat service failed to create user') - notification_response = requests.post(Notification_url, json=user_data, headers=headers) - if notification_response.status_code != 201: - logging.error(notification_response.json()) - print(notification_response.json()) - raise ValueError('Notification service failed to create user') - pong_response = requests.post(pong_url, json=user_data, headers=headers) - if pong_response.status_code != 201: - logging.error(pong_response.json()) - raise ValueError('Pong service failed to create user') - return True + + + # Create a savepoint for potential rollback + sid = transaction.savepoint() + + try: + + # Create user in Chat service + chat_response = requests.post(Chat_url, json=user_data, headers=headers) + if chat_response.status_code != 201: + logging.error(f"Chat service error: {chat_response.status_code} - {chat_response.text}") + raise ValueError('Chat service failed to create user') + + # Create user in Notification service + notification_response = requests.post(Notification_url, json=user_data, headers=headers) + if notification_response.status_code != 201: + logging.error(f"Notification service error: {notification_response.status_code} - {notification_response.text}") + raise ValueError('Notification service failed to create user') + + # Create user in Pong service + pong_response = requests.post(pong_url, json=user_data, headers=headers) + if pong_response.status_code != 201: + logging.error(f"Pong service error: {pong_response.status_code} - {pong_response.text}") + raise ValueError('Pong service failed to create user') + + # Create user in User service + if 'given_name' in kwargs or 'family_name' in kwargs or 'picture' in kwargs: + user_data.update({ + 'first_name': kwargs.get('given_name'), + 'last_name': kwargs.get('family_name'), + 'current_avatar_url': kwargs.get('picture'), + }) + + user_response = requests.post(User_url, json=user_data, headers=headers) + if user_response.status_code != 201: + logging.error(f"User service error: {user_response.status_code} - {user_response.text}") + raise ValueError('User service failed to create user') + logger.debug(f"User {user.email} created in all microservices successfully") + logger.debug(f"Chat response: {chat_response.json()}") + logger.debug(f"Notification response: {notification_response.json()}") + logger.debug(f"Pong response: {pong_response.json()}") + logger.debug(f"User service response: {user_response.json()}") + return True + + except Exception as e: + # If any service fails, rollback the transaction including the user creation + transaction.savepoint_rollback(sid) + logging.error(f"Failed to create user in all services: {str(e)}") + raise e +@method_decorator(csrf_exempt, name='dispatch') +class OAuthLoginView(APIView): + permission_classes = (permissions.AllowAny,) + + def get(self, request, provider): + logger.info(f"OAuth2 login initiated for provider: {provider}") + + provider_config = OAUTH2_PROVIDERS.get(provider.upper()) + if not provider_config: + return Response( + {"error": f"Provider '{provider}' not configured"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Genera code_verifier e code_challenge per PKCE + code_verifier = self.generate_code_verifier() + code_challenge = self.generate_code_challenge(code_verifier) + + # Salva il code_verifier nella sessione + request.session['code_verifier'] = code_verifier + request.session['oauth_provider'] = provider.upper() + + if provider.upper() == 'GOOGLE': + redirect_url = ( + f"{provider_config.get('authorization_url')}?" + f"client_id={provider_config.get('client_id')}&" + f"redirect_uri={provider_config.get('redirect_uri')}&" + "response_type=code&" + f"scope={provider_config.get('scope')}&" + f"code_challenge={code_challenge}&" + "code_challenge_method=S256&" + f"state={code_verifier}" + ) + else: # 42 + redirect_url = ( + f"{provider_config.get('authorization_url')}?" + f"client_id={provider_config.get('client_id')}&" + f"redirect_uri={provider_config.get('redirect_uri')}&" + "response_type=code&" + f"scope={provider_config.get('scope')}&" + f"state={code_verifier}" + ) + + logger.info(f"Generated redirect URL for {provider}") + return Response({"redirect_url": redirect_url}, status=status.HTTP_200_OK) + + def generate_code_verifier(self): + """Generates a code_verifier for PKCE""" + return ''.join(random.choice(string.ascii_letters + string.digits + '-._~') for _ in range(128)) + + def generate_code_challenge(self, code_verifier): + """Generates code_challenge from code_verifier using SHA256""" + sha256_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest() + return base64.urlsafe_b64encode(sha256_hash).decode('utf-8').replace('=', '') + +@method_decorator(csrf_exempt, name='dispatch') +class OAuthCallbackView(APIView): + permission_classes = (permissions.AllowAny,) + + + def get(self, request, provider): + try: + code = request.GET.get('code') + state = request.GET.get('state') + error = request.GET.get('error') + + # โœ… DEBUG SPECIFICO PER 42 + if provider.lower() == '42': + logger.info(f"๐Ÿ”ง 42 OAuth Debug - Code: {code[:20]}..." if code else "No code") + logger.info(f"๐Ÿ”ง 42 OAuth Debug - State: {state[:20]}..." if state else "No state") + + if error: + logger.error(f'OAuth error: {error}') + return self.handle_popup_response(error=error) + + if not code: + logger.error('No authorization code received') + return self.handle_popup_response(error='no_code') + + # Get provider configuration + provider_config = settings.OAUTH2_PROVIDERS.get(provider.upper()) + if not provider_config: + logger.error(f'Unsupported provider: {provider}') + return self.handle_popup_response(error='unsupported_provider') + + # โœ… DEBUG CONFIGURAZIONE 42 + if provider.lower() == '42': + logger.info(f"๐Ÿ”ง 42 Config - Client ID: {provider_config['client_id'][:10]}...") + logger.info(f"๐Ÿ”ง 42 Config - Client Secret present: {bool(provider_config['client_secret'])}") + logger.info(f"๐Ÿ”ง 42 Config - Token URL: {provider_config['token_url']}") + logger.info(f"๐Ÿ”ง 42 Config - Redirect URI: {provider_config['redirect_uri']}") + + # recupero code_verifier dalla sessione + code_verifier = request.session.get('code_verifier') + if not code_verifier: + logger.error('No code_verifier found in session') + return self.handle_popup_response(error='no_code_verifier') + + # dati del token in base al provider + if provider.upper() == 'GOOGLE': + token_data = { + 'client_id': provider_config['client_id'], + 'client_secret': provider_config['client_secret'], + 'code': code, + 'grant_type': 'authorization_code', + 'redirect_uri': provider_config['redirect_uri'], + 'code_verifier': code_verifier, # PKCE richiesto da Google + } + else: # 42 + token_data = { + 'client_id': provider_config['client_id'], + 'client_secret': provider_config['client_secret'], + 'code': code, + 'grant_type': 'authorization_code', + 'redirect_uri': provider_config['redirect_uri'], + } + + # โœ… DEBUG TOKEN DATA PER 42 + if provider.lower() == '42': + logger.info("๐Ÿ”ง 42 Token Request Data: [REDACTED]") + logger.info(" - Sensitive fields have been redacted for security.") + + logger.info(f"Making token request to: {provider_config['token_url']}") + logger.info("Token data keys: [REDACTED]") + logger.info(" - Sensitive keys have been excluded from logging for security.") + + # Exchange code for access token + token_response = requests.post(provider_config['token_url'], data=token_data) + + logger.info(f"Token response status: {token_response.status_code}") + logger.info(f"Token response text: {token_response.text}") + + # โœ… DEBUG RISPOSTA TOKEN PER 42 + if provider.lower() == '42': + logger.error(f"๐Ÿ”ง 42 Token Response:") + logger.error(f" - Status: {token_response.status_code}") + logger.error(f" - Headers: {dict(token_response.headers)}") + logger.error(f" - Text: {token_response.text}") + + if not token_response.ok: + logger.error(f'Token exchange failed: {token_response.text}') + return self.handle_popup_response(error='token_exchange_failed') + + token_response_data = token_response.json() + access_token = token_response_data.get('access_token') + + if not access_token: + logger.error('No access token in response') + return self.handle_popup_response(error='no_access_token') + + # Get user info from provider + user_info_response = requests.get( + provider_config['user_info_url'], + headers={'Authorization': f'Bearer {access_token}'} + ) + + if not user_info_response.ok: + logger.error(f'User info request failed: {user_info_response.text}') + return self.handle_popup_response(error='user_info_failed') + + user_info = user_info_response.json() + logger.info(f"User info received: {user_info}") + + # Create or get user + if provider.lower() == 'google': + email = user_info.get('email') + username = user_info.get('name', email.split('@')[0] if email else 'user') + elif provider.lower() == '42': + email = user_info.get('email') + username = user_info.get('login', email.split('@')[0] if email else 'user') + else: + return self.handle_popup_response(error='unsupported_provider') + + if not email: + logger.error('No email found in user info') + return self.handle_popup_response(error='no_email') + + # Get or create user + user, created = AppUser.objects.get_or_create( + email=email, + defaults={'username': username} + ) + + if created: + # Set a random password for OAuth users + user.set_password(secrets.token_urlsafe(32)) + user.save() + logger.info(f"New user created: {user.email}") + + # โœ… AGGIUNGI: Crea l'utente negli altri microservizi + try: + # Prepara dati aggiuntivi per i microservizi (da user_info se disponibili) + oauth_data = {} + if provider.lower() == 'google': + oauth_data = { + 'given_name': user_info.get('given_name'), + 'family_name': user_info.get('family_name'), + 'picture': user_info.get('picture'), + } + elif provider.lower() == '42': + oauth_data = { + 'given_name': user_info.get('first_name'), + 'family_name': user_info.get('last_name'), + 'picture': user_info.get('image', {}).get('link') if user_info.get('image') else None, + } + + CreateOnOtherServices(user, **oauth_data) + logger.info(f"User {user.email} created in all microservices") + + except Exception as e: + # Se fallisce la creazione negli altri servizi, elimina l'utente locale + logger.error(f"Failed to create user in microservices: {str(e)}") + user.delete() + return self.handle_popup_response(error='microservices_creation_failed') + # Se l'utente esiste giร , non fare nulla + + else: + logger.info(f"Existing user found: {user.email}") + + # PULISCI la sessione + request.session.pop('code_verifier', None) + request.session.pop('oauth_provider', None) + + # Generate JWT tokens + refresh = RefreshToken.for_user(user) + access_token_jwt = str(refresh.access_token) + refresh_token = str(refresh) + + logger.info(f"JWT tokens generated for user: {user.email}") + + # Return popup response with tokens + return self.handle_popup_response( + access_token=access_token_jwt, + refresh_token=refresh_token, + user_id=user.user_id, + username=user.username, + email=user.email + ) + + except Exception as e: + logger.error(f'OAuth callback error: {str(e)}', exc_info=True) + return self.handle_popup_response(error='callback_failed') + + def handle_popup_response(self, access_token=None, refresh_token=None, + user_id=None, username=None, email=None, error=None): + """ + Restituisce una pagina HTML che comunica con la finestra parent e si chiude + """ + if error: + # SANITIZZA l'errore per HTML + error_escaped = html.escape(str(error)) + + html_content = f""" + + + + OAuth Error + + + +
+

Authentication Error

+

Error: {error_escaped}

+

This window will close automatically...

+
+ + + + """ + else: + # SANITIZZA tutti i dati per HTML + username_html = html.escape(str(username)) if username else '' + + html_content = f""" + + + + OAuth Success + + + +
+

Authentication Successful!

+

Welcome, {username_html}!

+

This window will close automatically...

+
+ + + + """ + + return HttpResponse(html_content, content_type='text/html') + +@method_decorator(ratelimit(key='user', rate='50/m', method='GET'), name='get') +class Setup2FAView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + logger.info("Setting up 2FA for user") + user = request.user + + user.two_factor_secret = generate_random_base32() + user.save() + + otp_uri = create_manual_totp_uri(user.two_factor_secret, user.email) + + # Return the OTP URI as a URL for the frontend to generate the QR code + return Response({"otp_uri": otp_uri}, status=status.HTTP_200_OK) + + def post(self, request): + user = request.user + otp = request.data.get('otp_code') + if not otp: + return Response({"error": "OTP code is required"}, status=status.HTTP_400_BAD_REQUEST) + + if verify_otp(user, otp): + user.has_two_factor_auth = True + user.save() + # Update the has_two_factor_auth flag in user microservice + user_service_url = f"{Microservices['Users']}/user/user/update-2fa/" + + response = requests.post( + user_service_url, + json={ + 'user_id': user.user_id, + 'enabled': True + }, + headers={ + 'Authorization': f'Bearer {get_jwt_token_for_user(user)}', + 'Content-Type': 'application/json', + 'X-API-KEY': settings.API_KEY, + } + ) + + if response.status_code == 200: + return redirect('/#home') + else: + return Response({"error": "Failed to update 2FA status"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + return Response({"error": "Invalid OTP code"}, status=status.HTTP_400_BAD_REQUEST) + +def verify_otp(user, otp): + try: + if not user.two_factor_secret: + return False + + # 1. Ottieni il timestamp corrente + timestamp = int(time.time()) + + # 2. Calcola time counter (intervalli di 30 secondi dal 1970) + time_counter = timestamp // 30 + + # 3. Verifica il codice corrente e un intervallo prima/dopo (per compensare desincronizzazioni di orario) + for i in range(-1, 2): + current_counter = time_counter + i + + # 4. Converti il counter in bytes (formato big-endian 8-byte) + time_bytes = struct.pack(">Q", current_counter) + + # 5. Decodifica il secret dalla base32 + key = base64.b32decode(user.two_factor_secret.upper() + '=' * ((8 - len(user.two_factor_secret) % 8) % 8)) + + # 6. Calcola l'HMAC-SHA1 + h = hmac.new(key, time_bytes, hashlib.sha1).digest() + + # 7. Applica "dynamic truncation" per ottenere 4 bytes + offset = h[-1] & 0x0F + truncated_hash = h[offset:offset+4] + + # 8. Converti in un numero intero, elimina il bit piรน significativo + code = struct.unpack('>I', truncated_hash)[0] & 0x7FFFFFFF + + # 9. Prendi solo le ultime 6 cifre + code = code % 1000000 + + # 10. Formatta come stringa con zeri iniziali + code_str = '{:06d}'.format(code) + + # 11. Confronta con il codice inserito dall'utente + if code_str == otp: + return True + + return False + except Exception as e: + logger.error(f"Error verifying OTP: {str(e)}") + return False + +def create_manual_totp_uri(secret, email, issuer="Transcendence"): + # Codifica il label (issuer:account) per URL + label = f"{issuer}:{email}" + encoded_label = urllib.parse.quote(label) + + # Crea l'URI con i parametri necessari + uri = f"otpauth://totp/{encoded_label}?secret={secret}&issuer={urllib.parse.quote(issuer)}" + + # Parametri opzionali (questi sono i valori predefiniti) + uri += "&algorithm=SHA1&digits=6&period=30" + + return uri + +def generate_random_base32(length=16): + """ + Genera una stringa casuale in formato base32. + + Args: + length: Lunghezza in byte dei dati casuali prima della codifica + + Returns: + Una stringa base32 (caratteri A-Z, 2-7) + """ + # Ottieni byte casuali sicuri usando os.urandom + random_bytes = os.urandom(length) + + # Codifica in base32 + base32_str = base64.b32encode(random_bytes).decode('utf-8') + + # Rimuovi eventuali padding '=' + base32_str = base32_str.rstrip('=') + + return base32_str + +class Verify2FALoginView(APIView): + permission_classes = [permissions.AllowAny] + authentication_classes = [] # Use empty list to bypass DRF's authentication + + def post(self, request): + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if not auth_header.startswith('Bearer '): + return Response({"error": "Invalid authorization header"}, status=status.HTTP_401_UNAUTHORIZED) + + token = auth_header.split('Bearer ')[1] + otp_code = request.data.get('otp_code') + + if not otp_code: + logger.error("OTP code not provided") + return Response({"error": "OTP code is required"}, status=status.HTTP_400_BAD_REQUEST) + + try: + # Decodifica il token per ottenere user_id + decoded_token = RefreshToken(token) + user_id = decoded_token.get('user_id') + + # Ottieni l'utente direttamente dal database + User = get_user_model() + user = User.objects.get(user_id=user_id) + + logger.info(f"Verifying 2FA for user {user.username} with OTP code: {otp_code}") + + # Verify OTP code + if not verify_otp(user, otp_code): + logger.warning(f"Invalid OTP code for user {user.username}") + return Response({"error": "Invalid OTP code"}, + status=status.HTTP_400_BAD_REQUEST) + + # OTP verified, log the user in + login(request, user) + + # Generate JWT tokens + refresh = RefreshToken.for_user(user) + access_token = str(refresh.access_token) + refresh_token = str(refresh) + + logger.info(f"User {user.username} logged in with 2FA") + return Response({ + 'access_token': access_token, + 'refresh_token': refresh_token, + 'user_id': user.user_id, + 'username': user.username, + 'email': user.email + }, status=status.HTTP_200_OK) + + except User.DoesNotExist: + return Response({"error": "User not found"}, + status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error during 2FA verification: {str(e)}") + return Response({"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class Disable2FAView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + user = request.user + if user.has_two_factor_auth: + # First update the local model + user.has_two_factor_auth = False + user.two_factor_secret = None + user.save() + + # Then update the user microservice + user_service_url = f"{Microservices['Users']}/user/user/update-2fa/" + + response = requests.post( + user_service_url, + json={ + 'user_id': user.user_id, + 'enabled': False + }, + headers={ + 'Authorization': f'Bearer {get_jwt_token_for_user(user)}', + 'Content-Type': 'application/json', + 'X-API-KEY': settings.API_KEY, + } + ) + + if response.status_code == 200: + return redirect('/#home') + else: + # If the user service update fails, we should revert our local change + user.has_two_factor_auth = True + user.save() + return Response({"error": "Failed to disable 2FA in user service"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + return Response({"error": "2FA is not enabled"}, status=status.HTTP_400_BAD_REQUEST) + +class Is2FAEnabledView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + user = request.user + return Response({"is_enabled": user.has_two_factor_auth}, status=status.HTTP_200_OK) + + +# Update the UserRegister class to handle potential oauth users @method_decorator(csrf_exempt, name='dispatch') class UserRegister(APIView): permission_classes = (permissions.AllowAny,) + def post(self, request): try: clean_data = custom_validation(request.data) - serializer = UserRegisterSerializer(data=clean_data) - if serializer.is_valid(raise_exception=True): - user = serializer.create(clean_data) - if user: - serializer.data['password'] = user.password - CreateOnOtherServices(user) - return Response({'user': serializer.data, 'user_id': user.user_id}, status=status.HTTP_201_CREATED) + email = clean_data.get('email') + username = clean_data.get('username') + + User = get_user_model() + + # Check if email already exists + if User.objects.filter(email=email).exists(): + return Response( + {'error': 'Email already registered. Please login instead.'}, + status=status.HTTP_409_CONFLICT + ) + + # Check if username already exists + if User.objects.filter(username=username).exists(): + return Response( + {'error': 'Username already taken. Please choose another.'}, + status=status.HTTP_409_CONFLICT + ) + + with transaction.atomic(): + # Create the user + serializer = UserRegisterSerializer(data=clean_data) + if serializer.is_valid(raise_exception=True): + user = serializer.create(clean_data) + + # Try to create user in other services + try: + CreateOnOtherServices(user) + except Exception as e: + # If creating on other services fails, the transaction will roll back + raise ValueError(f"User creation failed: {str(e)}") + + return Response({ + 'user': { + 'email': user.email, + 'username': user.username, + 'user_id': user.user_id + } + }, status=status.HTTP_201_CREATED) + except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) + return Response( + {'error': str(e)}, + status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST) + ) + return Response(status=status.HTTP_400_BAD_REQUEST) - - +# Update the UserLogin class to work with both email/password and OAuth users @method_decorator(csrf_exempt, name='dispatch') class UserLogin(APIView): permission_classes = (permissions.AllowAny,) authentication_classes = (OAuth2Authentication,) + def post(self, request): try: - data = request.data - # Validate email and password if not validate_email(data): return Response({'error': 'Invalid email'}, status=status.HTTP_400_BAD_REQUEST) @@ -124,22 +783,37 @@ def post(self, request): serializer = UserLoginSerializer(data=data) serializer.is_valid(raise_exception=True) user = serializer.check_user(data) + + # Check if 2FA is enabled for this user + if user.has_two_factor_auth: + # Generate a temporary token valid only for 2FA verification + temp_token = RefreshToken.for_user(user) + temp_token['2fa_pending'] = True # Add a custom claim to indicate this token is for 2FA verification only + temp_token.set_exp(lifetime=timedelta(minutes=5)) # Set a short expiration time for the temp token + response = { + 'temp_token': str(temp_token), + 'message': '2FA is enabled. Please verify your OTP code.', + } + return Response(response, status=status.HTTP_200_OK) + + # If 2FA is not enabled, proceed with normal login login(request, user) - # Prepare data for tocken request - + # Generate tokens for the user refresh = RefreshToken.for_user(user) access_token = str(refresh.access_token) refresh_token = str(refresh) return Response({ 'access_token': access_token, - 'refresh_token': refresh_token}, - status=status.HTTP_200_OK) - + 'refresh_token': refresh_token, + 'user_id': user.user_id, + 'username': user.username, + 'email': user.email + }, status=status.HTTP_200_OK) except Exception as e: - return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) + return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) + # headers = { # 'Content-Type': 'application/x-www-form-urlencoded', # 'Authorization': f'Basic {client["CLIENT_ID"]}:{client["CLIENT_SECRET"]}', @@ -202,7 +876,6 @@ def post(self, request): return Response({'error': 'Invalid service password'}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: return Response({'error': str(e)}, status=error_codes.get(str(e), status.HTTP_400_BAD_REQUEST)) - return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/Back-End/login/requirements.txt b/Back-End/login/requirements.txt index cdfb1a6f..2b4953ec 100644 --- a/Back-End/login/requirements.txt +++ b/Back-End/login/requirements.txt @@ -1,24 +1,31 @@ asgiref==3.8.1 -certifi==2024.8.30 +certifi==2025.1.31 cffi==1.17.1 -charset-normalizer==3.4.0 -cryptography==44.0.0 -Django==5.1.3 -django-cors-headers==4.6.0 +charset-normalizer==3.4.1 +cryptography==44.0.2 +django==5.2.4 +django-cors-headers==4.7.0 django-oauth-toolkit==3.0.1 -djangorestframework==3.15.2 +django-ratelimit==4.1.0 +django-redis==5.4.0 +djangorestframework>=3.16.0 idna==3.10 jwcrypto==1.5.6 oauthlib==3.2.2 -psycopg==3.2.5 -psycopg-binary==3.2.5 +psycopg==3.2.9 +psycopg-binary==3.2.9 pycparser==2.22 -requests==2.32.3 -sqlparse==0.5.2 +redis==5.0.1 +requests>=2.32.4 +sqlparse==0.5.3 typing_extensions==4.12.2 -urllib3==2.2.3 -djangorestframework-simplejwt==5.4.0 +urllib3>=2.5.0 +djangorestframework-simplejwt==5.5.0 # via -r Back-End/requirements.in -drf-yasg==1.21.8 +drf-yasg==1.21.9 # via -r Back-End/requirements.in -gunicorn==21.2.0 \ No newline at end of file +gunicorn==23.0.0 +pyotp==2.9.0 +django-encrypted-model-fields==0.6.5 +django-prometheus==2.3.1 +prometheus-client==0.21.1 diff --git a/Back-End/pongProject/Dockerfile b/Back-End/pongProject/Dockerfile index e6a2350b..4841a6cb 100644 --- a/Back-End/pongProject/Dockerfile +++ b/Back-End/pongProject/Dockerfile @@ -1,5 +1,5 @@ # Use a stable Python version -FROM python:3.11-alpine AS base +FROM python:3.13-alpine AS base RUN apk add --no-cache postgresql-libs bash @@ -16,6 +16,9 @@ COPY --from=builder /usr/local /usr/local COPY . /app WORKDIR /app +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static + COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/Back-End/pongProject/debug_physics.py b/Back-End/pongProject/debug_physics.py new file mode 100644 index 00000000..e3427d90 --- /dev/null +++ b/Back-End/pongProject/debug_physics.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Debug Physics Comparison - Find where engines diverge +""" +import math +from physics_engines.legacy_physics import LegacyPhysicsEngine +from physics_engines.modern_physics import ModernPhysicsEngine + +def debug_physics_step(): + """Compare engines step by step""" + + # Initialize both engines with same state + legacy = LegacyPhysicsEngine() + modern = ModernPhysicsEngine() + + # Set identical initial states - position near paddle for collision + legacy.ball_pos = [-58.0, 5.0] # Near left paddle + legacy.angle = 180 # Moving left towards paddle + legacy.ball_speed = 2.0 + legacy.wall_hit_pos = 0 + + modern.ball_pos.x = -58.0 + modern.ball_pos.y = 5.0 + modern.angle = 180 + modern.ball_speed = 2.0 + modern.wall_hit_pos = 0 + + print("๐Ÿ” Initial State (near paddle collision):") + print(f"Legacy: pos=({legacy.ball_pos[0]:.3f}, {legacy.ball_pos[1]:.3f}), angle={legacy.angle}, speed={legacy.ball_speed}") + print(f"Modern: pos=({modern.ball_pos.x:.3f}, {modern.ball_pos.y:.3f}), angle={modern.angle}, speed={modern.ball_speed}") + + # Run 10 steps and compare + for step in range(10): + print(f"\n๐Ÿ“Š Step {step + 1}:") + + # Store positions before step + legacy_before = [legacy.ball_pos[0], legacy.ball_pos[1]] + modern_before = [modern.ball_pos.x, modern.ball_pos.y] + + # Execute physics step + legacy_result = legacy.physics_step() + modern_result = modern.physics_step() + + # Show results + print(f" Legacy: {legacy_before} -> ({legacy.ball_pos[0]:.3f}, {legacy.ball_pos[1]:.3f})") + print(f" Modern: {modern_before} -> ({modern.ball_pos.x:.3f}, {modern.ball_pos.y:.3f})") + + # Calculate difference + diff_x = abs(legacy.ball_pos[0] - modern.ball_pos.x) + diff_y = abs(legacy.ball_pos[1] - modern.ball_pos.y) + diff_total = math.sqrt(diff_x*diff_x + diff_y*diff_y) + + print(f" Diff: ({diff_x:.6f}, {diff_y:.6f}) = {diff_total:.6f} units") + print(f" Legacy angle: {legacy.angle:.3f}, Modern angle: {modern.angle:.3f}") + print(f" Legacy result: {legacy_result}, Modern result: {modern_result}") + + if diff_total > 0.001: # If significant difference + print(f" โš ๏ธ DIVERGENCE DETECTED!") + # Continue to see what happens next + + if legacy_result or modern_result: + print(f" ๐ŸŽฏ SCORING EVENT!") + break + +if __name__ == "__main__": + debug_physics_step() diff --git a/Back-End/pongProject/entrypoint.sh b/Back-End/pongProject/entrypoint.sh index 2b91084a..ae6448fc 100644 --- a/Back-End/pongProject/entrypoint.sh +++ b/Back-End/pongProject/entrypoint.sh @@ -4,17 +4,20 @@ set -e export DJANGO_SETTINGS_MODULE=pongProject.settings -# migrations at startup -python manage.py makemigrations -python manage.py makemigrations pong_app -python manage.py migrate +# mkdir -p /app/static +# ln -sf /app/staticfiles/* /app/static/ +# # collect static files +python manage.py collectstatic --noinput + -# REMOVE THIS LINE: -# python3 -m pip install daphne +# migrations at startup +python manage.py makemigrations --noinput +python manage.py makemigrations pong_app --noinput +python manage.py migrate --noinput # For development if [ "$DEBUG" = True ]; then python manage.py runserver 0.0.0.0:8000 else - /usr/local/bin/daphne -b 0.0.0.0 -p 8000 pongProject.asgi:application || { echo "Daphne failed to start"; exit 1; } + /usr/local/bin/daphne -v 3 --access-log - -b 0.0.0.0 -p 8000 pongProject.asgi:application || { echo "Daphne failed to start"; exit 1; } fi \ No newline at end of file diff --git a/Back-End/pongProject/manage.py b/Back-End/pongProject/manage.py old mode 100755 new mode 100644 diff --git a/Back-End/pongProject/physics_engines/README.md b/Back-End/pongProject/physics_engines/README.md new file mode 100644 index 00000000..aeda7171 --- /dev/null +++ b/Back-End/pongProject/physics_engines/README.md @@ -0,0 +1,25 @@ +# Physics Engine for Pong Game + +This directory contains different physics engine implementations that can be tested and compared. + +## Engines Available: + +1. **legacy_physics.py** - Current simple implementation +2. **modern_physics.py** - Improved collision detection and physics +3. **benchmark_physics.py** - Performance testing utilities + +## How to Test: + +Run the benchmark script to compare performance: +```bash +python benchmark_physics.py +``` + +## Features Comparison: + +| Feature | Legacy | Modern | +|---------|--------|---------| +| Collision Detection | Basic AABB | Swept Sphere | +| Performance | Medium | High | +| Accuracy | Good | Excellent | +| Predictability | Fair | Excellent | diff --git a/Back-End/pongProject/physics_engines/benchmark_physics.py b/Back-End/pongProject/physics_engines/benchmark_physics.py new file mode 100644 index 00000000..c56e3965 --- /dev/null +++ b/Back-End/pongProject/physics_engines/benchmark_physics.py @@ -0,0 +1,211 @@ +""" +Physics Engine Benchmark Tool +Compare performance and accuracy of different physics # Set identical initial states for fair comparison + legacy.ball_pos = [10, 5] + legacy.angle = 45 + legacy.ball_speed = 2.0 + + modern.ball_pos.x = 10 + modern.ball_pos.y = 5 + modern.angle = 45 # Add missing angle + modern.ball_speed = 2.0 # Add missing ball_speed + modern.ball_velocity.x = 2.0 * 0.707 # cos(45ยฐ) + modern.ball_velocity.y = 2.0 * 0.707 # sin(45ยฐ)""" +import time +import statistics +from typing import List, Dict +import sys +import os + +# Add parent directory to path to import engines +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from physics_engines.legacy_physics import LegacyPhysicsEngine +from physics_engines.modern_physics import ModernPhysicsEngine + +class PhysicsBenchmark: + def __init__(self): + self.results = {} + + def benchmark_engine(self, engine_class, engine_name: str, iterations: int = 1000): + """Benchmark a physics engine""" + print(f"\n๐Ÿ”ฌ Benchmarking {engine_name}...") + + engine = engine_class() + times = [] + collision_counts = [] + + # Warm up + for _ in range(100): + engine.physics_step() + + # Actual benchmark + start_total = time.perf_counter() + + for i in range(iterations): + start = time.perf_counter() + result = engine.physics_step() + end = time.perf_counter() + + times.append((end - start) * 1000) # Convert to milliseconds + + # Track collision checks if available + if hasattr(engine, 'collision_checks'): + collision_counts.append(engine.collision_checks) + + # Reset on score to continue testing + if result: + engine.reset_ball() + + end_total = time.perf_counter() + total_time = (end_total - start_total) * 1000 + + # Calculate statistics + avg_time = statistics.mean(times) + min_time = min(times) + max_time = max(times) + median_time = statistics.median(times) + std_dev = statistics.stdev(times) if len(times) > 1 else 0 + + avg_collisions = statistics.mean(collision_counts) if collision_counts else 0 + + self.results[engine_name] = { + 'total_time_ms': total_time, + 'avg_time_ms': avg_time, + 'min_time_ms': min_time, + 'max_time_ms': max_time, + 'median_time_ms': median_time, + 'std_dev_ms': std_dev, + 'fps_theoretical': 1000 / avg_time if avg_time > 0 else float('inf'), + 'avg_collision_checks': avg_collisions, + 'iterations': iterations + } + + print(f"โœ… {engine_name} completed:") + print(f" ๐Ÿ“Š Average: {avg_time:.4f}ms per step") + print(f" โšก Theoretical FPS: {1000/avg_time:.1f}" if avg_time > 0 else " โšก Theoretical FPS: โˆž") + print(f" ๐ŸŽฏ Collision checks: {avg_collisions:.1f} per step" if avg_collisions > 0 else " ๐ŸŽฏ Collision checks: N/A") + print(f" ๐Ÿ“ˆ Std Dev: {std_dev:.4f}ms") + + def accuracy_test(self, iterations: int = 100): + """Test accuracy by comparing deterministic scenarios""" + print(f"\n๐ŸŽฏ Accuracy Test ({iterations} iterations)...") + + legacy = LegacyPhysicsEngine() + modern = ModernPhysicsEngine() + + # Set identical initial conditions + legacy.ball_pos = [10, 5] + legacy.angle = 45 + legacy.ball_speed = 2.0 + + modern.ball_pos.x = 10 + modern.ball_pos.y = 5 + modern.ball_velocity.x = 2.0 * 0.707 # cos(45ยฐ) + modern.ball_velocity.y = 2.0 * 0.707 # sin(45ยฐ) + + legacy_positions = [] + modern_positions = [] + + for i in range(iterations): + # Store positions + legacy_positions.append((legacy.ball_pos[0], legacy.ball_pos[1])) + modern_positions.append((modern.ball_pos.x, modern.ball_pos.y)) + + # Step physics + legacy_result = legacy.physics_step() + modern_result = modern.physics_step() + + # Reset if scored + if legacy_result: + legacy.reset_ball(45) + if modern_result: + modern.reset_ball(45) + + # Calculate position differences + differences = [] + for i, (legacy_pos, modern_pos) in enumerate(zip(legacy_positions, modern_positions)): + diff = ((legacy_pos[0] - modern_pos[0])**2 + (legacy_pos[1] - modern_pos[1])**2)**0.5 + differences.append(diff) + + avg_diff = statistics.mean(differences) + max_diff = max(differences) + + print(f"๐Ÿ“ Average position difference: {avg_diff:.4f} units") + print(f"๐Ÿ“ Maximum position difference: {max_diff:.4f} units") + + return avg_diff, max_diff + + def print_comparison(self): + """Print detailed comparison of results""" + if len(self.results) < 2: + print("โŒ Need at least 2 engines to compare") + return + + print(f"\n๐Ÿ“‹ Detailed Comparison:") + print("=" * 80) + + # Find best and worst performers + engines = list(self.results.keys()) + best_avg = min(self.results.values(), key=lambda x: x['avg_time_ms']) + worst_avg = max(self.results.values(), key=lambda x: x['avg_time_ms']) + + for name, stats in self.results.items(): + print(f"\n๐Ÿ”ง {name}:") + print(f" โฑ๏ธ Avg Time: {stats['avg_time_ms']:.4f}ms") + print(f" ๐Ÿƒ FPS: {stats['fps_theoretical']:.1f}") + print(f" ๐Ÿ” Collision Checks: {stats['avg_collision_checks']:.1f}") + print(f" ๐Ÿ“Š Stability (StdDev): {stats['std_dev_ms']:.4f}ms") + + # Performance vs best + if stats == best_avg: + print(f" ๐Ÿ† FASTEST ENGINE") + else: + speedup = stats['avg_time_ms'] / best_avg['avg_time_ms'] + print(f" ๐Ÿ“‰ {speedup:.2f}x slower than fastest") + + print(f"\n๐ŸŽ–๏ธ Winner: {[name for name, stats in self.results.items() if stats == best_avg][0]}") + + # Performance summary + legacy_fps = self.results.get('Legacy', {}).get('fps_theoretical', 0) + modern_fps = self.results.get('Modern', {}).get('fps_theoretical', 0) + + if legacy_fps > 0 and modern_fps > 0: + improvement = ((modern_fps - legacy_fps) / legacy_fps) * 100 + print(f"๐Ÿš€ Performance improvement: {improvement:+.1f}%") + +def main(): + print("๐ŸŽฎ Pong Physics Engine Benchmark") + print("=" * 50) + + benchmark = PhysicsBenchmark() + + # Benchmark both engines + benchmark.benchmark_engine(LegacyPhysicsEngine, "Legacy", 2000) + benchmark.benchmark_engine(ModernPhysicsEngine, "Modern", 2000) + + # Accuracy test + benchmark.accuracy_test(500) + + # Print comparison + benchmark.print_comparison() + + print(f"\n๐Ÿ’ก Recommendations:") + + if 'Modern' in benchmark.results and 'Legacy' in benchmark.results: + modern_stats = benchmark.results['Modern'] + legacy_stats = benchmark.results['Legacy'] + + if modern_stats['avg_time_ms'] < legacy_stats['avg_time_ms']: + improvement = ((legacy_stats['avg_time_ms'] - modern_stats['avg_time_ms']) / legacy_stats['avg_time_ms']) * 100 + print(f"โœ… Use Modern Physics Engine ({improvement:.1f}% faster)") + else: + print(f"โš ๏ธ Legacy engine is still faster, investigate Modern engine") + + print(f"\n๐ŸŽฏ For your multiplayer game:") + print(f" - Target: 30 FPS backend = 33.33ms budget per frame") + print(f" - Modern engine theoretical max: {benchmark.results.get('Modern', {}).get('fps_theoretical', 0):.0f} FPS") + print(f" - Legacy engine theoretical max: {benchmark.results.get('Legacy', {}).get('fps_theoretical', 0):.0f} FPS") + +if __name__ == "__main__": + main() diff --git a/Back-End/pongProject/physics_engines/legacy_physics.py b/Back-End/pongProject/physics_engines/legacy_physics.py new file mode 100644 index 00000000..1230b579 --- /dev/null +++ b/Back-End/pongProject/physics_engines/legacy_physics.py @@ -0,0 +1,97 @@ +""" +Legacy Physics Engine - Current Implementation +This is the current physics engine extracted for comparison. +""" +import math +import random + +class LegacyPhysicsEngine: + def __init__(self, ring_length=160, ring_height=90, ring_thickness=3): + self.ring_length = ring_length + self.ring_height = ring_height + self.ring_thickness = ring_thickness + + # Game state + self.ball_pos = [0, 0] + self.ball_radius = 2.5 + self.ball_speed = 1.0 + self.angle = 0 + self.wall_hit_pos = 0 + + # Player state + self.player_1_pos = [-60, 0] + self.player_2_pos = [60, 0] + self.p_length = 20 + self.p_width = 2 + self.p_speed = 1.0 + + def p1_is_hit(self): + """Check if ball hits player 1 paddle""" + return ( + self.ball_pos[0] - self.ball_radius - self.ball_speed <= self.player_1_pos[0] + self.p_width / 2 and + self.ball_pos[0] - self.ball_speed > self.player_1_pos[0] - self.p_width / 2 and + self.ball_pos[1] - self.ball_radius <= self.player_1_pos[1] + self.p_length / 2 and + self.ball_pos[1] + self.ball_radius >= self.player_1_pos[1] - self.p_length / 2 + ) + + def p2_is_hit(self): + """Check if ball hits player 2 paddle""" + return ( + self.ball_pos[0] + self.ball_radius + self.ball_speed >= self.player_2_pos[0] - self.p_width / 2 and + self.ball_pos[0] + self.ball_speed < self.player_2_pos[0] + self.p_width / 2 and + self.ball_pos[1] - self.ball_radius <= self.player_2_pos[1] + self.p_length / 2 and + self.ball_pos[1] + self.ball_radius >= self.player_2_pos[1] - self.p_length / 2 + ) + + def physics_step(self, ball_acc=0.1): + """Execute one physics step - legacy implementation""" + # Move ball + self.ball_pos[0] += self.ball_speed * math.cos(math.radians(self.angle)) + self.ball_pos[1] += self.ball_speed * -math.sin(math.radians(self.angle)) + + # Paddle collisions + if self.ball_pos[0] < 0 and self.p1_is_hit(): + hit_pos = self.ball_pos[1] - self.player_1_pos[1] + self.wall_hit_pos = 0 + if self.p_length > 0: + self.angle = hit_pos / self.p_length * -90 + else: + self.angle = -45 + if self.ball_speed < 5 * self.p_length: + self.ball_speed += ball_acc + + elif self.ball_pos[0] > 0 and self.p2_is_hit(): + hit_pos = self.ball_pos[1] - self.player_2_pos[1] + self.wall_hit_pos = 0 + if self.p_length > 0: + self.angle = 180 + hit_pos / self.p_length * 90 + else: + self.angle = 135 + if self.ball_speed < 5 * self.p_length: + self.ball_speed += ball_acc + + # Wall collisions + elif ((self.wall_hit_pos <= 0 and + self.ball_pos[1] + self.ball_radius + self.ring_thickness + self.ball_speed >= self.ring_height / 2) or + (self.wall_hit_pos >= 0 and + self.ball_pos[1] - self.ball_radius - self.ring_thickness - self.ball_speed <= -self.ring_height / 2)): + self.wall_hit_pos = self.ball_pos[1] + self.angle = -self.angle + + # Check scoring + return self.check_score() + + def check_score(self): + """Check if ball scored""" + if self.ball_pos[0] - self.ball_radius <= -self.ring_length / 2: + return "player_2_scores" + elif self.ball_pos[0] + self.ball_radius >= self.ring_length / 2 + self.ring_thickness: + return "player_1_scores" + return None + + def reset_ball(self, angle=None): + """Reset ball to center""" + self.ball_pos = [0, 0] + self.angle = angle if angle is not None else random.uniform(45, 135) + self.ball_speed = 90 / 150 + self.wall_hit_pos = 0 diff --git a/Back-End/pongProject/physics_engines/modern_physics.py b/Back-End/pongProject/physics_engines/modern_physics.py new file mode 100644 index 00000000..d591f054 --- /dev/null +++ b/Back-End/pongProject/physics_engines/modern_physics.py @@ -0,0 +1,298 @@ +""" +Modern Physics Engine - Improved Implementation +Features: +- Swept sphere collision detection +- Continuous collision detection +- Better bounce angles +- Predictable physics +- Performance optimized +""" +import math +import random +from dataclasses import dataclass +from typing import Tuple, Optional + +@dataclass +class Vector2: + x: float + y: float + + def __add__(self, other): + return Vector2(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + return Vector2(self.x - other.x, self.y - other.y) + + def __mul__(self, scalar): + return Vector2(self.x * scalar, self.y * scalar) + + def dot(self, other): + return self.x * other.x + self.y * other.y + + def length(self): + return math.sqrt(self.x * self.x + self.y * self.y) + + def normalize(self): + length = self.length() + if length > 0: + return Vector2(self.x / length, self.y / length) + return Vector2(0, 0) + + def reflect(self, normal): + """Reflect vector around normal""" + return self - normal * (2 * self.dot(normal)) + +@dataclass +class AABB: + """Axis-Aligned Bounding Box""" + min_x: float + min_y: float + max_x: float + max_y: float + + def contains_point(self, point: Vector2) -> bool: + return (self.min_x <= point.x <= self.max_x and + self.min_y <= point.y <= self.max_y) + + def expand(self, radius: float): + """Expand AABB by radius""" + return AABB( + self.min_x - radius, self.min_y - radius, + self.max_x + radius, self.max_y + radius + ) + +class ModernPhysicsEngine: + def __init__(self, ring_length=160, ring_height=90, ring_thickness=3): + self.ring_length = ring_length + self.ring_height = ring_height + self.ring_thickness = ring_thickness + + # Ball state - use legacy-compatible format + self.ball_pos = Vector2(0, 0) + self.ball_velocity = Vector2(1, 0) + self.ball_radius = 2.5 + self.ball_speed = 1.0 + + # Legacy compatibility: also maintain angle for comparison + self.angle = 0 # Degrees, like legacy system + + # Player state - legacy compatible + self.player_1_pos = Vector2(-60, 0) + self.player_2_pos = Vector2(60, 0) + self.p_length = 20 + self.p_width = 2 + self.p_speed = 1.0 + + # Performance tracking + self.collision_checks = 0 + + # Legacy compatibility state + self.wall_hit_pos = 0 + + def get_paddle_aabb(self, player_pos: Vector2) -> AABB: + """Get paddle bounding box""" + half_width = self.p_width / 2 + half_length = self.p_length / 2 + return AABB( + player_pos.x - half_width, player_pos.y - half_length, + player_pos.x + half_width, player_pos.y + half_length + ) + + def swept_sphere_collision(self, start: Vector2, end: Vector2, radius: float, aabb: AABB) -> Optional[Tuple[float, Vector2]]: + """ + Swept sphere vs AABB collision detection + Returns: (time_of_impact, collision_normal) or None + """ + self.collision_checks += 1 + + # Expand AABB by sphere radius + expanded = aabb.expand(radius) + + # Ray from sphere center + direction = end - start + if direction.length() == 0: + return None + + # Check if ray intersects expanded AABB + t_min = 0.0 + t_max = 1.0 + + for i, (start_val, end_val, box_min, box_max) in enumerate([ + (start.x, end.x, expanded.min_x, expanded.max_x), + (start.y, end.y, expanded.min_y, expanded.max_y) + ]): + if abs(end_val - start_val) < 1e-8: # Ray is parallel to the slab + if start_val < box_min or start_val > box_max: + return None + else: + # Calculate intersection with slab + inv_dir = 1.0 / (end_val - start_val) + t1 = (box_min - start_val) * inv_dir + t2 = (box_max - start_val) * inv_dir + + if t1 > t2: + t1, t2 = t2, t1 + + t_min = max(t_min, t1) + t_max = min(t_max, t2) + + if t_min > t_max: + return None + + if t_min >= 0 and t_min <= 1: + # Calculate collision point and normal + collision_point = start + direction * t_min + + # Find which face was hit + if abs(collision_point.x - aabb.min_x) < 1e-6: + normal = Vector2(-1, 0) + elif abs(collision_point.x - aabb.max_x) < 1e-6: + normal = Vector2(1, 0) + elif abs(collision_point.y - aabb.min_y) < 1e-6: + normal = Vector2(0, -1) + else: + normal = Vector2(0, 1) + + return (t_min, normal) + + return None + + def physics_step(self, dt: float = 1.0/30.0, ball_acc: float = 0.1): + """Execute one physics step - legacy compatible version""" + self.collision_checks = 0 + + # Move ball using angle-based velocity (like legacy) + self.ball_pos.x += self.ball_speed * math.cos(math.radians(self.angle)) + self.ball_pos.y += self.ball_speed * -math.sin(math.radians(self.angle)) # Legacy uses -sin + + # Check paddle collisions (simplified like legacy) + if self.ball_pos.x < 0 and self.p1_is_hit(): + hit_pos = self.ball_pos.y - self.player_1_pos.y + self.wall_hit_pos = 0 + # Use legacy angle calculation + if self.p_length > 0: + self.angle = hit_pos / self.p_length * -90 + else: + self.angle = -45 + if self.ball_speed < 5 * self.p_length: + self.ball_speed += ball_acc + + elif self.ball_pos.x > 0 and self.p2_is_hit(): + hit_pos = self.ball_pos.y - self.player_2_pos.y + self.wall_hit_pos = 0 + # Use legacy angle calculation + if self.p_length > 0: + self.angle = 180 + hit_pos / self.p_length * 90 + else: + self.angle = 135 + if self.ball_speed < 5 * self.p_length: + self.ball_speed += ball_acc + + # Wall collisions (legacy style) + elif ((self.wall_hit_pos <= 0 and + self.ball_pos.y + self.ball_radius + self.ring_thickness + self.ball_speed >= self.ring_height / 2) or + (self.wall_hit_pos >= 0 and + self.ball_pos.y - self.ball_radius - self.ring_thickness - self.ball_speed <= -self.ring_height / 2)): + self.wall_hit_pos = self.ball_pos.y + self.angle = -self.angle + + # Update velocity based on angle (for consistency) + speed = math.sqrt(self.ball_speed * self.ball_speed) + self.ball_velocity.x = speed * math.cos(math.radians(self.angle)) + self.ball_velocity.y = speed * -math.sin(math.radians(self.angle)) + + # Check scoring + return self.check_score() + + def p1_is_hit(self): + """Legacy-compatible collision detection""" + return ( + self.ball_pos.x - self.ball_radius - self.ball_speed <= self.player_1_pos.x + self.p_width / 2 and + self.ball_pos.x - self.ball_speed > self.player_1_pos.x - self.p_width / 2 and + self.ball_pos.y - self.ball_radius <= self.player_1_pos.y + self.p_length / 2 and + self.ball_pos.y + self.ball_radius >= self.player_1_pos.y - self.p_length / 2 + ) + + def p2_is_hit(self): + """Legacy-compatible collision detection""" + return ( + self.ball_pos.x + self.ball_radius + self.ball_speed >= self.player_2_pos.x - self.p_width / 2 and + self.ball_pos.x + self.ball_speed < self.player_2_pos.x + self.p_width / 2 and + self.ball_pos.y - self.ball_radius <= self.player_2_pos.y + self.p_length / 2 and + self.ball_pos.y + self.ball_radius >= self.player_2_pos.y - self.p_length / 2 + ) + + def check_paddle_collisions(self, start: Vector2, end: Vector2) -> Optional[Tuple[float, Vector2, Vector2]]: + """Check collision with paddles""" + # Only check paddle in direction of movement + if end.x - start.x < 0: # Moving left + paddle_aabb = self.get_paddle_aabb(self.player_1_pos) + collision = self.swept_sphere_collision(start, end, self.ball_radius, paddle_aabb) + if collision: + return collision[0], collision[1], self.player_1_pos + elif end.x - start.x > 0: # Moving right + paddle_aabb = self.get_paddle_aabb(self.player_2_pos) + collision = self.swept_sphere_collision(start, end, self.ball_radius, paddle_aabb) + if collision: + return collision[0], collision[1], self.player_2_pos + + return None + + def check_wall_collisions(self, start: Vector2, end: Vector2) -> Optional[Tuple[float, Vector2]]: + """Check collision with top/bottom walls""" + wall_aabb = AABB( + -self.ring_length, -self.ring_height/2, + self.ring_length, self.ring_height/2 + ) + + collision = self.swept_sphere_collision(start, end, self.ball_radius, wall_aabb) + if collision and (collision[1].y != 0): # Only wall hits (not goal lines) + return collision + + return None + + def check_score(self) -> Optional[str]: + """Check if ball scored - legacy compatible""" + # Using exact legacy scoring boundaries + if self.ball_pos.x - self.ball_radius <= -self.ring_length / 2: + return "player_2_scores" + elif self.ball_pos.x + self.ball_radius >= self.ring_length / 2 + self.ring_thickness: + return "player_1_scores" + return None + + def check_ball_oob(self): + """Legacy compatibility method for checking out of bounds""" + return self.check_score() is not None + + def reset_ball(self, angle=None): + """Reset ball to center - legacy compatible""" + import random + self.ball_pos = Vector2(0, 0) + self.angle = angle if angle is not None else random.uniform(45, 135) + self.ball_speed = 90 / 150 + self.wall_hit_pos = 0 + + # Update velocity based on angle + speed = math.sqrt(self.ball_speed * self.ball_speed) + self.ball_velocity.x = speed * math.cos(math.radians(self.angle)) + self.ball_velocity.y = speed * -math.sin(math.radians(self.angle)) + + def reset_ball(self, angle: Optional[float] = None): + """Reset ball to center""" + self.ball_pos = Vector2(0, 0) + if angle is None: + angle = random.uniform(30, 150) + + self.ball_velocity = Vector2( + math.cos(math.radians(angle)), + math.sin(math.radians(angle)) + ) * self.ball_speed + + def get_stats(self) -> dict: + """Get performance statistics""" + return { + "collision_checks": self.collision_checks, + "ball_position": (self.ball_pos.x, self.ball_pos.y), + "ball_velocity": (self.ball_velocity.x, self.ball_velocity.y), + "ball_speed": self.ball_velocity.length() + } diff --git a/Back-End/pongProject/pongProject/asgi.py b/Back-End/pongProject/pongProject/asgi.py index 58b0ab46..72204706 100644 --- a/Back-End/pongProject/pongProject/asgi.py +++ b/Back-End/pongProject/pongProject/asgi.py @@ -7,7 +7,7 @@ https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ """ -import os , django +import os, django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pongProject.settings') from django.core.asgi import get_asgi_application #DO NOT REMOVE THIS LINE @@ -16,11 +16,13 @@ from pong_app.middleware import JWTAuthMiddlewareStack from pongProject.routing import websocket_urlpatterns from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator - -application = ProtocolTypeRouter ({ +application = ProtocolTypeRouter({ "http": get_asgi_application(), - "websocket": JWTAuthMiddlewareStack( - URLRouter(websocket_urlpatterns) - ), + "websocket": AllowedHostsOriginValidator( + JWTAuthMiddlewareStack( + URLRouter(websocket_urlpatterns) + ) + ) }) diff --git a/Back-End/pongProject/pongProject/routing.py b/Back-End/pongProject/pongProject/routing.py index d8afad1b..22c829cc 100644 --- a/Back-End/pongProject/pongProject/routing.py +++ b/Back-End/pongProject/pongProject/routing.py @@ -4,11 +4,15 @@ Routes: websocket_urlpatterns (list): A list of URL patterns for WebSocket connections. - - re_path(r'ws/pong/(?P\d+)/$', consumers.GameTableConsumer.as_asgi()): + - re_path(r'pong/ws/pong/(?P\\d+)/$', consumers.GameTableConsumer.as_asgi()): Routes WebSocket connections to the GameTableConsumer based on the room_id parameter. + - re_path(r'pong/ws/tournament/(?P\\d+)/$', consumers.TournamentConsumer.as_asgi()): + Routes WebSocket connections to the TournamentConsumer based on the tournament_id parameter. + example: /pong/ws/pong/123/ or /tournament/456/ """ from pong_app import consumers websocket_urlpatterns = [ - re_path(r'ws/pong/(?P\d+)/$', consumers.GameTableConsumer.as_asgi()), + re_path(r'pong/ws/pong/(?P\d+)/$', consumers.GameTableConsumer.as_asgi()), + re_path(r'pong/ws/tournament/(?P\d+)/$', consumers.TournamentConsumer.as_asgi()), ] \ No newline at end of file diff --git a/Back-End/pongProject/pongProject/settings.py b/Back-End/pongProject/pongProject/settings.py index 4e297185..bcda25bb 100644 --- a/Back-End/pongProject/pongProject/settings.py +++ b/Back-End/pongProject/pongProject/settings.py @@ -10,15 +10,13 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ +from datetime import datetime, timedelta import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Aggiungi queste impostazioni -FORCE_SCRIPT_NAME = '/api/pong' - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -30,75 +28,112 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', True) -def ensure_scheme(url): - if url and not url.startswith(('http://', 'https://')): - return f"http://{url}" - return url - +# Simple microservices definition Microservices = { - 'Login': ensure_scheme(os.getenv('LOGIN_SERVICE', 'http://localhost:8000')), - 'Chat': ensure_scheme(os.getenv('CHAT_SERVICE', 'http://localhost:8001')), - 'Users': ensure_scheme(os.getenv('USERS_SERVICE', 'http://localhost:8002')), - 'Notifications': ensure_scheme(os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003')), - 'Pong': ensure_scheme(os.getenv('PONG_SERVICE', 'http://localhost:8004')), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), } -ALLOWED_HOSTS = [ - 'localhost', - 'localhost:3000', - '127.0.0.1', - '[::1]', - 'trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] - -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] -# CORS_ALLOW_ALL_ORIGINS = True +K8S_ALLOWED_HOSTS = os.environ.get('K8S_ALLOWED_HOSTS', '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16').split(',') + +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN + +ALLOWED_HOSTS = ['*'] +# 'localhost', +# 'localhost:3000', +# '127.0.0.1', +# '[::1]', +# 'trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS + +# CORS_ALLOWED_ORIGINS = [ +# 'http://localhost:3000', +# 'http://localhost', +# 'http://127.0.0.1', +# 'http://[::1]', +# 'https://trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS_WITH_SCHEME +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + 'http://localhost:3000', + 'http://localhost', + 'http://127.0.0.1', + 'http://[::1]', + 'https://trascendence.42firenze.it', + # Aggiungi IP privati per HTTPS + 'https://10.0.2.15', # L'IP specifico della tua VM + # O meglio ancora, patterns per tutti gli IP privati: + 'http://10.*', + 'https://10.*', + 'http://192.168.*', + 'https://192.168.*', + 'http://172.16.*', + 'https://172.16.*', + 'http://172.17.*', + 'https://172.17.*', + 'http://172.18.*', + 'https://172.18.*', + # Continua per tutti i range 172.16.x.x fino a 172.31.x.x + 'http://localhost:8443', + 'http://127.0.0.1:8443', + 'http://10.0.2.15:8443', + 'http://10.0.2.15.xip.io:8443', + 'http://10.11.*.xip.io:8443', + Microservices['Login'], + Microservices['Chat'], + Microservices['Users'], + Microservices['Notifications'], + Microservices['Pong'], +] + K8S_SERVICE_HOSTS_WITH_SCHEME CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', ] # Application definition INSTALLED_APPS = [ + 'django_prometheus', # Add prometheus monitoring 'daphne', 'django.contrib.admin', 'django.contrib.auth', @@ -106,15 +141,20 @@ def ensure_scheme(url): 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'rest_framework', - 'django_filters', - 'corsheaders', - 'pong_app', - 'channels', - 'django_redis', + 'rest_framework', + 'django_filters', + 'corsheaders', + 'pong_app', + 'channels', + 'drf_yasg', + 'django_redis', + # 'rest_framework_simplejwt', # Removed to avoid cross-service app dependencies ] MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', # Add at the top + 'pong_app.middleware.ExceptionMiddleware', + 'pong_app.middleware.HealthCheckMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -122,7 +162,8 @@ def ensure_scheme(url): 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', # Add at the end ] ROOT_URLCONF = 'pongProject.urls' @@ -145,52 +186,58 @@ def ensure_scheme(url): ASGI_APPLICATION = 'pongProject.asgi.application' +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'pong_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5436'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'pong_db'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5439'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), - } -} import secrets -REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') -REDIS_PORT = os.getenv('REDIS_PORT', '6700') -REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '3') -REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '4') +REDIS_HOST = os.getenv('REDIS_HOST', 'my-umbrella-redis-chart-service.redis-namespace.svc.cluster.local') +REDIS_PORT = os.getenv('REDIS_PORT', '6379') +REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '1') # Pong: DB 1 +REDIS_CHANNEL_DB = os.getenv('REDIS_CHANNEL_DB', '8') # Pong: Channel DB 8 CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } - } + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'KEY_PREFIX': 'pong:', # Service prefix for data isolation + } + } } - - CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], - 'prefix': 'pong', - }, - } + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CHANNEL_DB}'], + 'prefix': 'pong', # Channel prefix + }, + } } # Password validation @@ -211,6 +258,21 @@ def ensure_scheme(url): }, ] +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, + 'ALGORITHM': 'HS256', + # 'ISSUER': 'login', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'user_id', + 'USER_ID_CLAIM': 'user_id', + # 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), # Removed to avoid simplejwt dependency + 'TOKEN_TYPE_CLAIM': 'token_type', +} # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -223,11 +285,16 @@ def ensure_scheme(url): USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Changed from '/static/' + +# Add this to define where Django should look for static files +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -236,12 +303,212 @@ def ensure_scheme(url): REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], - 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], - 'DEFAULT_AUTHENTICATION_CLASSES': ['pong_app.middleware.JWTAuth'], + 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], + 'DEFAULT_AUTHENTICATION_CLASSES': ['pong_app.middleware.JWTAuth'], } ADMIN = { - 'username': os.getenv('ADMIN_USERNAME', 'admin'), - 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), - 'password': os.getenv('ADMIN_PASSWORD', 'admin'), -} \ No newline at end of file + 'username': os.getenv('ADMIN_USERNAME', 'admin'), + 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), + 'password': os.getenv('ADMIN_PASSWORD', 'admin'), +} + +# ELK-Optimized Logging Configuration for pong service +# This configuration provides standardized logging for the ELK Stack + +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "pong"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'pong': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_pong': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/pongProject/pongProject/urls.py b/Back-End/pongProject/pongProject/urls.py index fc576df4..053a0462 100644 --- a/Back-End/pongProject/pongProject/urls.py +++ b/Back-End/pongProject/pongProject/urls.py @@ -15,9 +15,130 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path +from django.http import JsonResponse, HttpResponse +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Pong Game API", + default_version='v1', + description="API for Pong game management, multiplayer sessions, and game statistics", + contact=openapi.Contact(email="admin@trascendence.local"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +# Custom view that serves Swagger UI with JWT authentication +def custom_swagger_view(request): + html = """ + + + Pong Game API Documentation + + + + + + +
+ + + + +""" + return HttpResponse(html) urlpatterns = [ - path('admin/', admin.site.urls), + path('admin/', admin.site.urls), path('pong/', include('pong_app.urls')), + path('', include('django_prometheus.urls')), # Add metrics endpoint + + # Swagger/API documentation with JWT support + path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', custom_swagger_view, name='schema-swagger-ui'), + re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/schema/', schema_view.without_ui(cache_timeout=0), name='schema-json-api'), + path('health/', lambda request: JsonResponse({'status': 'healthy'}), name='health'), ] diff --git a/Back-End/pongProject/pong_app/consumers.py b/Back-End/pongProject/pong_app/consumers.py index df579c49..b1043771 100644 --- a/Back-End/pongProject/pong_app/consumers.py +++ b/Back-End/pongProject/pong_app/consumers.py @@ -1,37 +1,56 @@ import asyncio import json from channels.generic.websocket import AsyncWebsocketConsumer -from asgiref.sync import sync_to_async -from .signals import GameState ,TournamentState +from channels.db import database_sync_to_async +from .signals import GameState +from .tournament_manager import tournament_manager from .models import Game +import logging +from asgiref.sync import sync_to_async + +# Get dedicated loggers +logger = logging.getLogger('pong_app') +websocket_logger = logging.getLogger('websockets') active_games = {} -active_tournaments = {} player_ready = {} class GameTableConsumer(AsyncWebsocketConsumer): async def connect(self): - print('connected') + websocket_logger.info('New WebSocket connection attempt') self.room_id = self.scope['url_route']['kwargs']['room_id'] - self.channel_name = f'game_{self.room_id}' - self.tournament_id = self.scope['url_route']['kwargs']['tournament_id'] + self.room_name = f'game_{self.room_id}' + self.tournament_id = self.scope['url_route']['kwargs'].get('tournament_id', None) + + # Check authentication - reject AnonymousUser + user = self.scope.get('user') + if not user or not hasattr(user, 'user_id'): + websocket_logger.warning(f"Unauthenticated WebSocket connection attempt for game {self.room_id}") + await self.close(code=4001) # Custom close code for authentication failure + return + + # Store authenticated user info + self.player_id = user.user_id + websocket_logger.info(f"Authenticated user {self.player_id} connected to game {self.room_id}") + # Join room group await self.channel_layer.group_add( + self.room_name, self.channel_name ) - # Store player identification - user = self.scope.get('user', None) - if user and not user.is_anonymous: - self.player_id = user.id - else: - # Get from query parameters or other source - query_string = self.scope.get('query_string', b'').decode() - query_params = dict(qp.split('=') for qp in query_string.split('&') if '=' in qp) - self.player_id = query_params.get('player_id') + await self.accept() + logger.info(f"WebSocket connection accepted for game {self.room_id}") + + # Send welcome message to confirm successful connection + await self.send(text_data=json.dumps({ + 'type': 'connection_success', + 'message': f'Welcome to game {self.room_id}!', + 'player_id': self.player_id + })) async def disconnect(self, close_code): - print(f"Disconnected: {self.room_id} with code {close_code}") + websocket_logger.info(f"WebSocket disconnected: game={self.room_id}, code={close_code}") # Handle unexpected disconnections if self.room_id in active_games: @@ -42,8 +61,9 @@ async def disconnect(self, close_code): player = getattr(self, 'player_id', None) # Send notification to other players + logger.info(f"Player {player} disconnected unexpectedly from game {self.room_id}") await self.channel_layer.group_send( - self.channel_name, + self.room_name, # Use room_name instead of channel_name { 'type': 'quit_game', 'player': player, @@ -53,42 +73,66 @@ async def disconnect(self, close_code): ) # Clean up game resources - game_state.quit_game() - del active_games[self.room_id] + game_state.quit_game(player) + if self.room_id in active_games: + # Remove game from active games + del active_games[self.room_id] + logger.info(f"Game {self.room_id} resources cleaned up after disconnect") + + # Leave room group + await self.channel_layer.group_discard( + self.room_name, + self.channel_name + ) async def receive(self, text_data): - data = json.loads(text_data) - message_type = data.get('type') - # print(f"message_type: {message_type}") - handler = self.message_handlers.get(message_type, self.default_handler) - await handler(self, data) + try: + data = json.loads(text_data) + message_type = data.get('type') + websocket_logger.debug(f"Received WebSocket message: type={message_type}, game={self.room_id}") + handler = self.message_handlers.get(message_type, self.default_handler) + await handler(self, data) + except json.JSONDecodeError: + websocket_logger.error(f"Invalid JSON received: {text_data[:100]}") + except Exception as e: + websocket_logger.exception(f"Error processing WebSocket message: {str(e)}") async def default_handler(self, data): - print(f"Unhandled message type: {data['type']}") + websocket_logger.warning(f"Unhandled message type: {data.get('type')}") async def chat_message(self, data): message = data['message'] + logger.debug(f"Chat message in game {self.room_id}: {message[:30]}...") await self.send(text_data=json.dumps({ 'message': message })) async def player_ready(self, data): - player = data['player'] + # Use authenticated user from connection instead of client data + player = self.player_id if self.room_id not in player_ready: - player_ready[self.room_id] = [False, False] - player_ready[self.room_id][player] = True - print(player_ready[self.room_id]) - if all(player_ready[self.room_id]): - await self.send(text_data=json.dumps({ - 'message': 'All players are ready!', - 'ready': True - })) - # data['player1'] = await sync_to_async((Game.objects.get)(id=self.room_id).player_1) - # data['player2'] = await sync_to_async((Game.objects.get)(id=self.room_id).player_2) - game = await sync_to_async(Game.objects.get)(id=self.room_id) - data['player1'] = await sync_to_async(lambda: game.player_1)() - data['player2'] = await sync_to_async(lambda: game.player_2)() - await self.start_game(data) + player_ready[self.room_id] = set() + + player_ready[self.room_id].add(player) + logger.info(f"Player {player} ready in game {self.room_id}. Ready players: {len(player_ready[self.room_id])}") + + if len(player_ready[self.room_id]) == 2: + logger.info(f"All players ready in game {self.room_id}, starting game") + await self.channel_layer.group_send( + self.room_name, + { + 'type': 'all_players_ready', + 'message': 'All players are ready!' + } + ) + + try: + game = await sync_to_async(Game.objects.get)(id=self.room_id) + data['player1'] = await sync_to_async(lambda: game.player_1)() + data['player2'] = await sync_to_async(lambda: game.player_2)() + await self.start_game(data) + except Exception as e: + logger.error(f"Error retrieving game data: {str(e)}", exc_info=True) else: await self.send(text_data=json.dumps({ 'message': 'Waiting for players to be ready...', @@ -97,64 +141,141 @@ async def player_ready(self, data): async def start_game(self, data): if self.room_id in active_games: + logger.info(f"Game {self.room_id} already started, ignoring start request") return - del player_ready[self.room_id] - active_games[self.room_id] = GameState(data['player1'], data['player2'], self.room_id, data.get('player_length', 10), self.tournament_id if self.tournament_id else None) - asyncio.create_task(active_games[self.room_id].start()) + + logger.info(f"Starting game {self.room_id} with players {data}") + try: + del player_ready[self.room_id] + active_games[self.room_id] = GameState( + data['player1'], + data['player2'], + self.room_id, + data.get('player_length', 10), + self.tournament_id if self.tournament_id else None + ) + asyncio.create_task(active_games[self.room_id].start()) + logger.info(f"Game {self.room_id} successfully started") + except Exception as e: + logger.error(f"Error starting game {self.room_id}: {str(e)}", exc_info=True) async def up(self, data): - player = data['player'] - active_games[self.room_id].up(player) + try: + # Use authenticated user from connection instead of client data + player = self.player_id + active_games[self.room_id].up(player) + except KeyError: + logger.error(f"Game {self.room_id} not found for UP movement") + except Exception as e: + logger.error(f"Error in UP movement: {str(e)}") async def down(self, data): - player = data['player'] - active_games[self.room_id].down(player) + try: + # Use authenticated user from connection instead of client data + player = self.player_id + active_games[self.room_id].down(player) + except KeyError: + logger.error(f"Game {self.room_id} not found for DOWN movement") + except Exception as e: + logger.error(f"Error in DOWN movement: {str(e)}") async def stop(self, data): - player = data['player'] - active_games[self.room_id].stop(player) + try: + # Use authenticated user from connection instead of client data + player = self.player_id + active_games[self.room_id].stop(player) + logger.debug(f"Player {player} stopped in game {self.room_id}") + except KeyError: + logger.error(f"Game {self.room_id} not found for STOP movement") + except Exception as e: + logger.error(f"Error in STOP movement: {str(e)}") async def game_init(self, data): - print(f"game_init: {data}") - game_state = active_games[self.room_id] - game_state.ring_length = data.get('ring_length', 0) - game_state.ring_height = data.get('ring_height', 0) - game_state.ring_width = data.get('ring_width', 0) - game_state.ring_thickness = data.get('ring_thickness', 0) - game_state.p_length = data.get('p_length', 0) - game_state.p_width = data.get('p_width', 0) - game_state.p_height = data.get('p_height', 0) - game_state.ball_radius = data.get('ball_radius', 0) - game_state.player_1_pos = data.get('player_1_pos', [0, 0]) - game_state.player_2_pos = data.get('player_2_pos', [0, 0]) - game_state.ball_speed = data.get('ball_speed', 0) - game_state.p_speed = data.get('p_speed', 0) - await game_state.update() - + logger.info(f"Game initialization for {self.room_id}") + try: + game_state = active_games[self.room_id] + game_state.ring_length = data.get('ring_length', 160) + game_state.ring_height = data.get('ring_height', 90) + game_state.ring_width = data.get('ring_width', 200) + game_state.ring_thickness = data.get('ring_thickness', 3) + game_state.p_length = data.get('p_length', 20) + game_state.p_width = data.get('p_width', 2.5) + game_state.p_height = data.get('p_height', 2.5) + game_state.ball_radius = data.get('ball_radius', 2.5) + game_state.player_1_pos = data.get('player_1_pos', [-75, 0]) + game_state.player_2_pos = data.get('player_2_pos', [75, 0]) + game_state.ball_pos = data.get('ball_pos', [0, 0]) + game_state.ball_speed = data.get('ball_speed', 1.2) + game_state.p_speed = data.get('p_speed', 1.5) + + # โœ… CRITICAL: Don't call update() immediately to avoid spamming + # await game_state.update() + + # โœ… Use to_dict() method instead of non-existent game_state attribute + state_dict = game_state.to_dict() + logger.info(f'Game {self.room_id} initialized with ball_speed: {game_state.ball_speed}') + logger.debug(f'Game state configuration: {state_dict}') + except KeyError: + logger.error(f"Game {self.room_id} not found during initialization") + except Exception as e: + logger.error(f"Error during game initialization: {str(e)}", exc_info=True) async def game_state(self, data): - # print(data) + # Avoid excessive logging for high-frequency state updates + logger.debug(f"Game state update for {self.room_id}") await self.send(text_data=json.dumps({ 'type': 'game_state', 'game_state': data['game_state'] })) async def game_over(self, data): + logger.info(f"Game over for {self.room_id}") + + # Send game over message with results await self.send(text_data=json.dumps({ + 'type': 'game_over', 'message': 'Game Over!', - 'game_over': True + 'game_over': True, + 'winner': data.get('winner'), + 'loser': data.get('loser'), + 'final_scores': data.get('final_scores', {}) })) - del active_games[self.room_id] + + try: + # Clean up game resources + if self.room_id in active_games: + del active_games[self.room_id] + logger.info(f"Game {self.room_id} resources cleaned up after game over") + except KeyError: + logger.warning(f"Game {self.room_id} already removed from active_games") async def quit_game(self, data): - player = data['player'] + # Use authenticated user from connection instead of client data + player = self.player_id + logger.info(f"Player {player} quit game {self.room_id}") await self.send(text_data=json.dumps({ 'type': 'quit_game', 'message': f'Player {player} has quit the game!', 'game_over': True })) - GameState.quit_game(player) - del active_games[self.room_id] + try: + if self.room_id in active_games: + active_games[self.room_id].quit_game(player) + del active_games[self.room_id] + logger.info(f"Game {self.room_id} resources cleaned up after player quit") + except KeyError: + logger.warning(f"Game {self.room_id} already removed from active_games") + except Exception as e: + logger.error(f"Error during game quit: {str(e)}", exc_info=True) + + async def all_players_ready(self, event): + """Handle the all_players_ready group message""" + logger.info(f"Broadcasting all players ready message for game {self.room_id}") + await self.send(text_data=json.dumps({ + 'type': 'all_players_ready', + 'message': event.get('message', 'All players are ready!'), + 'ready': True + })) message_handlers = { 'chat_message': chat_message, @@ -166,127 +287,525 @@ async def quit_game(self, data): 'game_init': game_init, 'game_over': game_over, 'quit_game': quit_game, + 'all_players_ready': all_players_ready, } class TournamentConsumer(AsyncWebsocketConsumer): async def connect(self): - self.tournament_id = self.scope['url_route']['kwargs']['tournament_id'] - if not self.tournament_id: - pass #TODO boh - self.channel_name = f'tournament_{self.tournament_id}' + self.tournament_id = self.scope['url_route']['kwargs'].get('tournament_id', None) + self.room_name = f'tournament_{self.tournament_id}' # Consistent naming + websocket_logger.info(f"New tournament connection: {self.tournament_id}") + + # Check authentication - reject AnonymousUser + user = self.scope.get('user') + if not user or not hasattr(user, 'user_id'): + websocket_logger.warning(f"Unauthenticated tournament connection attempt for {self.tournament_id}") + await self.close(code=4001) + return + + # Store authenticated user info + self.player_id = user.user_id + websocket_logger.info(f"Authenticated user {self.player_id} connected to tournament {self.tournament_id}") + # Join room group await self.channel_layer.group_add( + self.room_name, self.channel_name ) await self.accept() + logger.info(f"Tournament websocket connection accepted: {self.tournament_id}") + # Send welcome message + await self.send(text_data=json.dumps({ + 'type': 'tournament_connection_success', + 'message': f'Welcome to tournament {self.tournament_id}!', + 'player_id': self.player_id + })) + + await self.join(data={}) - #TODO handle disconnection async def disconnect(self, close_code): - if self.room_id in active_games: - active_games[self.room_id].quit_game() + websocket_logger.info(f"Tournament disconnected: {self.tournament_id}, code={close_code}") + + # Leave room group + await self.channel_layer.group_discard( + self.room_name, # Use room_name instead of channel_name + self.channel_name + ) + + # Handle cleanup for tournament resources if needed + if hasattr(self, 'room_id') and self.room_id in active_games: + logger.info(f"Cleaning up game resources for tournament {self.tournament_id}") + active_games[self.room_id].quit_game(getattr(self, 'player_id', None)) del active_games[self.room_id] async def receive(self, text_data): - data = json.loads(text_data) - message_type = data.get('type') - # print(f"message_type: {message_type}") - handler = self.message_handlers.get(message_type, self.default_handler) - await handler(self, data) + try: + data = json.loads(text_data) + message_type = data.get('type') + websocket_logger.debug(f"Received tournament message: type={message_type}, tournament={self.tournament_id}") + handler = self.message_handlers.get(message_type, self.default_handler) + await handler(self, data) + except json.JSONDecodeError: + websocket_logger.error(f"Invalid JSON received in tournament: {text_data[:100]}") + except Exception as e: + websocket_logger.exception(f"Error processing tournament message: {str(e)}") async def default_handler(self, data): - print(f"Unhandled message type: {data['type']}") + websocket_logger.warning(f"Unhandled tournament message type: {data.get('type')}") async def join(self, data): - if not self.tournament_id in active_tournaments: + """Simplified join function with clear flow""" + logger.info(f"Join request for tournament {self.tournament_id} by player {self.player_id}") + + try: + # Step 1: Get or create the tournament + tournament = await self._get_or_create_tournament(data) + if not tournament: + return # Error already sent + + # Step 2: Check if player is already in tournament + if self.player_id in tournament.players: + await self._send_already_joined_response(tournament) + return + + # Step 3: Add player to tournament + success = await self._add_player_to_tournament(tournament) + if not success: + return # Error already sent + + # Step 4: Send success response + await self._send_join_success_response(tournament) + + # Step 5: Check if tournament is full + await self._check_tournament_full(tournament) + + except Exception as e: + logger.error(f"Error in join for tournament {self.tournament_id}: {str(e)}", exc_info=True) + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': f'Failed to join tournament: {str(e)}' + })) + + async def _get_or_create_tournament(self, data): + """Get existing tournament or create new one""" + tournament = await tournament_manager.get_tournament(self.tournament_id) + + if tournament: + logger.info(f"Found existing tournament {self.tournament_id}") + return tournament + + # Tournament doesn't exist in memory, try to load from database + try: + from .models import Tournament + tournament_db = await database_sync_to_async(Tournament.objects.get)(id=self.tournament_id) + + # Create tournament manager instance + tournament = await tournament_manager.create_tournament( + tournament_id=self.tournament_id, + name=tournament_db.name, + max_players=tournament_db.max_partecipants, + creator_id=tournament_db.creator.user_id if tournament_db.creator else self.player_id + ) + + # Load existing participants from database + await tournament.load_players_from_db() + logger.info(f"Loaded tournament {tournament_db.name} from database with {tournament.nbr_player} players") + return tournament + + except Exception as db_error: + logger.warning(f"Failed to load tournament from database: {str(db_error)}") + + # Fallback: create new tournament (for cases where tournament doesn't exist in DB yet) try: - name = data.get('name') - max_p = data.get('max_p') - req_lvl = data.get('req_lvl') - player = data.get('player') - player_id = player.user_id - active_tournaments[self.tournament_id] = TournamentState(self.tournament_id, name, max_p, req_lvl, player_id) - except Exception as e: - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : f'creation failed because {e}' - })) - else: - self.send(text_data=json.dumps({ - 'type' : 'success', - 'success' : f'Tournament {name} created' - })) - else: - player = data.get('player') - ret = active_tournaments[self.tournament_id].add_player(player) - if ret != "Player added to the tournament": - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : ret + name = data.get('name', f'Tournament_{self.tournament_id}') + max_p = data.get('max_p', 8) + + tournament = await tournament_manager.create_tournament( + tournament_id=self.tournament_id, + name=name, + max_players=max_p, + creator_id=self.player_id + ) + logger.info(f"Created new tournament {name} with creator {self.player_id}") + return tournament + + except Exception as create_error: + logger.error(f"Failed to create tournament: {str(create_error)}") + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': f'Failed to create tournament: {str(create_error)}' })) + return None + + async def _send_already_joined_response(self, tournament): + """Send response for player already in tournament""" + logger.info(f"Player {self.player_id} already in tournament {self.tournament_id}") + await self.send(text_data=json.dumps({ + 'type': 'success', + 'success': f'Welcome back to tournament {tournament.name}', + 'already_joined': True, + 'tournament_data': { + 'name': tournament.name, + 'current_players': tournament.nbr_player, + 'max_players': tournament.max_p, + 'creator_id': tournament.creator_id, + 'players': tournament.players, + 'initialized': tournament.initialized, + 'is_complete': tournament.is_complete + } + })) + + async def _add_player_to_tournament(self, tournament): + """Add player to tournament and database""" + user = self.scope.get('user') + user_dict = { + 'user_id': self.player_id, + 'username': getattr(user, 'username', f'User_{self.player_id}') + } + + # Add to tournament memory + result = tournament.add_player(user_dict) + if result != "Player added to the tournament": + logger.warning(f"Failed to add player {self.player_id} to tournament: {result}") + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': result + })) + return False + + # Add to database + try: + from .models import Tournament, UserProfile + tournament_db = await database_sync_to_async(Tournament.objects.get)(id=self.tournament_id) + user_profile_result = await database_sync_to_async(UserProfile.objects.get_or_create)( + user_id=self.player_id, + defaults={ + 'username': getattr(user, 'username', f'User_{self.player_id}'), + 'email': f'user_{self.player_id}@example.com' + } + ) + user_profile = user_profile_result[0] + await database_sync_to_async(user_profile.tournaments.add)(tournament_db) + tournament_db.partecipants = tournament.nbr_player + await database_sync_to_async(tournament_db.save)() + logger.info(f"Player {self.player_id} added to tournament {self.tournament_id} in database") + + except Exception as db_error: + logger.error(f"Failed to add player to database: {str(db_error)}") + # Continue anyway, memory state is correct + + return True + + async def _send_join_success_response(self, tournament): + """Send success response for joining tournament""" + await self.send(text_data=json.dumps({ + 'type': 'success', + 'success': f'Successfully joined tournament {tournament.name}', + 'newly_joined': True, + 'tournament_data': { + 'name': tournament.name, + 'current_players': tournament.nbr_player, + 'max_players': tournament.max_p, + 'creator_id': tournament.creator_id, + 'players': tournament.players, + 'initialized': tournament.initialized, + 'is_complete': tournament.is_complete + } + })) + + async def _check_tournament_full(self, tournament): + """Check if tournament is full and notify if ready to start""" + if tournament.nbr_player >= tournament.max_p: + logger.info(f"Tournament {self.tournament_id} is full with {tournament.nbr_player} players") + await self.channel_layer.group_send( + self.room_name, + { + 'type': 'tournament_auto_start', + 'message': 'Tournament is full! Creator can now start the tournament.', + 'tournament_data': { + 'players': tournament.players, + 'max_players': tournament.max_p, + 'ready_to_start': True + } + } + ) + + async def get_ready(self, data): + """Message handler for preparing all players for next round""" + logger.info(f"Get ready message for tournament {self.tournament_id}") + + # Use authenticated user from connection + player_id = self.player_id + + try: + tournament = await tournament_manager.get_tournament(self.tournament_id) + if tournament: + # Broadcast get ready message to all tournament partecipants + await self.channel_layer.group_send( + self.room_name, # Use room_name instead of channel_name + { + 'type': 'tournament_get_ready', + 'message': 'Get ready for the next round!', + 'round_info': data.get('round_info', {}), + 'sender': player_id + } + ) + + logger.info(f"Get ready message sent to all players in tournament {self.tournament_id}") else: - self.send(text_data=json.dumps({ - 'type' : 'success', - 'success' : ret + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': 'Tournament not found' })) + except Exception as e: + logger.error(f"Error in get_ready: {str(e)}", exc_info=True) + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': f'Error preparing round: {str(e)}' + })) + + async def start_tournament(self, data): + """Message handler for creator to initialize tournament brackets""" + logger.info(f"Start tournament request for tournament {self.tournament_id}") - async def start(self, data): - player = data.get('player') - if self.tournament_id in active_tournaments: - if player.user_id == active_tournaments[self.tournament_id].creator: - ret = active_tournaments[self.tournament_id].start() - self.send(text_data=ret) + # Use authenticated user from connection + player_id = self.player_id + + try: + tournament = await tournament_manager.get_tournament(self.tournament_id) + if tournament: + # Check if this player is the tournament creator + if tournament.creator_id == player_id: + # Call the tournament's start method to initialize brackets + result = tournament.start() + + if result['type'] == 'success': + logger.info(f"Tournament initialized successfully by creator {player_id} in tournament {self.tournament_id}") + + # Notify all players that tournament has been initialized + await self.channel_layer.group_send( + self.room_name, + { + 'type': 'tournament_initialized', + 'message': 'Tournament brackets have been initialized! Rounds will start automatically.', + 'tournament_data': { + 'players': tournament.players, + 'max_players': tournament.max_p, + 'initialized': True, + 'auto_rounds': True + } + } + ) + + await self.send(text_data=json.dumps({ + 'type': 'success', + 'success': result['success'] + })) + else: + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': result['error'] + })) + else: + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': 'Only tournament creator can start the tournament' + })) else: - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : 'Only the creator can start the tournament' + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': 'Tournament not found' })) - else: - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : 'Tournament empty' + except Exception as e: + logger.error(f"Error in start_tournament: {str(e)}", exc_info=True) + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': f'Error starting tournament: {str(e)}' })) - - async def create_game(self, data): - player_1 = data.get('player_1') - player_2 = data.get('player_2') - self.send(text_data=json.dumps({ - 'type' : 'create_game', - 'player_1' : player_1, - 'player_2' : player_2, - 'tournament_id' : self.tournament_id + + async def start_round(self, data): + """Message handler for creator to start tournament round (legacy - now auto-started)""" + logger.info(f"Manual start round request for tournament {self.tournament_id} (rounds are now auto-started)") + + await self.send(text_data=json.dumps({ + 'type': 'info', + 'message': 'Rounds start automatically in this tournament system' + })) + + async def end_round(self, data): + """Message handler for ending tournament round (legacy - now auto-ended)""" + logger.info(f"Manual end round request for tournament {self.tournament_id} (rounds end automatically)") + + await self.send(text_data=json.dumps({ + 'type': 'info', + 'message': 'Rounds end automatically when all games complete in this tournament system' + })) + + # Channel layer message handlers (these handle group_send messages) + async def tournament_get_ready(self, event): + """Handle get ready broadcast message""" + await self.send(text_data=json.dumps({ + 'type': 'get_ready', + 'message': event['message'], + 'round_info': event.get('round_info', {}), + 'sender': event.get('sender') + })) + + async def tournament_start_round(self, event): + """Handle start round broadcast message""" + await self.send(text_data=json.dumps({ + 'type': 'start_round', + 'message': event['message'], + 'round_data': event.get('round_data', {}), + 'games': event.get('games', []), + 'creator': event.get('creator') + })) + + async def tournament_end_round(self, event): + """Handle end round broadcast message""" + await self.send(text_data=json.dumps({ + 'type': 'end_round', + 'message': event['message'], + 'results': event.get('results', {}), + 'next_round_info': event.get('next_round_info', {}), + 'creator': event.get('creator') + })) + + async def tournament_game_completed(self, event): + """Handle tournament game completion notification""" + logger.info(f"Tournament game completed: {event.get('game_id')} - Winner: {event.get('winner')}") + await self.send(text_data=json.dumps({ + 'type': 'game_completed', + 'game_id': event.get('game_id'), + 'winner': event.get('winner'), + 'loser': event.get('loser'), + 'scores': event.get('scores', {}), + 'message': f"Game {event.get('game_id')} completed!" })) - async def game_result(self, data): - if self.tournament_id in active_tournaments: - winner = data.get('winner') - ret = active_tournaments[self.tournament_id].brackets(winner) - if ret != "Tournament ended" or ret != "Game registered successfully": - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : ret + async def get_brackets(self, data): + """Message handler for getting tournament brackets""" + logger.info(f"Get brackets request for tournament {self.tournament_id}") + + # Use authenticated user from connection + player_id = self.player_id + + try: + tournament = await tournament_manager.get_tournament(self.tournament_id) + if tournament: + # Generate brackets data using the new method + brackets_data = tournament.get_brackets() + + await self.send(text_data=json.dumps({ + 'type': 'brackets', + 'brackets': brackets_data, + 'message': 'Tournament brackets retrieved successfully' })) else: - self.send(text_data=json.dumps({ - 'type' : 'success', - 'success' : ret + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': 'Tournament not found in active tournaments' })) - else: - self.send(text_data=json.dumps({ - 'type' : 'error', - 'error' : 'Tournament empty' + + except Exception as e: + logger.error(f"Error getting tournament brackets: {str(e)}", exc_info=True) + await self.send(text_data=json.dumps({ + 'type': 'error', + 'error': f'Error getting brackets: {str(e)}' })) - async def tournament_cancelled(self, data): + async def tournament_auto_start(self, event): + """Handle tournament auto-start notification""" await self.send(text_data=json.dumps({ - 'type': 'tournament_cancelled', - 'message': data['message'] + 'type': 'tournament_auto_start', + 'message': event['message'], + 'tournament_data': event.get('tournament_data', {}) })) + async def tournament_initialized(self, event): + """Handle tournament initialization notification""" + await self.send(text_data=json.dumps({ + 'type': 'tournament_initialized', + 'message': event['message'], + 'tournament_data': event.get('tournament_data', {}) + })) + + async def tournament_brackets_update(self, event): + """Handle tournament brackets update broadcast""" + await self.send(text_data=json.dumps({ + 'type': 'brackets_update', + 'message': event.get('message', 'Tournament brackets updated'), + 'brackets': event.get('brackets', {}), + 'round_info': event.get('round_info', {}) + })) + + async def tournament_complete(self, event): + """Handle tournament completion notification""" + await self.send(text_data=json.dumps({ + 'type': 'tournament_complete', + 'message': event['message'], + 'winner': event.get('winner'), + 'tournament_data': event.get('tournament_data', {}) + })) + + async def create_game(self, event): + """Handle create_game notification from tournament manager""" + logger.info(f"Tournament game notification: {event.get('player_1')} vs {event.get('player_2')} for tournament {self.tournament_id}") + + try: + player_1_id = event.get('player_1') + player_2_id = event.get('player_2') + tournament_id = event.get('tournament_id') + game_id = event.get('game_id') + + # Only send notification if this consumer belongs to one of the players in the game + if self.player_id not in [player_1_id, player_2_id]: + logger.debug(f"Player {self.player_id} not involved in game {game_id}, skipping notification") + return + + # Get player usernames for display + try: + from .models import UserProfile + player_1 = await database_sync_to_async(UserProfile.objects.get)(user_id=player_1_id) + player_2 = await database_sync_to_async(UserProfile.objects.get)(user_id=player_2_id) + + player_1_username = await database_sync_to_async(lambda: player_1.username)() + player_2_username = await database_sync_to_async(lambda: player_2.username)() + except Exception: + player_1_username = f"Player_{player_1_id}" + player_2_username = f"Player_{player_2_id}" + + # Notify only the relevant player + await self.send(text_data=json.dumps({ + 'type': 'game_created', + 'message': f'Your match is ready! {player_1_username} vs {player_2_username}', + 'game_data': { + 'game_id': game_id, + 'player_1': { + 'user_id': player_1_id, + 'username': player_1_username + }, + 'player_2': { + 'user_id': player_2_id, + 'username': player_2_username + }, + 'tournament_id': tournament_id + } + })) + + logger.info(f"Game notification sent to player {self.player_id} for game {game_id}") + + except Exception as e: + logger.error(f"Error handling game creation notification: {str(e)}") + + # Message handlers dictionary message_handlers = { 'join': join, - 'start': start, - 'create_game': create_game, - 'game_result': game_result, + 'get_ready': get_ready, + 'start_tournament': start_tournament, + 'start_round': start_round, + 'end_round': end_round, + 'get_brackets': get_brackets, } \ No newline at end of file diff --git a/Back-End/pongProject/pong_app/middleware.py b/Back-End/pongProject/pong_app/middleware.py index aa70a64f..a9708c3e 100644 --- a/Back-End/pongProject/pong_app/middleware.py +++ b/Back-End/pongProject/pong_app/middleware.py @@ -1,18 +1,21 @@ -# filepath: /home/lollo/Documents/Fides/Back-End/chat/my_chat/middleware.py from urllib.parse import parse_qs from channels.middleware import BaseMiddleware from channels.db import database_sync_to_async from django.contrib.auth.models import AnonymousUser from channels.auth import AuthMiddlewareStack from django.core.cache import cache -from rest_framework_simplejwt.tokens import AccessToken -from rest_framework_simplejwt.authentication import JWTAuthentication +from django.db import OperationalError, connections from rest_framework.permissions import BasePermission from .models import UserProfile import logging from rest_framework import authentication, exceptions from django.conf import settings -logger = logging.getLogger('django') +import jwt + +# Get dedicated loggers for different components +logger = logging.getLogger('pong_app') +auth_logger = logging.getLogger('django.request') +websocket_logger = logging.getLogger('websockets') class ServiceAuthentication(authentication.BaseAuthentication): """ @@ -20,107 +23,170 @@ class ServiceAuthentication(authentication.BaseAuthentication): Authenticates requests from other microservices using API keys and optionally JWT tokens. """ def authenticate(self, request): - # Only apply this authentication to the new_user endpoint - if not request.path.endswith('/chat/new_user/') or request.method != 'POST': + # Only apply this authentication to specific endpoints + if not request.path.endswith('/pong/api/endpoint/') or request.method != 'POST': return None # Check for API key api_key = request.headers.get('X-API-KEY') if not api_key or api_key != settings.API_KEY: - logger.warning(f"Invalid API key attempt: {api_key}") + auth_logger.warning(f"Invalid API key attempt: {api_key}, path: {request.path}") raise exceptions.AuthenticationFailed('Invalid API key') - # For additional security, you could also validate the JWT token - # if you want to ensure the request comes from an authenticated service + # For additional security, validate JWT token auth_header = request.headers.get('Authorization') if auth_header and auth_header.startswith('Bearer '): token = auth_header.replace('Bearer ', '') try: - # You could validate the token or extract service info - # This is optional since we're already validating with API key - decoded_token = AccessToken(token) - # You could check for specific claims that identify the service - # service_name = decoded_token.get('service_name') - logger.info(f"Request from authenticated service") + # Validate the token manually + decoded_payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=['HS256'] + ) + auth_logger.info(f"Request from authenticated service: {request.path}") + except jwt.ExpiredSignatureError: + auth_logger.warning("Service JWT token has expired") + except jwt.InvalidTokenError as e: + auth_logger.warning(f"Service JWT token validation failed: {str(e)}") except Exception as e: - # We don't fail here since API key was valid - logger.warning(f"JWT token validation failed: {str(e)}") + # Log but don't fail since API key was valid + auth_logger.warning(f"JWT token validation failed in service auth: {str(e)}") - # Return None for user since this is service-to-service authentication - # The view will handle creating the user based on the request data + # Return Anonymous user since this is service-to-service return (AnonymousUser(), None) @database_sync_to_async def get_user_from_token(token): try: - # Decode the token - access_token = AccessToken(token) - user_id = access_token['user_id'] + # Decode the token manually without Django apps validation + decoded_payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=['HS256'] + ) + user_id = decoded_payload.get('user_id') + if not user_id: + websocket_logger.warning("No user_id found in token payload") + return AnonymousUser() + # Check if the user is already cached user = cache.get(token) if user: + websocket_logger.debug(f"User {user_id} found in cache") return user # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) + user = UserProfile.objects.get(user_id=user_id) # Cache the user with a timeout (e.g., 5 minutes) cache.set(token, user, timeout=300) + websocket_logger.debug(f"User {user_id} loaded from database and cached") return user except UserProfile.DoesNotExist: + websocket_logger.warning(f"User {user_id} from token not found in database") return AnonymousUser() except Exception as e: + websocket_logger.error(f"Error processing token: {str(e)}") return AnonymousUser() -class JWTAuth(JWTAuthentication): +class JWTAuth(authentication.BaseAuthentication): """ - Enhanced JWT authentication class with caching support and improved error handling. + Enhanced JWT authentication class with cross-microservice support. + Uses manual JWT decoding to avoid Django app dependencies. """ + user_id_claim = 'user_id' + def authenticate(self, request): # Skip authentication for specific paths if needed - if request.path.endswith('/chat/new_user/') and request.method == 'POST': - logger.debug(f"Skipping JWT auth for {request.path}") - return (AnonymousUser(), None) + if request.path.endswith('/health') and request.method == 'GET': + auth_logger.debug(f"Skipping JWT auth for health check: {request.path}") + return None - # Check for cached authentication result first + # Log the auth attempt + auth_logger.debug(f"JWT auth attempt: {request.path}") + + # Get Authorization header auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if auth_header and auth_header.startswith('Bearer '): - token = auth_header.replace('Bearer ', '') + if not auth_header or not auth_header.startswith('Bearer '): + auth_logger.debug("No Bearer token found in request") + return None - # Try to get from cache first - cache_key = f"jwt_auth_{token}" - cached_result = cache.get(cache_key) + token = auth_header.replace('Bearer ', '') + + # Try to get from cache first + cache_key = f"pong_jwt_auth_{token[:16]}" # Use pong prefix and token prefix for unique key + cached_result = cache.get(cache_key) + + if cached_result is not None: + auth_logger.debug("Using cached JWT authentication result") + if cached_result == "anonymous": + return None + return cached_result - if cached_result is not None: - logger.debug("Using cached JWT authentication result") - if cached_result == "anonymous": - return None - return cached_result - + try: + # Decode the JWT token manually + decoded_payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=['HS256'] + ) + + user_id = decoded_payload.get('user_id') + if not user_id: + auth_logger.warning("No user_id found in token payload") + cache.set(cache_key, "anonymous", timeout=60) + return None + + # Try to get user from database try: - # Call parent class to validate token - auth_result = super().authenticate(request) + user = UserProfile.objects.get(user_id=user_id) + auth_result = (user, token) - # Cache the result (None results don't need caching) - if auth_result: - # Cache successful authentications for 5 minutes (adjust as needed) - cache.set(cache_key, auth_result, timeout=300) - logger.debug(f"JWT auth successful for user {auth_result[0]}") - return auth_result - else: - # Cache negative results for a shorter time (1 minute) - cache.set(cache_key, "anonymous", timeout=60) - return None - - except Exception as e: - # Log the error but don't cache exceptions - logger.warning(f"JWT authentication failed: {str(e)}") + # Cache successful authentications for 5 minutes + cache.set(cache_key, auth_result, timeout=300) + auth_logger.info(f"JWT auth successful for user {user_id}") + return auth_result + + except UserProfile.DoesNotExist: + auth_logger.warning(f"User {user_id} from token not found in database") + cache.set(cache_key, "anonymous", timeout=60) return None - + + except jwt.ExpiredSignatureError: + auth_logger.warning("JWT token has expired") + cache.set(cache_key, "anonymous", timeout=60) + return None + except jwt.InvalidTokenError as e: + auth_logger.warning(f"Invalid JWT token: {str(e)}") + cache.set(cache_key, "anonymous", timeout=60) + return None + except Exception as e: + auth_logger.error(f"JWT authentication error: {str(e)}") + return None + # No auth header or not a Bearer token - return super().authenticate(request) + return None + + def get_user(self, validated_token): + """ + Override get_user to use UserProfile instead of standard User model + """ + try: + user_id = validated_token[self.user_id_claim] + + # Look up user in UserProfile model + user = UserProfile.objects.get(user_id=user_id) + auth_logger.info(f"User {user_id} found in get_user") + return user + except UserProfile.DoesNotExist: + auth_logger.warning(f"User ID {user_id} from token not found in UserProfile") + return None + except Exception as e: + auth_logger.error(f"Error getting user from token: {str(e)}") + return None class JWTAuthMiddleware(BaseMiddleware): """ @@ -128,8 +194,12 @@ class JWTAuthMiddleware(BaseMiddleware): Extracts the JWT token from query parameters or headers and authenticates the user. """ async def __call__(self, scope, receive, send): + # Record connection attempt + websocket_logger.debug(f"WebSocket connection attempt: {scope['path']}") + # Skip authentication for specific paths if needed - if scope['path'] == '/chat/new_user/': + if scope['path'].endswith('/health/'): + websocket_logger.debug(f"Skipping auth for: {scope['path']}") scope['user'] = AnonymousUser() return await super().__call__(scope, receive, send) @@ -138,26 +208,29 @@ async def __call__(self, scope, receive, send): token = None if 'token' in query_string: - # Token from query param: ws://example.com/ws/chat/1/?token= + # Token from query param token = query_string['token'][0] + websocket_logger.debug("Found token in query string") else: - # Try to get token from headers (less common for WebSockets but possible) + # Try to get token from headers headers = dict(scope['headers']) auth_header = headers.get(b'authorization') if auth_header: token_str = auth_header.decode() if token_str.startswith('Bearer '): token = token_str[7:] # Remove 'Bearer ' prefix + websocket_logger.debug("Found token in authorization header") if token: # Get user from token try: scope['user'] = await self.get_user_from_jwt(token) - logger.info(f"WebSocket authenticated user: {scope['user']}") + websocket_logger.info(f"WebSocket authenticated: user={getattr(scope['user'], 'user_id', 'anonymous')}, path={scope['path']}") except Exception as e: - logger.error(f"WebSocket JWT authentication failed: {str(e)}") + websocket_logger.error(f"WebSocket JWT auth failed: {str(e)}") scope['user'] = AnonymousUser() else: + websocket_logger.warning(f"No token found for WebSocket connection: {scope['path']}") scope['user'] = AnonymousUser() return await super().__call__(scope, receive, send) @@ -165,25 +238,49 @@ async def __call__(self, scope, receive, send): @database_sync_to_async def get_user_from_jwt(self, token): try: - # Decode the JWT token - access_token = AccessToken(token) - user_id = access_token['user_id'] + websocket_logger.debug("Starting JWT token decode") + # Decode the JWT token manually + decoded_payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=['HS256'] + ) + websocket_logger.debug("JWT token decoded successfully") + + user_id = decoded_payload.get('user_id') + if not user_id: + websocket_logger.warning("No user_id found in token payload") + return AnonymousUser() - # Check if the user is already cached - user = cache.get(f"jwt_ws_user_{token}") + websocket_logger.debug(f"Looking for user with user_id: {user_id}") + # Check if the user is already cached + cache_key = f"pong_jwt_ws_user_{user_id}_{token[:8]}" # Use user_id and token prefix for unique key + user = cache.get(cache_key) if user: + websocket_logger.debug(f"WebSocket user {user_id} found in cache") return user - + + websocket_logger.debug("User not in cache, querying database") # Retrieve the user from the database try: - user = UserProfile.objects.get(id=user_id) - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(f"jwt_ws_user_{token}", user, timeout=300) + user = UserProfile.objects.get(user_id=user_id) + # Cache the user with specific pong prefix + cache.set(cache_key, user, timeout=300) + websocket_logger.debug(f"WebSocket user {user_id} loaded from database and cached") return user except UserProfile.DoesNotExist: + websocket_logger.warning(f"WebSocket user {user_id} not found in database") return AnonymousUser() + except jwt.ExpiredSignatureError: + websocket_logger.warning("WebSocket JWT token has expired") + return AnonymousUser() + except jwt.InvalidTokenError as e: + websocket_logger.warning(f"WebSocket JWT token is invalid: {str(e)}") + return AnonymousUser() except Exception as e: - logger.error(f"JWT token validation failed: {str(e)}") + websocket_logger.error(f"WebSocket JWT validation failed: {str(e)}") + import traceback + websocket_logger.error(f"WebSocket JWT validation traceback: {traceback.format_exc()}") return AnonymousUser() def JWTAuthMiddlewareStack(inner): @@ -192,107 +289,104 @@ def JWTAuthMiddlewareStack(inner): """ return JWTAuthMiddleware(AuthMiddlewareStack(inner)) -# class TokenAuthMiddleware(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# query_string = parse_qs(scope["query_string"].decode()) -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) -# try: -# token = query_string.get("token") -# if token: -# scope["user"] = await get_user_from_token(token[0]) -# else: -# scope["user"] = AnonymousUser() -# except Exception as e: -# scope["user"] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# def TokenAuthMiddlewareStack(inner): -# return TokenAuthMiddleware(AuthMiddlewareStack(inner)) +def UnhandledExceptionMiddleware(inner): + """ + Middleware to catch unhandled exceptions and log them. + """ + async def middleware(scope, receive, send): + try: + return await inner(scope, receive, send) + except Exception as e: + # Add connection type to the log + conn_type = scope.get('type', 'unknown') + path = scope.get('path', 'unknown_path') + + if conn_type == 'websocket': + websocket_logger.error(f"Unhandled WebSocket exception in {path}: {str(e)}", exc_info=True) + + if scope.get('websocket_state', None) == 'connected': + try: + await send({ + 'type': 'websocket.close', + 'code': 1011, + 'reason': 'Server error occurred' + }) + except: + websocket_logger.error("Failed to send close frame", exc_info=True) + else: + logger.error(f"Unhandled {conn_type} exception in {path} from {scope.get('client', 'unknown')}: {str(e)}", exc_info=True) + + raise + return middleware -# class TokenAuthMiddlewareHTTP(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) +import sys +import traceback +from django.http import JsonResponse +from django.conf import settings +import logging -# headers = dict(scope['headers']) -# token = headers.get(b'authorization') -# if token: -# token = token.decode().replace("Bearer ", "") -# scope['user'] = await self.get_user_from_token(token) -# else: -# scope['user'] = AnonymousUser() +logger = logging.getLogger('django.request') -# return await super().__call__(scope, receive, send) +class ExceptionMiddleware: + def __init__(self, get_response): + self.get_response = get_response -# @database_sync_to_async -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] - -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user + def __call__(self, request): + return self.get_response(request) -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() + def process_exception(self, request, exception): + # Stampa l'errore dettagliato sulla console + print("="*80) + print(f"ERRORE NON GESTITO: {str(exception)}") + print("Traceback completo:") + traceback.print_exc() + print("="*80) + + # Log dettagliato + logger.error( + f"Errore non gestito nella richiesta {request.path}: {str(exception)}", + exc_info=True, + extra={'request': request} + ) + if settings.DEBUG: + # In modalitร  debug, restituisci i dettagli dell'errore + return JsonResponse({ + 'error': str(exception), + 'traceback': traceback.format_exc(), + 'request_path': request.path, + 'request_method': request.method, + 'request_data': getattr(request, 'data', {}) + }, status=500) + + return None # Lascia che Django gestisca la risposta in produzione + +class HealthCheckMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Allow health check endpoint regardless of host + if request.path.endswith('/health'): # Liveness probe + return JsonResponse({'status': 'ok'}) + elif request.path.endswith('/ready'): # Readiness probe + db_ready = False + try: + # Check the default database connection + db_conn = connections['default'] + db_conn.cursor() # Attempt to get a cursor to check connectivity + # Optionally, you could execute a very simple query: + # with db_conn.cursor() as cursor: + # cursor.execute("SELECT 1") + # cursor.fetchone() + db_ready = True + except OperationalError: + logger.error("Readiness probe: Database connection failed.") + # db_ready remains False -# class TokenAuthPermission(BasePermission): -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] + if db_ready: + return JsonResponse({'status': 'ready', 'database': 'ok'}) + else: + return JsonResponse({'status': 'unready', 'database': 'unavailable'}, status=503) -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user - -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() - -# def has_permission(self, request, view): -# if request.path == '/chat/new_user/' and request.method == 'POST': -# request.user = AnonymousUser() -# return True -# token = request.META.get('HTTP_AUTHORIZATION') -# if token: -# token = token.replace("Bearer ", "") -# user = self.get_user_from_token(token) -# if user and not isinstance(user, AnonymousUser): -# request.user = user -# return True -# return False - -# from django.conf import settings -# from rest_framework.permissions import BasePermission - -# class APIKeyPermission(BasePermission): -# def has_permission(self, request, view): -# api_key = request.headers.get('X-API-KEY') -# if api_key: -# return api_key == settings.API_KEY -# return False \ No newline at end of file + return self.get_response(request) \ No newline at end of file diff --git a/Back-End/pongProject/pong_app/models.py b/Back-End/pongProject/pong_app/models.py index 00f4a555..75a73590 100644 --- a/Back-End/pongProject/pong_app/models.py +++ b/Back-End/pongProject/pong_app/models.py @@ -8,11 +8,25 @@ class Tournament(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=255) begin_date = models.DateTimeField(auto_now_add=True) - level_required = models.IntegerField() partecipants = models.IntegerField(default=0) max_partecipants = models.IntegerField() winner = models.ForeignKey('UserProfile', on_delete=models.SET_NULL, related_name='winner', null=True) creator = models.ForeignKey('UserProfile', on_delete=models.SET_NULL, related_name='creator', null=True) + status= models.CharField(choices=[ + ('pending', 'Pending'), + ('active', 'Active'), + ('completed', 'Completed') + ], max_length=10, default='pending') + + @property + def is_finished(self): + """Check if tournament is finished (completed status)""" + return self.status == 'completed' + + @property + def is_active(self): + """Check if tournament is currently active""" + return self.status == 'active' class UserProfile(models.Model): user_id = models.IntegerField(primary_key=True) @@ -22,9 +36,56 @@ class UserProfile(models.Model): class Game(models.Model): id = models.AutoField(primary_key=True) - player_1 = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='player_1') + player_1 = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='player_1', null=True) player_2 = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='player_2', null=True) player_1_score = models.IntegerField(default=0) player_2_score = models.IntegerField(default=0) begin_date = models.DateTimeField(auto_now_add=True) tournament_id = models.ForeignKey(Tournament, on_delete=models.CASCADE, related_name='game', null=True) + status = models.CharField(choices=[ + ('pending', 'Pending'), + ('active', 'Active'), + ('completed', 'Completed') + ], max_length=10, default='pending') + + @property + def winner(self): + """Return the winner of the game based on scores""" + if self.player_1_score > self.player_2_score: + return self.player_1 + elif self.player_2_score > self.player_1_score: + return self.player_2 + else: + return None # Tie or no scores yet + + @property + def winner_id(self): + """Return the winner's user_id""" + winner = self.winner + return winner.user_id if winner else None + + @property + def loser(self): + """Return the loser of the game based on scores""" + if self.player_1_score > self.player_2_score: + return self.player_2 + elif self.player_2_score > self.player_1_score: + return self.player_1 + else: + return None # Tie or no scores yet + + @property + def loser_id(self): + """Return the loser's user_id""" + loser = self.loser + return loser.user_id if loser else None + + @property + def is_finished(self): + """Check if game is finished""" + return self.status == 'completed' + + @property + def is_active(self): + """Check if game is currently active""" + return self.status == 'active' diff --git a/Back-End/pongProject/pong_app/notification.py b/Back-End/pongProject/pong_app/notification.py new file mode 100644 index 00000000..44fe2b76 --- /dev/null +++ b/Back-End/pongProject/pong_app/notification.py @@ -0,0 +1,75 @@ +from django.db import models +import os +import asyncio +# import aiohttp +from pongProject.settings import Microservices + +Types = { + 'IM' : 'Immediate', + 'QU' : 'Queued', + 'SC' : 'Scheduled', + 'SE' : 'Sent', +} + +class BaseNotification(models.Model): + id = models.AutoField(primary_key=True) + Sender = models.CharField(max_length=200, choices=[(key, key) for key in Microservices.keys()]) + message = models.JSONField() + + class Meta: + abstract = True + +class UserNotification(BaseNotification): + user_id = models.IntegerField(default=None, null=True) + + class Meta: + abstract = True + +class GroupNotification(BaseNotification): + group_id = models.IntegerField(default=None, null=True) + + class Meta: + abstract = True + +class ImmediateNotification(UserNotification, GroupNotification): + pass + +class ScheduledNotification(UserNotification, GroupNotification): + pass + +# async def SendNotification(Notification): +# noification_url = Microservices['Notifications'] +# async with aiohttp.ClientSession() as session: +# async with session.post(noification_url, data=Notification) as response: +# return response.status + +import requests +from rest_framework import serializers + +class ImmediateNotificationSerializer(serializers.ModelSerializer): + class Meta: + model = ImmediateNotification + fields = '__all__' + +class ScheduledNotificationSerializer(serializers.ModelSerializer): + class Meta: + model = ScheduledNotification + fields = '__all__' + + + +def SendNotificationSync(notification): + notification_url = Microservices['Notifications'] + '/notification/new/' + try: + # Serializza l'oggetto notification + serialized_notification = ImmediateNotificationSerializer(notification).data + headers = { + 'Content-Type': 'application/json', + 'X-API-KEY': os.getenv('API_KEY', "123") + } + response = requests.post(notification_url,headers=headers, json=serialized_notification) + print(f"response.json(): {response.json()}") + return response.status_code + except requests.RequestException as e: + print(f"Error sending notification: {e}") + return None \ No newline at end of file diff --git a/Back-End/pongProject/pong_app/physics_integration.py b/Back-End/pongProject/pong_app/physics_integration.py new file mode 100644 index 00000000..d836a8b7 --- /dev/null +++ b/Back-End/pongProject/pong_app/physics_integration.py @@ -0,0 +1,102 @@ +""" +Physics Engine Integration for GameState +Allows switching between different physics engines +""" +import os +import sys + +# Add physics engines to path +current_dir = os.path.dirname(os.path.abspath(__file__)) +physics_dir = os.path.join(current_dir, 'physics_engines') +sys.path.append(physics_dir) + +from physics_engines.legacy_physics import LegacyPhysicsEngine +from physics_engines.modern_physics import ModernPhysicsEngine + +class PhysicsManager: + """Manages physics engine switching and integration""" + + def __init__(self, engine_type="modern", **kwargs): + self.engine_type = engine_type + self.engine = self._create_engine(engine_type, **kwargs) + + def _create_engine(self, engine_type, **kwargs): + """Create physics engine instance""" + if engine_type == "legacy": + return LegacyPhysicsEngine(**kwargs) + elif engine_type == "modern": + return ModernPhysicsEngine(**kwargs) + else: + raise ValueError(f"Unknown engine type: {engine_type}") + + def switch_engine(self, new_type, preserve_state=True): + """Switch to different physics engine""" + if preserve_state and hasattr(self.engine, 'ball_pos'): + # Save current state + if self.engine_type == "legacy": + ball_x, ball_y = self.engine.ball_pos + ball_speed = self.engine.ball_speed + angle = self.engine.angle + else: # modern + ball_x, ball_y = self.engine.ball_pos.x, self.engine.ball_pos.y + ball_speed = self.engine.ball_velocity.length() + angle = 0 # Calculate from velocity + + # Create new engine + old_engine = self.engine + self.engine = self._create_engine(new_type, + ring_length=old_engine.ring_length, + ring_height=old_engine.ring_height, + ring_thickness=old_engine.ring_thickness + ) + + # Restore state + if new_type == "legacy": + self.engine.ball_pos = [ball_x, ball_y] + self.engine.ball_speed = ball_speed + self.engine.angle = angle + else: # modern + self.engine.ball_pos.x = ball_x + self.engine.ball_pos.y = ball_y + # Convert angle back to velocity + import math + self.engine.ball_velocity.x = ball_speed * math.cos(math.radians(angle)) + self.engine.ball_velocity.y = ball_speed * math.sin(math.radians(angle)) + else: + self.engine = self._create_engine(new_type) + + self.engine_type = new_type + print(f"๐Ÿ”„ Switched to {new_type} physics engine") + + def physics_step(self, *args, **kwargs): + """Execute physics step with current engine""" + return self.engine.physics_step(*args, **kwargs) + + def get_ball_position(self): + """Get ball position (unified interface)""" + if self.engine_type == "legacy": + return self.engine.ball_pos + else: + return [self.engine.ball_pos.x, self.engine.ball_pos.y] + + def get_engine_stats(self): + """Get engine-specific statistics""" + stats = { + "engine_type": self.engine_type, + "ball_position": self.get_ball_position() + } + + if hasattr(self.engine, 'get_stats'): + stats.update(self.engine.get_stats()) + + return stats + +# Convenience function for GameState integration +def create_physics_manager(engine_type="legacy", ring_length=160, ring_height=90, ring_thickness=3): + """Create physics manager with game parameters""" + return PhysicsManager( + engine_type=engine_type, + ring_length=ring_length, + ring_height=ring_height, + ring_thickness=ring_thickness + ) diff --git a/Back-End/pongProject/pong_app/serializer.py b/Back-End/pongProject/pong_app/serializer.py index c5f4ac90..d655f313 100644 --- a/Back-End/pongProject/pong_app/serializer.py +++ b/Back-End/pongProject/pong_app/serializer.py @@ -4,23 +4,87 @@ class PlayerSerializer(serializers.ModelSerializer): - # def validate_username(self, value): - # if len(str(value)) < 1: - # raise serializers.ValidationError('username is not valid') - # return value - - class Meta: - model = UserProfile - fields = '__all__' + class Meta: + model = UserProfile + fields = '__all__' + + +class gamesCreateSerializer(serializers.ModelSerializer): + def validate(self, data): + if data['player_1'] == data['player_2']: + raise serializers.ValidationError('player_1 and player_2 cannot be the same') + return data + class Meta: + model = Game + fields = [ + 'player_1', + 'player_2', + 'tournament_id' + ] + +class TournamentSerializer(serializers.ModelSerializer): + name = serializers.CharField(max_length=255) + begin_date = serializers.DateTimeField(read_only=True) + partecipants = serializers.IntegerField(min_value=0, default=0, read_only=True) + max_partecipants = serializers.IntegerField(min_value=2, help_text="Will be adjusted to nearest power of 2 (4, 8, 16, 32, 64)") + winner = serializers.SerializerMethodField(read_only=True) + creator = serializers.SerializerMethodField(read_only=True) + current_participants = serializers.SerializerMethodField(read_only=True) + status = serializers.ReadOnlyField() + is_finished = serializers.ReadOnlyField() + is_active = serializers.ReadOnlyField() + + def get_creator(self, obj): + """Return creator information""" + if obj.creator: + return { + 'user_id': obj.creator.user_id, + 'username': obj.creator.username + } + return None + + def get_winner(self, obj): + """Return winner information""" + if obj.winner: + return { + 'user_id': obj.winner.user_id, + 'username': obj.winner.username + } + return None + + def get_current_participants(self, obj): + """Return list of current participants""" + participants = obj.player.all() + return [ + { + 'user_id': participant.user_id, + 'username': participant.username + } + for participant in participants + ] + + def validate_name(self, value): + if len(str(value)) < 1: + raise serializers.ValidationError('name is not valid') + return value + + def validate_partecipants(self, value): + if value < 0: + raise serializers.ValidationError('partecipants is not valid') + return value + + def validate_max_partecipants(self, value): + if value < 2: + raise serializers.ValidationError('max_partecipants must be at least 2') + if value > 128: + raise serializers.ValidationError('max_partecipants cannot exceed 128') + return value + + class Meta: + model = Tournament + fields = ['id', 'name', 'begin_date', 'partecipants', 'max_partecipants', 'winner', 'creator', 'current_participants', 'status', 'is_finished', 'is_active'] class GamesSerializer(serializers.ModelSerializer): - # player_1 = serializers.PrimaryKeyRelatedField(queryset=Player.objects.all()) - # player_2 = serializers.PrimaryKeyRelatedField(queryset=Player.objects.all(), required=False) - # player_1_score = serializers.IntegerField(min_value=0, default=0) - # player_2_score = serializers.IntegerField(min_value=0, default=0) - # begin_date = serializers.DateTimeField(read_only=True, default=datetime.datetime.now) - # tournament_id = serializers.PrimaryKeyRelatedField(queryset=Tournament.objects.all(), required=False) - def validate(self, data): if data['player_1'] == data['player_2']: raise serializers.ValidationError('player_1 and player_2 cannot be the same') @@ -29,43 +93,59 @@ class Meta: model = Game fields = '__all__' -class TournamentSerializer(serializers.ModelSerializer): - name = serializers.CharField(max_length=255) - begin_date = serializers.DateTimeField(read_only=True, default=datetime.datetime.now) - level_required = serializers.IntegerField(min_value=0) - participants = serializers.IntegerField(min_value=0, default=0) - max_participants = serializers.IntegerField(min_value=4) - winner = serializers.IntegerField(min_value=0, default=0) +class GameHistorySerializer(serializers.ModelSerializer): + player_1 = PlayerSerializer(read_only=True) + player_2 = PlayerSerializer(read_only=True) + tournament_id = TournamentSerializer(read_only=True) + winner = serializers.SerializerMethodField() - def validate_name(self, value): - if len(str(value)) < 1: - raise serializers.ValidationError('name is not valid') - return value - - def validate_participants(self, value): - if value < 0: - raise serializers.ValidationError('participants is not valid') - return value + def get_winner(self, obj): + """Return winner information""" + if obj.player_1_score > obj.player_2_score: + return { + 'user_id': obj.player_1.user_id, + 'username': obj.player_1.username + } + elif obj.player_2_score > obj.player_1_score: + return { + 'user_id': obj.player_2.user_id, + 'username': obj.player_2.username + } + else: + # If scores are equal, return None or a message indicating no winner + return None + class Meta: + model = Game + fields = [ + 'id', + 'player_1', + 'player_2', + 'player_1_score', + 'player_2_score', + 'begin_date', + 'tournament_id', + 'status', + 'winner' + ] - class Meta: - model = Tournament - fields = '__all__' - class GameStateSerializer(serializers.Serializer): - player_1_score = serializers.IntegerField() - player_2_score = serializers.IntegerField() - player_1_pos = serializers.ListField(child=serializers.IntegerField()) - player_2_pos = serializers.ListField(child=serializers.IntegerField()) - ball_pos = serializers.ListField(child=serializers.FloatField()) - ball_speed = serializers.FloatField() - angle = serializers.FloatField() - ring_length = serializers.IntegerField() - ring_height = serializers.IntegerField() - ring_width = serializers.IntegerField() - ring_thickness = serializers.IntegerField() - p_length = serializers.IntegerField() - p_height = serializers.IntegerField() - p_width = serializers.IntegerField() - ball_radius = serializers.IntegerField() - p_speed = serializers.IntegerField() - \ No newline at end of file + player_1_score = serializers.IntegerField() + player_2_score = serializers.IntegerField() + player_1_pos = serializers.ListField(child=serializers.IntegerField()) + player_2_pos = serializers.ListField(child=serializers.IntegerField()) + ball_pos = serializers.ListField(child=serializers.FloatField()) + ball_speed = serializers.FloatField() + angle = serializers.FloatField() + ring_length = serializers.IntegerField() + ring_height = serializers.IntegerField() + +class UserStatisticsSerializer(serializers.Serializer): + """Simple serializer for user statistics response""" + user_id = serializers.IntegerField() + username = serializers.CharField() + total_games = serializers.IntegerField() + total_wins = serializers.IntegerField() + total_losses = serializers.IntegerField() + win_rate = serializers.FloatField() + total_tournaments = serializers.IntegerField() + total_tournament_wins = serializers.IntegerField() \ No newline at end of file diff --git a/Back-End/pongProject/pong_app/signals.py b/Back-End/pongProject/pong_app/signals.py index 494183ff..fa463a4f 100644 --- a/Back-End/pongProject/pong_app/signals.py +++ b/Back-End/pongProject/pong_app/signals.py @@ -1,18 +1,24 @@ # praticamente quando un modello viene creato ricveve il segnale e fa quello che gli chiedi import math import random +import logging from django.shortcuts import render, get_object_or_404 from django.db.models.signals import post_save from django.dispatch import receiver from .models import * -import logging from channels.layers import get_channel_layer from asgiref.sync import async_to_sync import time, asyncio from .serializer import * +from .physics_integration import create_physics_manager + +logger = logging.getLogger('pong_app') + +# โœ… CRITICAL: Reduce tick rate to prevent lag and channel overflow +tick_rate = 30 # Reduced from 60 to 30 FPS for better performance +websocket_update_rate = 20 # Send WebSocket updates at 20 FPS (every 3rd frame) # ring_size = [160 , 90] -tick_rate = 60 # self.ball_radius = 2.5 # self.p_width = 2 # ring_thickness = 3 @@ -25,25 +31,41 @@ def __init__(self, player_1, player_2, game_id, player_length, tournament_id): self.player_2 = player_2 self.player_1_score = 0 self.player_2_score = 0 - self.player_1_pos = [0 , 0] - self.player_2_pos = [0 , 0] + self.player_1_pos = [-60 , 0] + self.player_2_pos = [60 , 0] self.player_1_move = 0 self.player_2_move = 0 self.ball_pos = [0 , 0] - self.p_length = 0 - self.p_height = 0 - self.p_width = 0 - self.p_speed = 0 - self.ball_radius = 0 - self.ball_speed = 0 - self.ring_length = 0 - self.ring_height = 0 - self.ring_width = 0 - self.ring_thickness = 0 + # Initialize with safe defaults to prevent NaN values + self.p_length = 20 + self.p_height = 2.5 + self.p_width = 2 + self.p_speed = 1.0 + self.ball_radius = 2.5 + self.ball_speed = 0.6 + self.ring_length = 160 + self.ring_height = 90 # Critical: Initialize to prevent division by zero + self.ring_width = 200 + self.ring_thickness = 3 self.is_started = [0 , 0] self.wall_hit_pos = 0 self.avg_frame_time = [0 , 1] self.tournament_id = tournament_id + + # โœ… CRITICAL: Add frame tracking for reduced WebSocket update frequency + self.frame_count = 0 + self.last_websocket_update = 0 + + # ๐Ÿš€ NEW: Physics engine integration (can switch between legacy/modern) + # Set via environment variable or default to modern for better performance + engine_type = "modern" # Using modern physics engine! + self.physics = create_physics_manager( + engine_type=engine_type, + ring_length=self.ring_length, + ring_height=self.ring_height, + ring_thickness=self.ring_thickness + ) + logger.info(f"Game {self.game_id} initialized with {engine_type} physics engine") async def start(self): self.running = True @@ -54,34 +76,127 @@ async def start(self): else: self.angle = random.uniform(110, 250) - print(f"Game {self.__dict__} started") - while self.ball_speed == 0: - await asyncio.sleep(0.1) + try: + from django.utils import timezone + from asgiref.sync import sync_to_async + await sync_to_async(Game.objects.filter(id=self.game_id).update)( + status='active' + ) + logger.info(f"Game {self.game_id} database updated: status=active") + except Exception as e: + logger.error(f"Failed to update game {self.game_id} start in database: {e}") + + logger.info(f"Game {self.game_id} starting with angle: {self.angle}") + # Wait a bit longer for game_init, but don't wait forever + await asyncio.sleep(1.5) # Wait max 1.5 seconds for initialization + + self.ball_speed = 1.0 + + logger.info(f"Game {self.game_id} starting main loop with ball_speed: {self.ball_speed}") + start_time = time.monotonic() + frame_count = 0 + while self.running: - start_time = time.monotonic() - - self.physics() + self.physics_step() self.movement() await self.update() - - if self.player_1_score == 5 or self.player_2_score == 5: #TODO add different games scores + + if self.player_1_score == 5 or self.player_2_score == 5: + logger.info(f"Game {self.game_id} ended due to score limit") self.running = False break - elapsed = time.monotonic() - start_time - self.avg_frame_time += [elapsed, 1] - await asyncio.sleep(max(0, tick_interval - elapsed)) - self.game_end() - - def game_end(self): - if self.tournament_id: - Game.objects.update_or_create( - player_1_score = self.player_1_score, - player_2_score = self.player_2_score, - tournament_id = self.tournament_id - ) + + frame_count += 1 + # Log periodically to confirm game is running + if frame_count % (tick_rate * 10) == 0: # Every 10 seconds + logger.info(f"Game {self.game_id} running - Frame {frame_count}") + + start_time += tick_interval + await asyncio.sleep(max(0, start_time - time.monotonic())) + logger.info(f"Game {self.game_id} ended with scores: Player 1: {self.player_1_score}, Player 2: {self.player_2_score}") + await self.game_end() + async def game_end(self): + logger = logging.getLogger(__name__) + logger.info(f"Game {self.game_id} ending with scores: Player 1: {self.player_1_score}, Player 2: {self.player_2_score}") + + # Save game to database asynchronously + try: + from django.utils import timezone + from asgiref.sync import sync_to_async + await sync_to_async(Game.objects.filter(id=self.game_id).update)( + player_1_score=self.player_1_score, + player_2_score=self.player_2_score, + status='completed' + ) + logger.info(f"Game {self.game_id} saved to database with scores P1:{self.player_1_score} P2:{self.player_2_score}") + + # Get the updated game object to access properties + game = await sync_to_async(Game.objects.get)(id=self.game_id) + winner_id = await sync_to_async(lambda: game.winner_id)() + loser_id = await sync_to_async(lambda: game.loser_id)() + + except Exception as e: + logger.error(f"Error saving game {self.game_id} to database: {str(e)}") + # Fallback: calculate winner manually if DB access fails + if self.player_1_score > self.player_2_score: + winner_id = self.player_1.user_id + loser_id = self.player_2.user_id + else: + winner_id = self.player_2.user_id + loser_id = self.player_1.user_id + + # If this is a tournament game, register the result + if self.tournament_id and winner_id and loser_id: + try: + from .tournament_manager import tournament_manager + tournament = await tournament_manager.get_tournament(self.tournament_id) + if tournament: + await tournament.register_game_result(self.game_id, winner_id, loser_id) + logger.info(f"Tournament game result registered for game {self.game_id}") + + # Notify tournament about game completion + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'tournament_game_completed', + 'game_id': self.game_id, + 'winner': winner_id, + 'loser': loser_id, + 'scores': { + 'player_1_score': self.player_1_score, + 'player_2_score': self.player_2_score + } + } + ) + else: + logger.warning(f"Tournament {self.tournament_id} not found in active tournaments") + except Exception as e: + logger.error(f"Error registering tournament result: {str(e)}") + + # Send game over message to players + try: + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'game_{self.game_id}', + { + 'type': 'game_over', + 'winner': winner_id, + 'loser': loser_id, + 'final_scores': { + 'player_1_score': self.player_1_score, + 'player_2_score': self.player_2_score + } + } + ) + except Exception as e: + logger.error(f"Error sending game over message: {str(e)}") + + + # Legacy collision detection methods (kept for compatibility) def p1_is_hit(self): if ( self.ball_pos[0] - self.ball_radius - self.ball_speed <= self.player_1_pos[0] + self.p_width / 2 and @@ -102,37 +217,65 @@ def p2_is_hit(self): return True return False - #TODO fixing radius and thickness and create function for score - - def physics(self): - self.ball_pos[0] += self.ball_speed * math.cos(math.radians(self.angle)) - self.ball_pos[1] += self.ball_speed * -math.sin(math.radians(self.angle)) - if self.ball_pos[0] < 0 and self.p1_is_hit(): - hit_pos = self.ball_pos[1] - self.player_1_pos[1] - self.wall_hit_pos = 0 - self.angle = hit_pos / self.p_length * -90 - if (self.ball_speed < 5 * self.p_length): - self.ball_speed += ball_acc - elif self.ball_pos[0] > 0 and self.p2_is_hit(): - hit_pos = self.ball_pos[1] - self.player_2_pos[1] - self.wall_hit_pos = 0 - self.angle = 180 + hit_pos / self.p_length * 90 - if (self.ball_speed < 5 * self.p_length): - self.ball_speed += ball_acc - elif (self.wall_hit_pos <= 0 and self.ball_pos[1] + self.ball_radius + self.ring_thickness + self.ball_speed >= self.ring_height / 2) or (self.wall_hit_pos >= 0 and self.ball_pos[1] - self.ball_radius - self.ring_thickness - self.ball_speed <= -self.ring_height / 2): - self.wall_hit_pos = self.ball_pos[1] - self.angle = -self.angle - self.check_score() - if (self.player_1_move > 0 and self.player_1_pos[1] + self.p_length / 2 < self.ring_height / 2 - self.ring_thickness) or (self.player_1_move < 0 and self.player_1_pos[1] - self.p_length / 2 > -self.ring_height / 2 + self.ring_thickness): - self.player_1_pos[1] += self.player_1_move - if (self.player_2_move > 0 and self.player_2_pos[1] + self.p_length / 2 < self.ring_height / 2 - self.ring_thickness) or (self.player_2_move < 0 and self.player_2_pos[1] - self.p_length / 2 > -self.ring_height / 2 + self.ring_thickness): - self.player_2_pos[1] += self.player_2_move + def physics_step(self): + """๐Ÿš€ NEW: Use physics manager instead of inline physics""" + # Sync game state to physics engine + if self.physics.engine_type == "legacy": + self.physics.engine.player_1_pos = self.player_1_pos.copy() + self.physics.engine.player_2_pos = self.player_2_pos.copy() + self.physics.engine.ball_pos = self.ball_pos.copy() + self.physics.engine.ball_speed = self.ball_speed + else: # modern + self.physics.engine.player_1_pos.x = self.player_1_pos[0] + self.physics.engine.player_1_pos.y = self.player_1_pos[1] + self.physics.engine.player_2_pos.x = self.player_2_pos[0] + self.physics.engine.player_2_pos.y = self.player_2_pos[1] + self.physics.engine.ball_pos.x = self.ball_pos[0] + self.physics.engine.ball_pos.y = self.ball_pos[1] + + # Execute physics step + result = self.physics.physics_step(ball_acc=ball_acc) + + # Sync back to game state + self.ball_pos = self.physics.get_ball_position() + + # Sync angle from physics engine + if hasattr(self.physics.engine, 'angle'): + self.angle = self.physics.engine.angle + + # Handle scoring + if result == "player_1_scores": + self.player_1_score += 1 + self.reset_ball(random.uniform(110, 250)) + elif result == "player_2_scores": + self.player_2_score += 1 + self.reset_ball(random.uniform(70, -70)) + + # Log physics stats occasionally for debugging + if self.frame_count % 300 == 0: # Every 10 seconds + stats = self.physics.get_engine_stats() + logger.debug(f"Game {self.game_id} physics stats: {stats}") + def switch_physics_engine(self, engine_type): + """๐Ÿ”ง Switch physics engine (for testing)""" + logger.info(f"Game {self.game_id} switching to {engine_type} physics engine") + self.physics.switch_engine(engine_type, preserve_state=True) + async def update(self): + # โœ… PERFORMANCE: Throttle WebSocket updates to reduce lag + self.frame_count += 1 + + # Only send WebSocket updates every 3rd frame (10 FPS instead of 30 FPS) + if self.frame_count % 2 != 0: + return + channel_layer = get_channel_layer() try: serialized_data = GameStateSerializer(self.to_dict()).data - # print(f"Sent game state: {serialized_data}") + # Reduce logging verbosity - only log occasionally + if self.frame_count % 300 == 0: # Log every 10 seconds at 30fps + logger.debug(f"Sending game state for game {self.game_id} (frame {self.frame_count})") + await channel_layer.group_send( f'game_{self.game_id}', { @@ -141,21 +284,18 @@ async def update(self): } ) except Exception as e: + logger.error(f"Error sending game state for game {self.game_id}: {e}") print(f"Error sending game state: {e}") #TODO logg - def check_score(self): - if self.ball_pos[0] - self.ball_radius <= -self.ring_length / 2: - self.player_2_score += 1 - self.reset_ball(random.uniform(70, -70)) - elif self.ball_pos[0] + self.ball_radius >= self.ring_length / 2 + self.ring_thickness: - self.player_1_score += 1 - self.reset_ball(random.uniform(110, 250)) - def reset_ball(self, angle): self.ball_pos = [0, 0] self.angle = angle self.ball_speed = 90 / 150 self.wall_hit_pos = 0 + + # Also reset physics engine state + if hasattr(self.physics, 'engine'): + self.physics.engine.reset_ball(angle) def movement(self): @@ -169,154 +309,151 @@ def movement(self): self.player_2_pos[1] -= self.p_speed def up(self, player): - print(f"Player {player} up") if player == self.player_1.user_id: - self.player_1_move = 1 + self.player_1_move = -1 # Set movement state, not direct position elif player == self.player_2.user_id: - self.player_2_move = 1 - + self.player_2_move = -1 + def down(self, player): if player == self.player_1.user_id: - self.player_1_move = -1 + self.player_1_move = 1 # Set movement state, not direct position elif player == self.player_2.user_id: - self.player_2_move = -1 - + self.player_2_move = 1 + def stop(self, player): if player == self.player_1.user_id: self.player_1_move = 0 elif player == self.player_2.user_id: self.player_2_move = 0 - + def to_dict(self): + # โœ… PERFORMANCE: Only send essential game state data to reduce payload size return { 'player_1_score': self.player_1_score, 'player_2_score': self.player_2_score, - 'player_1_pos': self.player_1_pos, - 'player_2_pos': self.player_2_pos, + 'player_1_pos': self.to_percent(self.player_1_pos), + 'player_2_pos': self.to_percent(self.player_2_pos), 'ball_pos': self.ball_pos, 'ball_speed': self.ball_speed, 'angle': self.angle, - 'ring_length': self.ring_length, - 'ring_height': self.ring_height, - 'ring_width': self.ring_width, - 'ring_thickness': self.ring_thickness, - 'p_length': self.p_length, - 'p_height': self.p_height, - 'p_width': self.p_width, - 'ball_radius': self.ball_radius, - 'p_speed': self.p_speed + # Send ring dimensions only occasionally (they don't change during game) + 'ring_length': self.ring_length if self.frame_count % 30 == 0 else None, + 'ring_height': self.ring_height if self.frame_count % 30 == 0 else None, } - - def quit_game(self): - print(f"Game {self.game_id} quit") - self.running = False - #TODO set to 0 who quit -# @receiver(post_save, sender=Game) -# def start_game(sender, instance, created, **kwargs): -# if created: -# game_state = GameState(instance.player_1, instance.player_2, instance.id, 10) -# instance.game_state = game_state -# instance.save() -# logger = logging.getLogger(__name__) -# logger.info(f'Game started: {instance}') -# else: -# logger = logging.getLogger(__name__) -# logger.info(f'Game already started: {instance}') - -class TournamentState: - def __init__(self, *args, **kwargs): - self.id = kwargs['tournament_id'] - self.name = kwargs['name'] - self.max_p = kwargs['max_p'] - self.req_lvl = kwargs['req_lvl'] - self.nbr_player = 1 - self.players = [] - self.next_round = [] - self.players.append(kwargs['player_id']) - self.creator = kwargs['player_id'] - def add_player(self, player): - p = get_object_or_404(UserProfile, user_id=player['user_id']) - if player['user_id'] in self.players: - return 'Player already in the tournament' - if p.level < self.req_lvl: - return 'Player level is too low' - if self.nbr_player >= self.max_p: - return 'Tournament is full' - self.nbr_player += 1 - self.players.append(player['user_id']) - return 'Player added to the tournament' - - def start(self): - if self.nbr_player < 3: - return { - "type": "error", - 'error':'Not enough players' - } - self.players = random.shuffle(self.players) - self.max_p = len(self.players) - next_2pow = int(math.pow(2, math.ceil(math.log2(self.nbr_player)))) - nbr_bye = next_2pow - self.nbr_player - self.partecipants = self.players - self.partecipants.extend([0] * nbr_bye) - self.half = len(self.partecipants) / 2 - first = self.partecipants[0:self.half] - last = self.partecipants[self.half:] - self.next_round = [] - for pair in zip(first, last): - if pair[0] == 0: - self.next_round.append(pair[1]) - elif pair[1] == 0: - self.next_round.append(pair[0]) - else: - self.create_game(pair[0], pair[1]) - return { - 'type' : 'success', - 'success' : 'Tournament started' + def to_percent(self, player_pos): + """Convert player position to percentage (0 = top, 100 = bottom)""" + normalized_pos = player_pos[1] + (self.ring_height / 2) # Shift from [-h/2, h/2] to [0, h] + percentage = (normalized_pos / self.ring_height) * 100 # Convert to percentage + + # Clamp percentage to valid range to prevent out-of-bounds values + percentage = max(0.0, min(100.0, percentage)) + + # Debug logging + logger.debug(f"Game {self.game_id}: pos_y={player_pos[1]}, ring_height={self.ring_height}, normalized={normalized_pos}, percentage={percentage}") + + return [player_pos[0], percentage] # Return [x, percentage] + + def quit_game(self, player_id=None): + """Handle game quit - can be called statically or as instance method""" + print(f"Game {getattr(self, 'game_id', 'unknown')} quit by player {player_id}") + if hasattr(self, 'running'): + self.running = False + #TODO: implement forfeit logic - set quitting player score to 0 and opponent to 5 + + + +@receiver(post_save, sender=Game) +def start_game(sender, instance, created, **kwargs): + from .notification import SendNotificationSync, ImmediateNotification + from .serializer import PlayerSerializer + logger = logging.getLogger(__name__) + + if created: + # Check if both players are set + if not instance.player_1 or not instance.player_2: + logger.warning(f'๐ŸŽฎ Game {instance.id} created but missing players: player_1={instance.player_1}, player_2={instance.player_2}') + return + + if getattr(instance.tournament_id, 'id', None) is not None: + logger.debug(f'๐Ÿ”„Game {instance.id} created with tournament_id: {instance.tournament_id.id}') + return + + logger.info(f'๐ŸŽฎ Creating new game {instance.id} between player {instance.player_1.user_id} and player {instance.player_2.user_id}') + + # Prepare notification data + notification_data = { + 'type': 'game_created', + 'game_id': instance.id, + 'player_1': PlayerSerializer(instance.player_1).data, + 'player_2': PlayerSerializer(instance.player_2).data, + 'tournament_id': getattr(instance.tournament_id, 'id', None) if instance.tournament_id else None } - def brackets(self, *args, **kwargs): + # Send notification to player 1 try: - if self.partecipants.get(kwargs['winner']) == None: - return 'Player not in the tournament' - self.next_round.append(kwargs['winner']) - if len(self.next_round) == self.half: - self.partecipants = self.next_round - self.next_round = [] - if len(self.partecipants) == 1: - self.winner = self.partecipants[0] - self.save_tournament() - return 'Tournament ended' - else: - self.half = len(self.partecipants) / 2 - first = self.partecipants[0:self.half] - last = self.partecipants[self.half:] - for pair in zip(first, last): - if pair[0] == 0: - self.next_round.append(pair[1]) - elif pair[1] == 0: - self.next_round.append(pair[0]) - else: - self.create_game(pair[0], pair[1]) - else: - return 'Game registered successfully' + notification_p1 = ImmediateNotification( + Sender='Pong', + message=notification_data, + user_id=instance.player_1.user_id + ) + SendNotificationSync(notification_p1) + logger.info(f'โœ… Game notification sent to player 1 (ID: {instance.player_1.user_id}) for game {instance.id}') except Exception as e: - return f'Error registering game: {e}' - + logger.error(f'โŒ Failed to send game notification to player 1 (ID: {instance.player_1.user_id if instance.player_1 else "None"}): {str(e)}') + print(f"โŒ Error sending notification to player 1: {str(e)}") - def save_tournament(self): - t = get_object_or_404(Tournament, id=self.id) - t.winner = self.winner - t.save() - - def create_game(self, player_1, player_2): - get_channel_layer().group_send( - f'tournament_{self.id}', - { - 'type': 'create_game', - 'player_1': player_1, - 'player_2': player_2, - 'tournament_id': self.id - } - ) \ No newline at end of file + # Send notification to player 2 + try: + notification_p2 = ImmediateNotification( + Sender='Pong', + message=notification_data, + user_id=instance.player_2.user_id + ) + SendNotificationSync(notification_p2) + logger.info(f'โœ… Game notification sent to player 2 (ID: {instance.player_2.user_id}) for game {instance.id}') + except Exception as e: + logger.error(f'โŒ Failed to send game notification to player 2 (ID: {instance.player_2.user_id if instance.player_2 else "None"}): {str(e)}') + print(f"โŒ Error sending notification to player 2: {str(e)}") + + logger.info(f'โœ… Game {instance.id} created successfully') + + else: + logger.debug(f'๐Ÿ”„ Game {instance.id} updated (not created)') + + +@receiver(post_save, sender=Tournament) +def create_tournament(sender, instance, created, **kwargs): + from .notification import SendNotificationSync, ImmediateNotification + logger = logging.getLogger(__name__) + + if created: + logger.info(f'๐Ÿ† Creating new tournament {instance.id}') + + # Prepare notification data + notification_data = { + 'type': 'tournament_created', + 'tournament_id': instance.id, + 'name': instance.name, + 'max_players': instance.max_partecipants, + 'creator_id': instance.creator.user_id, + 'begin_date': instance.begin_date.isoformat() if instance.begin_date else None, + } + + # Send notification to creator + try: + notification = ImmediateNotification( + Sender='Pong', + message=notification_data, + user_id=instance.creator.user_id + ) + SendNotificationSync(notification) + logger.info(f'โœ… Tournament notification sent to creator (ID: {instance.creator.user_id}) for tournament {instance.id}') + except Exception as e: + logger.error(f'โŒ Failed to send tournament notification to creator (ID: {instance.creator.user_id}): {str(e)}') + print(f"โŒ Error sending notification to creator: {str(e)}") + + logger.info(f'โœ… Tournament {instance.id} created successfully') + else: + logger.debug(f'๐Ÿ”„ Tournament {instance.id} updated (not created)') diff --git a/Back-End/pongProject/pong_app/tests.py b/Back-End/pongProject/pong_app/tests.py index 7ce503c2..742fe0fd 100644 --- a/Back-End/pongProject/pong_app/tests.py +++ b/Back-End/pongProject/pong_app/tests.py @@ -1,3 +1,58 @@ -from django.test import TestCase -# Create your tests here. +from django.test import TestCase, Client + +class PongAppURLTests(TestCase): + def setUp(self): + self.client = Client() + + def test_tournament_gen(self): + response = self.client.get('/pong/tournament') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_tournament_manage(self): + response = self.client.get('/pong/tournament/1/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_join_tournament(self): + response = self.client.get('/pong/tournament/join') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_end_tournament(self): + response = self.client.get('/pong/tournament/end') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_tournament_match_history(self): + response = self.client.get('/pong/tournament/match-history') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_game_gen(self): + response = self.client.get('/pong/game') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_game_manage(self): + response = self.client.get('/pong/game/1/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_check_pending_games(self): + response = self.client.get('/pong/games/pending/1/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_player_match_history(self): + response = self.client.get('/pong/games/history') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_player_gen(self): + response = self.client.get('/pong/player') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_player_manage(self): + response = self.client.get('/pong/player/1/') + self.assertIn(response.status_code, [200, 401, 403, 404]) + + def test_user_statistics(self): + response = self.client.get('/pong/player/stats') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_health_check(self): + response = self.client.get('/pong/health') + self.assertIn(response.status_code, [200, 401, 403]) diff --git a/Back-End/pongProject/pong_app/tournament_manager.py b/Back-End/pongProject/pong_app/tournament_manager.py new file mode 100644 index 00000000..b9decbe3 --- /dev/null +++ b/Back-End/pongProject/pong_app/tournament_manager.py @@ -0,0 +1,622 @@ +import asyncio +import json +import logging +from typing import Dict, List, Optional, Any +from channels.db import database_sync_to_async +from channels.layers import get_channel_layer +from datetime import datetime, timedelta + +logger = logging.getLogger('pong_app') + +class TournamentManager: + """ + Async tournament manager using asyncio tasks for auto-round progression + """ + + def __init__(self): + self.active_tournaments: Dict[int, 'TournamentState'] = {} + self.tournament_tasks: Dict[int, asyncio.Task] = {} + self.channel_layer = get_channel_layer() + + async def create_tournament(self, tournament_id: int, name: str, max_players: int, creator_id: int) -> 'TournamentState': + """Create a new tournament and start its management task""" + tournament = TournamentState( + tournament_id=tournament_id, + name=name, + max_p=max_players, + creator_id=creator_id, + manager=self + ) + + self.active_tournaments[tournament_id] = tournament + + # Start the tournament management task + task = asyncio.create_task(self._manage_tournament(tournament)) + self.tournament_tasks[tournament_id] = task + + logger.info(f"Tournament {tournament_id} created and management task started") + return tournament + + async def get_tournament(self, tournament_id: int) -> Optional['TournamentState']: + """Get tournament by ID, loading from database if not in memory""" + tournament = self.active_tournaments.get(tournament_id) + + if not tournament: + # Try to load from database + try: + from .models import Tournament + tournament_db = await database_sync_to_async(Tournament.objects.get)(id=tournament_id) + + # Create tournament state from database + tournament = TournamentState( + tournament_id=tournament_id, + name=tournament_db.name, + max_p=tournament_db.max_partecipants, + creator_id=tournament_db.creator.user_id if tournament_db.creator else None, + manager=self + ) + + # Load players from database + await tournament.load_players_from_db() + + # Add to active tournaments but don't start management task for completed tournaments + self.active_tournaments[tournament_id] = tournament + + if tournament_db.status != 'completed': + # Start management task only for active tournaments + task = asyncio.create_task(self._manage_tournament(tournament)) + self.tournament_tasks[tournament_id] = task + + logger.info(f"Tournament {tournament_id} loaded from database with {tournament.nbr_player} players") + + except Exception as e: + logger.warning(f"Failed to load tournament {tournament_id} from database: {e}") + return None + + return tournament + + async def remove_tournament(self, tournament_id: int): + """Remove tournament and cancel its management task""" + if tournament_id in self.tournament_tasks: + self.tournament_tasks[tournament_id].cancel() + del self.tournament_tasks[tournament_id] + + if tournament_id in self.active_tournaments: + del self.active_tournaments[tournament_id] + + logger.info(f"Tournament {tournament_id} removed and task cancelled") + + async def _manage_tournament(self, tournament: 'TournamentState'): + """Main tournament management loop""" + try: + logger.info(f"Starting tournament management for {tournament.tournament_id}") + + # Wait for tournament to be initialized (brackets created) + while not tournament.initialized: + await asyncio.sleep(1) + + logger.info(f"Tournament {tournament.tournament_id} initialized, starting auto-round progression") + + # Auto-start rounds until tournament is complete + while not tournament.is_complete: + if tournament.can_start_next_round(): + await tournament.start_round() + + # Wait for round to complete (with timeout) + await self._wait_for_round_completion(tournament) + else: + # Wait a bit before checking again + await asyncio.sleep(2) + + logger.info(f"Tournament {tournament.tournament_id} completed!") + + except asyncio.CancelledError: + logger.info(f"Tournament {tournament.tournament_id} management cancelled") + except Exception as e: + logger.error(f"Error in tournament {tournament.tournament_id} management: {e}", exc_info=True) + + async def _wait_for_round_completion(self, tournament: 'TournamentState'): + """Wait for round to complete with timeout handling""" + round_timeout = 300 # 5 minutes per round + start_time = datetime.now() + + while tournament.is_round_active and not tournament.is_complete: + # Check if round timeout exceeded + if datetime.now() - start_time > timedelta(seconds=round_timeout): + logger.warning(f"Round timeout in tournament {tournament.tournament_id}") + await tournament.handle_round_timeout() + break + + await asyncio.sleep(5) # Check every 5 seconds + + +class TournamentState: + """ + Enhanced tournament state with asyncio task integration + """ + + def __init__(self, tournament_id: int, name: str, max_p: int, creator_id: int, manager: TournamentManager = None): + self.tournament_id = tournament_id + self.name = name + self.max_p = max_p + self.creator_id = creator_id + self.manager = manager + + # Tournament state + self.players: List[int] = [] + self.nbr_player = 0 + self.initialized = False + self.is_complete = False + self.status = 'pending' # Initialize with pending status + + # Round management + self.current_round = 0 + self.is_round_active = False + self.partecipants: List[int] = [] + self.next_round: List[int] = [] + self.active_games: Dict[int, Dict] = {} # game_id -> game_info + self.round_start_time: Optional[datetime] = None + + # Tournament structure + self.brackets: Dict[int, List[int]] = {} # round -> [players] + self.winner: Optional[int] = None + + logger.info(f"TournamentState created: {name} (ID: {tournament_id})") + + def add_player(self, user_dict: Dict[str, Any]) -> str: + """Add player to tournament""" + player_id = user_dict['user_id'] + + if self.initialized: + return "Tournament has already started" + + if self.nbr_player >= self.max_p: + return "Tournament is full" + + if player_id in self.players: + return "Player already in tournament" + + self.players.append(player_id) + self.nbr_player += 1 + + logger.info(f"Player {player_id} added to tournament {self.tournament_id}. Players: {self.nbr_player}/{self.max_p}") + return "Player added to the tournament" + + async def load_players_from_db(self): + """Load players from database and sync with in-memory state""" + try: + from .models import Tournament + tournament_db = await database_sync_to_async(Tournament.objects.get)(id=self.tournament_id) + participants = await database_sync_to_async( + lambda: list(tournament_db.player.all()) + )() + # Update in-memory state + self.players = [p.user_id for p in participants] + self.nbr_player = len(self.players) + + # Load tournament status and other fields + self.status = tournament_db.status + if tournament_db.winner: + self.winner = tournament_db.winner.user_id + self.is_complete = True + + # Set initialized flag based on status + if self.status in ['active', 'completed']: + self.initialized = True + + logger.info(f"Loaded tournament {self.tournament_id} from database: {self.nbr_player} players, status: {self.status}") + return True + except Exception as e: + logger.error(f"Failed to load players from database for tournament {self.tournament_id}: {e}") + return False + + def start(self) -> Dict[str, Any]: + """Initialize tournament brackets""" + if self.initialized: + return {'type': 'error', 'error': 'Tournament already initialized'} + + if self.nbr_player < 2: + return {'type': 'error', 'error': 'Need at least 2 players to start tournament'} + + # Initialize brackets + self.partecipants = self.players.copy() + self.next_round = self.players.copy() + self.current_round = 0 + self.initialized = True + self.status = 'active' # Set tournament status to active + + # Create initial bracket structure + self.brackets[0] = self.players.copy() + + logger.info(f"Tournament {self.tournament_id} initialized with {self.nbr_player} players") + + # Update tournament status in database + asyncio.create_task(self._update_tournament_status_in_db('active')) + + return {'type': 'success', 'success': 'Tournament initialized successfully'} + + def can_start_next_round(self) -> bool: + """Check if next round can be started""" + if not self.initialized or self.is_complete or self.is_round_active: + return False + + # Check if we have players for next round + if len(self.next_round) < 2: + return False + + # If only one player left, tournament is complete + if len(self.next_round) == 1: + self.winner = self.next_round[0] + self.is_complete = True + return False + + return True + + async def start_round(self) -> Dict[str, Any]: + """Start the next tournament round (auto-called by manager)""" + if not self.can_start_next_round(): + return {'type': 'error', 'error': 'Cannot start round'} + + self.current_round += 1 + self.is_round_active = True + self.round_start_time = datetime.now() + self.partecipants = self.next_round.copy() + + # Clear next_round for this round (only winners will be added back) + self.next_round = [] + + # Create bracket for this round + self.brackets[self.current_round] = self.partecipants.copy() + + # Create games for this round + games = [] + remaining_players = self.partecipants.copy() + + # Pair players for games + while len(remaining_players) >= 2: + player_1 = remaining_players.pop(0) + player_2 = remaining_players.pop(0) + + # Send game creation message + await self._create_game(player_1, player_2) + games.append({'player_1': player_1, 'player_2': player_2}) + + # Handle odd player (bye) + if remaining_players: + bye_player = remaining_players[0] + logger.info(f"Player {bye_player} gets a bye in round {self.current_round}") + # Automatically advance bye player + await self.register_game_result(None, bye_player, None, auto_advance=True) + + logger.info(f"Round {self.current_round} started in tournament {self.tournament_id}") + logger.info(f"Participants: {self.partecipants}") + logger.info(f"Games created: {len(games)}") + logger.info(f"Active games: {list(self.active_games.keys())}") + + # Broadcast round start to all players + await self._broadcast_round_start(games) + + return {'type': 'success', 'success': f'Round {self.current_round} started'} + + async def register_game_result(self, game_id: str, winner: str, loser: str, auto_advance: bool = False) -> Dict[str, Any]: + """Register the result of a tournament game""" + if not self.is_round_active: + logger.warning(f"Attempted to register result for {game_id} but no active round in tournament {self.tournament_id}") + return {'type': 'error', 'error': 'No active round'} + + # Handle bye advancement + if auto_advance: + logger.info(f"Auto-advancing {winner} (bye) in tournament {self.tournament_id}") + if winner not in self.next_round: + self.next_round.append(winner) + logger.info(f"Bye player {winner} added to next round. Current next_round: {self.next_round}") + await self._check_round_completion() + return {'type': 'success', 'success': f'{winner} advanced (bye)'} + + # Check if this game belongs to the tournament + if game_id not in self.active_games: + logger.warning(f"Game {game_id} not found in tournament {self.tournament_id} active games: {list(self.active_games.keys())}") + return {'type': 'error', 'error': 'Game not found in tournament'} + + # Remove the completed game + game_info = self.active_games.pop(game_id) + logger.info(f"Game {game_id} completed in tournament {self.tournament_id}: {winner} beat {loser}") + logger.info(f"Game info: {game_info}") + logger.info(f"Remaining active games: {list(self.active_games.keys())}") + + # In single elimination, only the winner advances + if winner in self.partecipants: + if winner not in self.next_round: + self.next_round.append(winner) + logger.info(f"Winner {winner} added to next round. Current next_round: {self.next_round}") + logger.info(f"Loser {loser} eliminated from tournament") + else: + logger.warning(f"Winner {winner} already in next round: {self.next_round}") + else: + logger.error(f"Winner {winner} not in current participants: {self.partecipants}") + + # Check if round is complete (all games finished) + logger.info(f"Checking round completion. Active games remaining: {len(self.active_games)}") + await self._check_round_completion() + + return {'type': 'success', 'success': f'Result registered: {winner} beats {loser}'} + + async def _check_round_completion(self): + """Check if current round is complete and handle progression""" + if not self.is_round_active: + logger.warning(f"_check_round_completion called but no active round in tournament {self.tournament_id}") + return + + # Round is complete when no more active games + if len(self.active_games) == 0: + logger.info(f"Round {self.current_round} completed in tournament {self.tournament_id}") + logger.info(f"Winners advancing to next round: {self.next_round}") + + # Mark round as inactive + self.is_round_active = False + + # Check if tournament is complete + if len(self.next_round) <= 1: + logger.info(f"Tournament {self.tournament_id} completed!") + if self.next_round: + winner = self.next_round[0] + self.winner = winner # Set the winner + logger.info(f"Tournament winner: {winner}") + await self._broadcast_tournament_complete(winner) + else: + logger.error(f"Tournament {self.tournament_id} completed but no winner found!") + + # Mark tournament as complete + self.status = 'completed' + self.is_complete = True + self.completion_time = datetime.now() + + # Update database + await self._update_tournament_in_db() + + # Tournament is complete, don't schedule next round + return + + # Broadcast round end + await self._broadcast_round_end() + + # Schedule next round (wait a bit for clients to process round end) + await asyncio.sleep(3) + logger.info(f"Scheduling next round for tournament {self.tournament_id}") + await self.start_round() + else: + logger.info(f"Round {self.current_round} still active. Games remaining: {len(self.active_games)}") + logger.info(f"Active games: {list(self.active_games.keys())}") + logger.info(f"Current next_round: {self.next_round}") + + async def _complete_round(self): + """Complete the current round""" + self.is_round_active = False + + logger.info(f"Round {self.current_round} completed in tournament {self.tournament_id}") + logger.info(f"Winners advancing: {self.next_round}") + + # Check if tournament is complete + if len(self.next_round) == 1: + self.winner = self.next_round[0] + self.is_complete = True + await self._broadcast_tournament_complete() + else: + # Prepare for next round + self.partecipants = [] # Clear current round participants + await self._broadcast_round_complete() + + async def handle_round_timeout(self): + """Handle round timeout by checking connections and advancing players""" + logger.warning(f"Handling round timeout for tournament {self.tournament_id}") + + # Check for incomplete games and resolve them + unfinished_games = list(self.active_games.values()) + + for game_info in unfinished_games: + player_1 = game_info['player_1'] + player_2 = game_info['player_2'] + + # Check player connections (simplified - in real implementation, check WebSocket connections) + player_1_connected = await self._check_player_connection(player_1) + player_2_connected = await self._check_player_connection(player_2) + + if player_1_connected and not player_2_connected: + winner = player_1 + elif player_2_connected and not player_1_connected: + winner = player_2 + else: + # Both connected or both disconnected - random choice + import random + winner = random.choice([player_1, player_2]) + + logger.info(f"Timeout resolution: {winner} advances (connection check)") + await self.register_game_result(game_info['game_id'], winner, None, auto_advance=True) + + async def _check_player_connection(self, player_id: int) -> bool: + """Check if player is connected (simplified implementation)""" + # In a real implementation, you'd check WebSocket connections in channel layer + # For now, return True (assume connected) + return True + + async def _create_game(self, player_1: int, player_2: int): + """Create a game between two players""" + try: + from .models import Tournament, UserProfile, Game + + # Get the players and tournament + player_1_obj = await database_sync_to_async(UserProfile.objects.get)(user_id=player_1) + player_2_obj = await database_sync_to_async(UserProfile.objects.get)(user_id=player_2) + tournament_obj = await database_sync_to_async(Tournament.objects.get)(id=self.tournament_id) + + # Create the game + game = await database_sync_to_async(Game.objects.create)( + player_1=player_1_obj, + player_2=player_2_obj, + tournament_id=tournament_obj + ) + + # Store game info + self.active_games[game.id] = { + 'game_id': game.id, + 'player_1': player_1, + 'player_2': player_2, + 'created_at': datetime.now() + } + + # Broadcast game creation to tournament players + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'create_game', + 'game_id': game.id, + 'player_1': player_1, + 'player_2': player_2, + 'tournament_id': self.tournament_id + } + ) + + logger.info(f"Game {game.id} created for tournament {self.tournament_id}: {player_1} vs {player_2}") + + except Exception as e: + logger.error(f"Error creating game for tournament: {e}", exc_info=True) + + async def _broadcast_round_start(self, games: List[Dict]): + """Broadcast round start to all tournament players""" + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'tournament_start_round', + 'message': f'Round {self.current_round} has started!', + 'round_data': { + 'round_number': self.current_round, + 'games_count': len(games) + }, + 'games': games + } + ) + + async def _broadcast_round_complete(self): + """Broadcast round completion to all tournament players""" + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'tournament_end_round', + 'message': f'Round {self.current_round} completed!', + 'results': { + 'round': self.current_round, + 'winners': self.next_round + }, + 'next_round_info': { + 'players_advancing': len(self.next_round), + 'next_round_number': self.current_round + 1 + } + } + ) + + async def _broadcast_round_end(self): + """Broadcast round end to all tournament players""" + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'tournament_round_end', + 'message': f'Round {self.current_round} has ended!', + 'round_data': { + 'round_number': self.current_round, + 'winners': self.next_round, + 'players_advancing': len(self.next_round) + } + } + ) + + async def _broadcast_tournament_complete(self, winner=None): + """Broadcast tournament completion""" + tournament_winner = winner or self.winner + channel_layer = get_channel_layer() + await channel_layer.group_send( + f'tournament_{self.tournament_id}', + { + 'type': 'tournament_complete', + 'message': f'Tournament completed!', + 'winner': tournament_winner, + 'tournament_data': { + 'tournament_id': self.tournament_id, + 'name': self.name, + 'total_rounds': self.current_round, + 'total_players': self.nbr_player + } + } + ) + + async def _update_tournament_in_db(self): + """Update tournament status in database""" + try: + from .models import Tournament + + def update_tournament(): + tournament_obj = Tournament.objects.get(id=self.tournament_id) + tournament_obj.status = self.status + if hasattr(self, 'winner') and self.winner: + from .models import UserProfile + winner_obj = UserProfile.objects.get(user_id=self.winner) + tournament_obj.winner = winner_obj + tournament_obj.save() + return tournament_obj + + tournament_obj = await database_sync_to_async(update_tournament)() + + # await database_sync_to_async(tournament_obj.save)() + logger.info(f"Tournament {self.tournament_id} updated in database with status: {self.status}") + + except Exception as e: + logger.error(f"Failed to update tournament {self.tournament_id} in database: {e}") + + async def _update_tournament_status_in_db(self, status: str): + """Update tournament status in database""" + try: + from .models import Tournament + + tournament_obj = await database_sync_to_async(Tournament.objects.get)(id=self.tournament_id) + tournament_obj.status = status + await database_sync_to_async(tournament_obj.save)() + logger.info(f"Tournament {self.tournament_id} status updated to '{status}' in database") + + except Exception as e: + logger.error(f"Failed to update tournament {self.tournament_id} status in database: {e}") + + def get_brackets(self) -> Dict[str, Any]: + """Get tournament brackets data for frontend""" + # Convert active games to JSON-serializable format + active_games_serializable = [] + for game_info in self.active_games.values(): + game_data = game_info.copy() + # Convert datetime to string if present + if 'created_at' in game_data and isinstance(game_data['created_at'], datetime): + game_data['created_at'] = game_data['created_at'].isoformat() + active_games_serializable.append(game_data) + + return { + 'tournament_id': self.tournament_id, + 'name': self.name, + 'max_partecipants': self.max_p, + 'current_partecipants': self.nbr_player, + 'creator_id': self.creator_id, + 'players': self.players, + 'current_round': self.current_round, + 'is_round_active': self.is_round_active, + 'initialized': self.initialized, + 'is_complete': self.is_complete, + 'winner': self.winner, + 'brackets': self.brackets, + 'active_games': active_games_serializable, + 'next_round_players': self.next_round, + 'round_start_time': self.round_start_time.isoformat() if self.round_start_time else None, + 'completion_time': self.completion_time.isoformat() if hasattr(self, 'completion_time') and self.completion_time else None + } + +# Global tournament manager instance +tournament_manager = TournamentManager() diff --git a/Back-End/pongProject/pong_app/urls.py b/Back-End/pongProject/pong_app/urls.py index 5c38efd4..77f99eae 100644 --- a/Back-End/pongProject/pong_app/urls.py +++ b/Back-End/pongProject/pong_app/urls.py @@ -1,12 +1,31 @@ from django.urls import path from . import views +from django.urls import path +from . import views + urlpatterns = [ - path('tournament', views.TournamentGen.as_view(), name='tournament_gen'), - path('tournament//', views.TournamentManage.as_view(), name='tournament_manage'), - path('game', views.GameGen.as_view(), name='game_gen'), - path('game//', views.GameManage.as_view(), name='game_manage'), - path('join', views.JoinTournament.as_view(), name='join_tournament'), - path('history', views.PlayerMatchHistory.as_view(), name='history'), - path('player', views.PlayerGen.as_view(), name='player_gen'), + # Tournament endpoints + path('tournament', views.TournamentGen.as_view(), name='tournament_gen'), + path('tournament//', views.TournamentManage.as_view(), name='tournament_manage'), + path('tournament/join', views.JoinTournament.as_view(), name='join_tournament'), + path('tournament/leave', views.LeaveTournament.as_view(), name='leave_tournament'), + path('tournament/end', views.EndTournament.as_view(), name='end_tournament'), + path('tournament/start', views.StartTournament.as_view(), name='start_tournament'), + path('tournament/match-history', views.TournamentMatchHistory.as_view(), name='tournament_match_history'), + path('user-tournaments', views.UserTournaments.as_view(), name='user_tournaments'), + + # Game endpoints + path('game', views.GameGen.as_view(), name='game_gen'), + path('game//', views.GameManage.as_view(), name='game_manage'), + path('games/pending//', views.CheckPendingGames.as_view(), name='check_pending_games'), + path('games/history', views.PlayerMatchHistory.as_view(), name='player_match_history'), + + # Player endpoints + path('player', views.PlayerGen.as_view(), name='player_gen'), + path('player//', views.PlayerManage.as_view(), name='player_manage'), + path('player/stats', views.UserStatistics.as_view(), name='user_statistics'), + + # Health check + path('health', views.health_check, name='health_check'), ] \ No newline at end of file diff --git a/Back-End/pongProject/pong_app/views.py b/Back-End/pongProject/pong_app/views.py index 25d9a7b7..f7bdfa82 100644 --- a/Back-End/pongProject/pong_app/views.py +++ b/Back-End/pongProject/pong_app/views.py @@ -1,12 +1,54 @@ from django.http import JsonResponse from django.shortcuts import render, get_object_or_404 +from django.db import models +from django.db.models import Q, Count, Case, When, IntegerField +from django.utils import timezone +from datetime import timedelta +from psycopg import logger from rest_framework import permissions, status, generics, filters from rest_framework.views import APIView from rest_framework.response import Response -from .models import * +from rest_framework.pagination import PageNumberPagination from .serializer import * +from .models import UserProfile , Game, Tournament +from .middleware import ServiceAuthentication , JWTAuth +from django.contrib.auth.models import AnonymousUser from django_filters.rest_framework import DjangoFilterBackend from django.views.decorators.csrf import csrf_exempt +import logging + +logger = logging.getLogger('pong_app') + +class IsAuthenticatedUserProfile(permissions.BasePermission): + """ + Permesso personalizzato per il modello UserProfile. + Verifica semplicemente se l'utente รจ autenticato (non รจ AnonymousUser). + """ + def has_permission(self, request, view): + return request.user is not None and not isinstance(request.user, AnonymousUser) + +class IsOwnUserProfile(permissions.BasePermission): + """ + Permesso che verifica se l'utente sta accedendo ai propri dati. + Da usare per le richieste che manipolano dati utente. + """ + def has_permission(self, request, view): + # Verifica prima se l'utente รจ autenticato + if not IsAuthenticatedUserProfile().has_permission(request, view): + return False + + # Per le viste che usano l'ID utente nell'URL + user_id = view.kwargs.get('user_id') + if user_id and str(request.user.user_id) == str(user_id): + return True + + # Per le richieste che usano l'ID utente nei parametri query + user_id_param = request.query_params.get('user_id') + if user_id_param and str(request.user.user_id) == str(user_id_param): + return True + + return False + class MultipleFieldLookupMixin: """ @@ -15,115 +57,608 @@ class MultipleFieldLookupMixin: """ def get_object(self): queryset = self.get_queryset() # Get the base queryset - queryset = self.filter_queryset(queryset) # Apply any filter backends + queryset = self.filter_queryset(queryset) # Apply any filter backends filter = {} for field in self.lookup_fields: if self.kwargs.get(field): # Ignore empty fields. filter[field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter) # Lookup the object + obj = get_object_or_404(queryset, **filter) # Lookup the object self.check_object_permissions(self.request, obj) return obj class PlayerGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) + permission_classes = [permissions.AllowAny] + authentication_classes = [ServiceAuthentication] serializer_class = PlayerSerializer lookup_fields = ['user_id', 'tournaments__id'] class PlayerManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] serializer_class = PlayerSerializer lookup_url_kwarg = 'user_id' queryset = UserProfile.objects.all() class GameGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] serializer_class = GamesSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['player_1__user_id', 'player_2__user_id', 'tournament_id'] lookup_fields = ['id', 'player_1__user_id', 'player_2__user_id', 'tournament_id'] queryset = Game.objects.all() - + class GameManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] serializer_class = GamesSerializer lookup_url_kwarg = 'id' queryset = Game.objects.all() +class CheckPendingGames(APIView): + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + def get(self, request, user_id): + try: + # Cerca giochi recenti che potrebbero essere pending (ultimi 10 minuti) + recent_games = Game.objects.filter( + models.Q(player_1__user_id=user_id) | models.Q(player_2__user_id=user_id), + begin_date__gte=timezone.now() - timedelta(minutes=10) + ).order_by('-begin_date') + + games_data = [] + for game in recent_games: + # Considera un gioco come "pending" se entrambi i punteggi sono 0 + is_pending = game.player_1_score == 0 and game.player_2_score == 0 + opponent_id = game.player_2.user_id if game.player_1.user_id == user_id else game.player_1.user_id + + games_data.append({ + 'id': game.id, + 'opponent_id': opponent_id, + 'player_1_id': game.player_1.user_id, + 'player_2_id': game.player_2.user_id, + 'player_1_score': game.player_1_score, + 'player_2_score': game.player_2_score, + 'begin_date': game.begin_date, + 'is_pending': is_pending + }) + + # Trova il gioco piรน recente che รจ ancora pending + active_game = None + for game_data in games_data: + if game_data['is_pending']: + active_game = game_data + break + + return Response({ + 'active_game': active_game, + 'recent_games': games_data + }) + + except Exception as e: + logging.error(f"Error checking pending games: {str(e)}") + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + class TournamentGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) + """ + Tournament management endpoint. + + GET: List all tournaments with optional filtering + POST: Create a new tournament + + When creating a tournament: + - The authenticated user is automatically set as the creator + - The creator is automatically joined to the tournament + - max_partecipants is automatically adjusted to the nearest power of 2 (4, 8, 16, 32, 64) + - partecipants count starts at 1 (the creator) + + Example request body for creation: + { + "name": "Summer Championship", + "max_partecipants": 12, // Will be adjusted to 16 + } + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] serializer_class = TournamentSerializer filter_backends = [DjangoFilterBackend] - filterset_fields = ['name', 'partecipants__user_id', 'level_required', 'max_partecipants', 'winner__user_id'] - lookup_fields = ['id', 'name', 'partecipants__user_id', 'level_required', 'max_partecipants', 'winner__user_id'] + filterset_fields = ['name', 'partecipants__user_id', 'max_partecipants', 'winner__user_id'] + lookup_fields = ['id', 'name', 'partecipants__user_id', 'max_partecipants', 'winner__user_id'] + + def get_queryset(self): + """Override to include participants for efficient loading""" + return Tournament.objects.select_related('creator', 'winner').prefetch_related('player').order_by('-begin_date') + + def get_nearest_power_of_2(self, num): + """Get the nearest power of 2 for tournament partecipants""" + if num <= 0: + return 4 # Minimum tournament size + + # Find the nearest power of 2 + import math + if num <= 4: + return 4 + elif num <= 8: + return 8 + elif num <= 16: + return 16 + elif num <= 32: + return 32 + elif num <= 64: + return 64 + elif num <= 128: + return 128 + else: + return 128 # Maximum tournament size + + def perform_create(self, serializer): + """Override to auto-set creator and adjust max_partecipants to nearest power of 2""" + # Get or create the user profile for the creator + creator, created = UserProfile.objects.get_or_create( + user_id=self.request.user.user_id, + defaults={ + 'username': getattr(self.request.user, 'username', f'User_{self.request.user.user_id}'), + 'email': getattr(self.request.user, 'email', f'user_{self.request.user.user_id}@example.com') + } + ) + + # Get the requested max_partecipants and adjust to nearest power of 2 + max_partecipants = serializer.validated_data.get('max_partecipants', 8) + adjusted_max_partecipants = self.get_nearest_power_of_2(max_partecipants) + + # Get initial partecipants from request data (if any) + initial_partecipants = self.request.data.get('partecipants', []) + logger.info(f"Creating tournament with initial partecipants: {initial_partecipants}") + # Calculate the number of partecipants (creator + initial partecipants) + participant_count = 1 # Start with creator + if initial_partecipants: + participant_count += len(initial_partecipants) + + # Save the tournament with auto-set creator and adjusted max partecipants + tournament = serializer.save( + creator=creator, + max_partecipants=adjusted_max_partecipants, + partecipants=participant_count + ) + + # Add the creator to the tournament partecipants + creator.tournaments.add(tournament) + try: + from .notification import SendNotificationSync, ImmediateNotification + notification_data = { + 'type': 'tournament_created', + 'tournament_id': tournament.id, + 'name': tournament.name, + 'max_players': tournament.max_partecipants, + 'creator_id': tournament.creator.user_id, + 'begin_date': tournament.begin_date.isoformat() if tournament.begin_date else None, + } + SendNotificationSync( + ImmediateNotification( + Sender='Pong', + message=notification_data, + user_id=creator.user_id + ) + ) + logger.info(f'โœ… Tournament notification sent to creator (ID: {creator.user_id}) for tournament {tournament.id}') + except Exception as e: + logger.error(f'โŒ Failed to send tournament notification to creator (ID: {creator.user_id}): {str(e)}') + # Add initial partecipants if provided + if initial_partecipants: + from .notification import SendNotificationSync, ImmediateNotification + notification_data = { + 'type': 'tournament_created', + 'tournament_id': tournament.id, + 'name': tournament.name, + 'max_players': tournament.max_partecipants, + 'creator_id': tournament.creator.user_id, + 'begin_date': tournament.begin_date.isoformat() if tournament.begin_date else None, + } + for user_id in initial_partecipants: + try: + participant = UserProfile.objects.get(user_id=int(user_id)) + # Avoid adding duplicates (in case creator is in the list) + if not participant.tournaments.filter(id=tournament.id).exists(): + participant.tournaments.add(tournament) + try: + notification = ImmediateNotification( + Sender='Pong', + message=notification_data, + user_id=participant.user_id + ) + SendNotificationSync(notification) + logger.info(f'โœ… Tournament notification sent to creator (ID: {tournament.creator.user_id}) for tournament {tournament.id}') + except Exception as e: + logger.error(f'โŒ Failed to send tournament notification to creator (ID: {tournament.creator.user_id}): {str(e)}') + except UserProfile.DoesNotExist: + # Log the error but don't fail the entire creation + logging.warning(f"User with ID {user_id} not found, skipping from tournament {tournament.id}") class TournamentManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticatedUserProfile,) serializer_class = TournamentSerializer + authentication_classes = [JWTAuth] lookup_url_kwarg = 'id' - queryset = Tournament.objects.all() + lookup_fields = ['id'] + + def get_queryset(self): + """Override to include participants for efficient loading""" + return Tournament.objects.select_related('creator', 'winner').prefetch_related('player') + + + class JoinTournament(APIView): """ Use this endpoint to join a tournament. - + Args: tournament_id (int): The id of the tournament. user_id (int): The id of the player. """ permission_classes = (permissions.AllowAny,) - def post(self, request, *args, **kwargs): tournament_id = request.data.get('tournament_id') user_id = request.data.get('user_id') if not tournament_id or not user_id: return Response({'error': 'tournament_id and user_id are required'}, status=status.HTTP_400_BAD_REQUEST) + tournament = get_object_or_404(Tournament, id=tournament_id) player = get_object_or_404(UserProfile, user_id=user_id) + + # Check if tournament is full if tournament.partecipants >= tournament.max_partecipants: return Response({'error': 'tournament is full'}, status=status.HTTP_400_BAD_REQUEST) - if tournament.winner != 0: + + # Check if tournament has already started or is completed + if tournament.status == 'active': + return Response({'error': 'tournament has already started'}, status=status.HTTP_400_BAD_REQUEST) + + if tournament.status == 'completed': return Response({'error': 'tournament is already finished'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if player is already in the tournament + if player.tournaments.filter(id=tournament_id).exists(): + return Response({'error': 'user is already in this tournament'}, status=status.HTTP_400_BAD_REQUEST) + + # Add player to tournament player.tournaments.add(tournament) tournament.partecipants += 1 tournament.save() - return Response({'message': 'user joined tournament'}, status=status.HTTP_200_OK) + + return Response({ + 'message': 'user joined tournament', + 'tournament_id': tournament_id, + 'current_partecipants': tournament.partecipants, + 'max_partecipants': tournament.max_partecipants + }, status=status.HTTP_200_OK) + +class LeaveTournament(APIView): + """ Use this endpoint to leave a tournament (only before it starts). + + Args: + tournament_id (int): The id of the tournament. + user_id (int): The id of the player. + """ + permission_classes = (permissions.AllowAny,) + def post(self, request, *args, **kwargs): + tournament_id = request.data.get('tournament_id') + user_id = request.data.get('user_id') + if not tournament_id or not user_id: + return Response({'error': 'tournament_id and user_id are required'}, status=status.HTTP_400_BAD_REQUEST) + + tournament = get_object_or_404(Tournament, id=tournament_id) + player = get_object_or_404(UserProfile, user_id=user_id) + + # Check if tournament has already started or completed + if tournament.status == 'active': + return Response({'error': 'tournament has already started, cannot leave'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if tournament is already completed + if tournament.status == 'completed': + return Response({'error': 'tournament has already finished'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if player is actually in the tournament + if not player.tournaments.filter(id=tournament_id).exists(): + return Response({'error': 'user is not in this tournament'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if user is the creator + if tournament.creator and tournament.creator.user_id == user_id: + return Response({'error': 'tournament creator cannot leave the tournament'}, status=status.HTTP_400_BAD_REQUEST) + + # Remove player from tournament + player.tournaments.remove(tournament) + tournament.partecipants -= 1 + tournament.save() + + return Response({ + 'message': 'user left tournament', + 'tournament_id': tournament_id, + 'current_partecipants': tournament.partecipants, + 'max_partecipants': tournament.max_partecipants + }, status=status.HTTP_200_OK) class EndTournament(APIView): """ Use this endpoint to end a tournament. - + Args: tournament_id (int): The id of the tournament. winner_id (int): The id of the player who won the tournament. """ permission_classes = (permissions.AllowAny,) - def post(self, request, *args, **kwargs): tournament_id = request.data.get('tournament_id') winner_id = request.data.get('winner_id') if not tournament_id or not winner_id: return Response({'error': 'tournament_id and winner_id are required'}, status=status.HTTP_400_BAD_REQUEST) + tournament = get_object_or_404(Tournament, id=tournament_id) winner = get_object_or_404(UserProfile, user_id=winner_id) - if tournament.winner != 0: + + # Check if tournament is already completed + if tournament.status == 'completed': return Response({'error': 'tournament is already finished'}, status=status.HTTP_400_BAD_REQUEST) - tournament.winner = winner.user_id + + # Check if winner is actually a participant in the tournament + if not winner.tournaments.filter(id=tournament_id).exists(): + return Response({'error': 'winner must be a participant in the tournament'}, status=status.HTTP_400_BAD_REQUEST) + + # Set the winner and mark tournament as completed + tournament.winner = winner + tournament.status = 'completed' tournament.save() - return Response({'message': 'tournament ended'}, status=status.HTTP_200_OK) + + return Response({ + 'message': 'tournament ended', + 'tournament_id': tournament_id, + 'winner': { + 'user_id': winner.user_id, + 'username': winner.username + } + }, status=status.HTTP_200_OK) -class PlayerMatchHistory(APIView): - """ Use this endpoint to get the match history of a player.' - Args: - user_id (int): The id of the player. + + +class GamePagination(PageNumberPagination): + """Simple pagination for game history""" + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 50 + +class PlayerMatchHistory(generics.ListAPIView): + """ Use this endpoint to get the match history of a player. + + URL: /api/player-match-history/?user_id=123 + Optional params: ?page=2&page_size=20 + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + serializer_class = GameHistorySerializer + pagination_class = GamePagination + + def get_queryset(self): + user_id = self.request.query_params.get('user_id') + if not user_id: + return Game.objects.none() + + # Get games where user is either player_1 or player_2 + # Order by most recent first + return Game.objects.filter( + models.Q(player_1__user_id=user_id) | models.Q(player_2__user_id=user_id) + ).select_related('player_1', 'player_2', 'tournament_id').order_by('-begin_date') + + +class TournamentMatchHistory(APIView): + """ Use this endpoint to get the match history of a tournament. + + URL: /api/tournament-match-history/?tournament_id=123 """ - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + serializer_class = TournamentSerializer + pagination_class = GamePagination + + def get_queryset(self): + tournament_id = self.request.query_params.get('tournament_id') + if not tournament_id: + return Game.objects.none() + + # Get games for the specified tournament + return Game.objects.filter(tournament_id=tournament_id).select_related('player_1', 'player_2').order_by('-begin_date') + +class UserTournaments(generics.ListAPIView): + """ + Get tournaments for a specific user. + + Query Parameters: + - user_id: User ID to get tournaments for (optional, defaults to authenticated user) + - current_only: Set to 'true' to get only active/pending tournaments (optional) + - status: Filter by tournament status - 'pending', 'active', 'finished' (optional) + + Examples: + - GET /api/user-tournaments/ - Get all tournaments for authenticated user + - GET /api/user-tournaments/?user_id=123 - Get all tournaments for user 123 + - GET /api/user-tournaments/?current_only=true - Get only current tournaments + - GET /api/user-tournaments/?status=active - Get only active tournaments + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + serializer_class = TournamentSerializer + pagination_class = GamePagination + + def get_queryset(self): + # Get user_id from query params or use authenticated user + user_id = self.request.query_params.get('user_id', self.request.user.user_id) + current_only = self.request.query_params.get('current_only', '').lower() == 'true' + status_filter = self.request.query_params.get('status', '').lower() + + try: + # Get tournaments where user is a participant + queryset = Tournament.objects.filter( + player__user_id=user_id + ).select_related('creator', 'winner').prefetch_related('player').order_by('-begin_date') + + # Apply filters + if current_only: + # Get only tournaments that are not completed + queryset = queryset.exclude(status='completed') + + if status_filter in ['pending', 'active', 'completed']: + queryset = queryset.filter(status=status_filter) + + return queryset + + except Exception as e: + logger.error(f"Error getting user tournaments: {str(e)}") + return Tournament.objects.none() + + def list(self, request, *args, **kwargs): + """Override to add extra metadata""" + queryset = self.get_queryset() + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + response_data = self.get_paginated_response(serializer.data) + + # Add summary statistics using status choices + total_tournaments = queryset.count() + pending_count = queryset.filter(status='pending').count() + active_count = queryset.filter(status='active').count() + completed_count = queryset.filter(status='completed').count() + + response_data.data['summary'] = { + 'total_tournaments': total_tournaments, + 'pending': pending_count, + 'active': active_count, + 'completed': completed_count + } + + return response_data + + serializer = self.get_serializer(queryset, many=True) + return Response({ + 'results': serializer.data, + 'summary': { + 'total_tournaments': queryset.count(), + 'pending': queryset.filter(status='pending').count(), + 'active': queryset.filter(status='active').count(), + 'completed': queryset.filter(status='completed').count() + } + }) + +class StartTournament(APIView): + """ + Start a tournament (only creator can start it). + + POST /api/start-tournament/ + Body: {"tournament_id": 123} + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + + def post(self, request, *args, **kwargs): + tournament_id = request.data.get('tournament_id') + if not tournament_id: + return Response({'error': 'tournament_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + tournament = get_object_or_404(Tournament, id=tournament_id) + + # Check if user is the creator + if not tournament.creator or tournament.creator.user_id != request.user.user_id: + return Response({'error': 'Only tournament creator can start the tournament'}, status=status.HTTP_403_FORBIDDEN) + + # Check if tournament is already started + if tournament.status == 'active': + return Response({'error': 'Tournament has already started'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if tournament is already finished + if tournament.status == 'completed': + return Response({'error': 'Tournament is already completed'}, status=status.HTTP_400_BAD_REQUEST) + + # Check if tournament has enough participants (minimum 2) + if tournament.partecipants < 2: + return Response({'error': 'Tournament needs at least 2 participants to start'}, status=status.HTTP_400_BAD_REQUEST) + + # Start the tournament by changing status to active + tournament.status = 'active' + tournament.save() + + # Serialize and return updated tournament + serializer = TournamentSerializer(tournament) + + return Response({ + 'message': 'Tournament started successfully', + 'tournament': serializer.data + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"Error starting tournament: {str(e)}") + return Response({'error': 'An error occurred while starting the tournament'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class UserStatistics(APIView): + """ Use this endpoint to get the statistics of a user. + + args: + user_id (int): The id of the user. (optional, if not provided, the authenticated user will be used) + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id') + user_id = request.query_params.get('user_id', request.user.user_id) + if not user_id: return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) - games = Game.objects.filter(player_1=user_id) | Game.objects.filter(player_2=user_id) - serializer = GamesSerializer(games, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + # elif not isinstance(user_id, int): + # return Response({'error': 'user_id must be an integer'}, status=status.HTTP_400_BAD_REQUEST) + try: + user = get_object_or_404(UserProfile, user_id=int(user_id)) + games_stats = Game.objects.filter( + Q(player_1__user_id=user_id) | Q(player_2__user_id=user_id) + ).aggregate( + total_games=Count('id'), + total_wins=Count(Case( + When(Q(player_1__user_id=user_id) & Q(player_1_score__gt=models.F('player_2_score')), then=1), + When(Q(player_2__user_id=user_id) & Q(player_2_score__gt=models.F('player_1_score')), then=1), + output_field=IntegerField() + )), + total_losses=Count(Case( + When(Q(player_1__user_id=user_id) & Q(player_1_score__lt=models.F('player_2_score')), then=1), + When(Q(player_2__user_id=user_id) & Q(player_2_score__lt=models.F('player_1_score')), then=1), + output_field=IntegerField() + )) + ) + + # Tournament statistics + tournament_stats = user.tournaments.aggregate( + total_tournaments=Count('id'), + total_tournament_wins=Count('id', filter=Q(winner__user_id=user_id)) + ) + + # Calculate win rate + total_games = games_stats['total_games'] or 0 + total_wins = games_stats['total_wins'] or 0 + win_rate = (total_wins / total_games * 100) if total_games > 0 else 0 + + stats_data = { + 'user_id': user.user_id, + 'username': user.username, + 'total_games': total_games, + 'total_wins': total_wins, + 'total_losses': games_stats['total_losses'] or 0, + 'win_rate': round(win_rate, 2), + 'total_tournaments': tournament_stats['total_tournaments'] or 0, + 'total_tournament_wins': tournament_stats['total_tournament_wins'] or 0 + } + + return Response(stats_data, status=status.HTTP_200_OK) + except Exception as e: + logging.error(f"An error occurred: {str(e)}") + return Response({'error': 'An internal error occurred.'}, status=status.HTTP_400_BAD_REQUEST) + @csrf_exempt def health_check(request): diff --git a/Back-End/pongProject/requirements.txt b/Back-End/pongProject/requirements.txt index eb930f9c..0b925bc3 100644 --- a/Back-End/pongProject/requirements.txt +++ b/Back-End/pongProject/requirements.txt @@ -8,33 +8,35 @@ cffi==1.17.1 channels-redis==4.2.1 charset-normalizer==3.4.1 constantly==23.10.4 -cryptography==44.0.1 +cryptography==44.0.2 daphne==4.1.2 -Django==5.1.6 +django==5.2.4 django-cors-headers==4.7.0 django-filter==25.1 django-redis==5.4.0 -djangorestframework==3.15.2 -djangorestframework_simplejwt==5.4.0 +djangorestframework>=3.16.0 +drf-yasg==1.21.7 +# djangorestframework_simplejwt==5.4.0 # Removed to avoid cross-service dependencies hyperlink==21.0.0 idna==3.10 incremental==24.7.2 oauthlib==3.2.2 -psycopg==3.2.5 -psycopg-binary==3.2.5 +psycopg[binary]==3.2.9 pyasn1==0.6.1 pyasn1_modules==0.4.1 pycparser==2.22 PyJWT==2.10.1 pyOpenSSL==25.0.0 redis==5.2.1 -requests==2.32.3 +requests>=2.32.4 requests-oauthlib==2.0.0 service-identity==24.2.0 six==1.17.0 sqlparse==0.5.3 -Twisted==24.11.0 +twisted[tls]==24.11.0 txaio==23.1.1 typing_extensions==4.12.2 -urllib3==2.3.0 -zope.interface==7.2 +urllib3>=2.5.0 +zope-interface==7.2 +django-prometheus==2.3.1 +prometheus-client==0.21.1 diff --git a/Back-End/requirements.in b/Back-End/requirements.in index 730ee9bb..5c31013e 100644 --- a/Back-End/requirements.in +++ b/Back-End/requirements.in @@ -1,94 +1,106 @@ -aiohappyeyeballs==2.4.3 -aiohttp==3.11.8 -aiosignal==1.3.1 -amqp==5.2.0 +aiohappyeyeballs==2.5.0 +aiohttp>=3.12.14 +aiosignal>=1.4.0 +amqp==5.3.1 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 asgiref==3.8.1 -async-timeout==4.0.3 -attrs==24.2.0 +async-timeout==5.0.1 +attrs==25.1.0 autobahn==24.4.2 Automat==24.8.1 billiard==4.2.1 build==1.2.2.post1 celery==5.4.0 -certifi==2024.8.30 +certifi==2025.1.31 cffi==1.17.1 -channels==4.1.0 -channels-redis==4.2.0 -charset-normalizer==3.3.2 -click==8.1.7 +channels==4.2.0 +channels_redis==4.2.1 +charset-normalizer==3.4.1 +click==8.1.8 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 constantly==23.10.4 coreapi==2.3.3 coreschema==0.0.4 -cryptography==43.0.1 +cryptography==44.0.2 daphne==4.1.2 -Django==5.1.3 -django-cors-headers==4.4.0 -django-filter==24.3 +Django==5.1.6 +django-cors-headers==4.7.0 +django-encrypted-model-fields==0.6.5 +django-fernet-fields==0.6 +django-filter==25.1 django-loki==0.1.4 +django-minio-storage==0.5.7 django-oauth-toolkit==3.0.1 django-prometheus==2.3.1 +django-ratelimit==4.1.0 django-redis==5.4.0 django-rest-swagger==2.2.0 -djangorestframework==3.15.2 -djangorestframework_simplejwt==5.4.0 -drf-yasg==1.21.8 +djangorestframework>=3.16.0 +djangorestframework_simplejwt==5.5.0 +drf-yasg==1.21.9 frozenlist==1.5.0 grafana==0.0.1 +gunicorn==23.0.0 hyperlink==21.0.0 idna==3.10 incremental==24.7.2 inflection==0.5.1 itypes==1.2.0 -Jinja2==3.1.5 +Jinja2==3.1.6 joblib==1.4.2 jwcrypto==1.5.6 kombu==5.4.2 MarkupSafe==3.0.2 +minio==7.2.15 msgpack==1.1.0 multidict==6.1.0 -numpy==2.1.2 +numpy==2.2.3 oauthlib==3.2.2 openapi-codec==1.3.2 packaging==24.2 -pillow==11.0.0 -pip==23.0.1 +pillow==11.1.0 +pip-review==1.3.0 pip-tools==7.4.1 -prometheus_client==0.21.0 -prompt_toolkit==3.0.48 -propcache==0.2.0 -psycopg==3.2.3 +prometheus_client==0.21.1 +prompt_toolkit==3.0.50 +propcache==0.3.0 +psycopg==3.2.9 +psycopg2-binary==2.9.10 pyasn1==0.6.1 pyasn1_modules==0.4.1 pycparser==2.22 -PyJWT==2.10.1 -pyOpenSSL==24.2.1 +pycryptodome==3.21.0 +PyJWT==2.9.0 +pyOpenSSL==25.0.0 +pyotp==2.9.0 pyproject_hooks==1.2.0 python-dateutil==2.9.0.post0 -pytz==2024.2 +pytz==2025.1 PyYAML==6.0.2 -redis==5.1.1 -requests==2.32.3 -scikit-learn==1.5.2 -scipy==1.14.1 -service-identity==24.1.0 -setuptools==75.2.0 +qrcode==8.2 +redis==5.2.1 +requests>=2.32.4 +scikit-learn==1.6.1 +scipy==1.15.2 +service-identity==24.2.0 +setuptools==80.7.1 simplejson==3.20.1 -six==1.16.0 -sqlparse==0.5.1 +six==1.17.0 +sqlparse==0.5.3 style==1.1.0 threadpoolctl==3.5.0 -Twisted==24.7.0 +Twisted==24.11.0 txaio==23.1.1 typing_extensions==4.12.2 -tzdata==2024.2 +tzdata==2025.1 update==0.0.1 uritemplate==4.1.1 -urllib3==2.2.3 +urllib3>=2.5.0 vine==5.1.0 wcwidth==0.2.13 wheel==0.45.1 -yarl==1.18.0 -zope.interface==7.1.0 +yarl==1.18.3 +zope.interface==7.2 diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index a51123e6..00941c9f 100644 --- a/Back-End/requirements.txt +++ b/Back-End/requirements.txt @@ -2,79 +2,89 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile Back-End/requirements.in +# pip-compile requirements.in # -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.5.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp -aiohttp==3.11.8 - # via -r Back-End/requirements.in -aiosignal==1.3.1 +aiohttp>=3.12.14 + # via -r requirements.in +aiosignal>=1.4.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp -amqp==5.2.0 +amqp==5.3.1 # via - # -r Back-End/requirements.in + # -r requirements.in # kombu +argon2-cffi==23.1.0 + # via + # -r requirements.in + # minio +argon2-cffi-bindings==21.2.0 + # via + # -r requirements.in + # argon2-cffi asgiref==3.8.1 # via - # -r Back-End/requirements.in + # -r requirements.in # channels # channels-redis # daphne # django # django-cors-headers -async-timeout==4.0.3 +async-timeout==5.0.1 # via - # -r Back-End/requirements.in + # -r requirements.in # redis -attrs==24.2.0 +attrs==25.1.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp # service-identity # twisted autobahn==24.4.2 # via - # -r Back-End/requirements.in + # -r requirements.in # daphne automat==24.8.1 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted billiard==4.2.1 # via - # -r Back-End/requirements.in + # -r requirements.in # celery build==1.2.2.post1 # via - # -r Back-End/requirements.in + # -r requirements.in # pip-tools celery==5.4.0 - # via -r Back-End/requirements.in -certifi==2024.8.30 + # via -r requirements.in +certifi==2025.1.31 # via - # -r Back-End/requirements.in + # -r requirements.in + # minio # requests cffi==1.17.1 # via - # -r Back-End/requirements.in + # -r requirements.in + # argon2-cffi-bindings # cryptography -channels==4.1.0 +channels==4.2.0 # via - # -r Back-End/requirements.in + # -r requirements.in # channels-redis -channels-redis==4.2.0 - # via -r Back-End/requirements.in -charset-normalizer==3.3.2 +channels-redis==4.2.1 + # via -r requirements.in +charset-normalizer==3.4.1 # via - # -r Back-End/requirements.in + # -r requirements.in # requests -click==8.1.7 +click==8.1.8 # via - # -r Back-End/requirements.in + # -r requirements.in # celery # click-didyoumean # click-plugins @@ -82,302 +92,337 @@ click==8.1.7 # pip-tools click-didyoumean==0.3.1 # via - # -r Back-End/requirements.in + # -r requirements.in # celery click-plugins==1.1.1 # via - # -r Back-End/requirements.in + # -r requirements.in # celery click-repl==0.3.0 # via - # -r Back-End/requirements.in + # -r requirements.in # celery constantly==23.10.4 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted coreapi==2.3.3 # via - # -r Back-End/requirements.in + # -r requirements.in # django-rest-swagger # openapi-codec coreschema==0.0.4 # via - # -r Back-End/requirements.in + # -r requirements.in # coreapi -cryptography==43.0.1 +cryptography==44.0.2 # via - # -r Back-End/requirements.in + # -r requirements.in # autobahn + # django-encrypted-model-fields + # django-fernet-fields # jwcrypto # pyopenssl # service-identity daphne==4.1.2 - # via -r Back-End/requirements.in -django==5.1.3 + # via -r requirements.in +django==5.2.4 # via - # -r Back-End/requirements.in + # -r requirements.in # channels # django-cors-headers + # django-encrypted-model-fields + # django-fernet-fields # django-filter + # django-minio-storage # django-oauth-toolkit # django-redis # djangorestframework # djangorestframework-simplejwt # drf-yasg -django-cors-headers==4.4.0 - # via -r Back-End/requirements.in -django-filter==24.3 - # via -r Back-End/requirements.in +django-cors-headers==4.7.0 + # via -r requirements.in +django-encrypted-model-fields==0.6.5 + # via -r requirements.in +django-fernet-fields==0.6 + # via -r requirements.in +django-filter==25.1 + # via -r requirements.in django-loki==0.1.4 - # via -r Back-End/requirements.in + # via -r requirements.in +django-minio-storage==0.5.7 + # via -r requirements.in django-oauth-toolkit==3.0.1 - # via -r Back-End/requirements.in + # via -r requirements.in django-prometheus==2.3.1 - # via -r Back-End/requirements.in + # via -r requirements.in +django-ratelimit==4.1.0 + # via -r requirements.in django-redis==5.4.0 - # via -r Back-End/requirements.in + # via -r requirements.in django-rest-swagger==2.2.0 - # via -r Back-End/requirements.in -djangorestframework==3.15.2 + # via -r requirements.in +djangorestframework>=3.16.0 # via - # -r Back-End/requirements.in + # -r requirements.in # django-rest-swagger # djangorestframework-simplejwt # drf-yasg -djangorestframework-simplejwt==5.4.0 - # via -r Back-End/requirements.in -drf-yasg==1.21.8 - # via -r Back-End/requirements.in +djangorestframework-simplejwt==5.5.0 + # via -r requirements.in +drf-yasg==1.21.9 + # via -r requirements.in frozenlist==1.5.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp # aiosignal grafana==0.0.1 - # via -r Back-End/requirements.in + # via -r requirements.in +gunicorn==23.0.0 + # via -r requirements.in hyperlink==21.0.0 # via - # -r Back-End/requirements.in + # -r requirements.in # autobahn # twisted idna==3.10 # via - # -r Back-End/requirements.in + # -r requirements.in # hyperlink # requests # twisted # yarl incremental==24.7.2 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted inflection==0.5.1 # via - # -r Back-End/requirements.in + # -r requirements.in # drf-yasg itypes==1.2.0 # via - # -r Back-End/requirements.in + # -r requirements.in # coreapi -jinja2==3.1.5 +jinja2==3.1.6 # via - # -r Back-End/requirements.in + # -r requirements.in # coreschema joblib==1.4.2 # via - # -r Back-End/requirements.in + # -r requirements.in # scikit-learn jwcrypto==1.5.6 # via - # -r Back-End/requirements.in + # -r requirements.in # django-oauth-toolkit kombu==5.4.2 # via - # -r Back-End/requirements.in + # -r requirements.in # celery markupsafe==3.0.2 # via - # -r Back-End/requirements.in + # -r requirements.in # jinja2 +minio==7.2.15 + # via + # -r requirements.in + # django-minio-storage msgpack==1.1.0 # via - # -r Back-End/requirements.in + # -r requirements.in # channels-redis multidict==6.1.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp # yarl -numpy==2.1.2 +numpy==2.2.3 # via - # -r Back-End/requirements.in + # -r requirements.in # scikit-learn # scipy oauthlib==3.2.2 # via - # -r Back-End/requirements.in + # -r requirements.in # django-oauth-toolkit openapi-codec==1.3.2 # via - # -r Back-End/requirements.in + # -r requirements.in # django-rest-swagger packaging==24.2 # via - # -r Back-End/requirements.in + # -r requirements.in # build # drf-yasg -pillow==11.0.0 - # via -r Back-End/requirements.in + # gunicorn + # pip-review +pillow==11.1.0 + # via -r requirements.in +pip-review==1.3.0 + # via -r requirements.in pip-tools==7.4.1 - # via -r Back-End/requirements.in -prometheus-client==0.21.0 + # via -r requirements.in +prometheus-client==0.21.1 # via - # -r Back-End/requirements.in + # -r requirements.in # django-prometheus -prompt-toolkit==3.0.48 +prompt-toolkit==3.0.50 # via - # -r Back-End/requirements.in + # -r requirements.in # click-repl -propcache==0.2.0 +propcache==0.3.0 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp # yarl -psycopg==3.2.5 -psycopg-binary==3.2.5 - # via -r Back-End/requirements.in +psycopg==3.2.9 + # via -r requirements.in +psycopg2-binary==2.9.10 + # via -r requirements.in pyasn1==0.6.1 # via - # -r Back-End/requirements.in + # -r requirements.in # pyasn1-modules # service-identity pyasn1-modules==0.4.1 # via - # -r Back-End/requirements.in + # -r requirements.in # service-identity pycparser==2.22 # via - # -r Back-End/requirements.in + # -r requirements.in # cffi -pyjwt==2.10.1 +pycryptodome==3.21.0 # via - # -r Back-End/requirements.in + # -r requirements.in + # minio +pyjwt==2.9.0 + # via + # -r requirements.in # djangorestframework-simplejwt -pyopenssl==24.2.1 +pyopenssl==25.0.0 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted +pyotp==2.9.0 + # via -r requirements.in pyproject-hooks==1.2.0 # via - # -r Back-End/requirements.in + # -r requirements.in # build # pip-tools python-dateutil==2.9.0.post0 # via - # -r Back-End/requirements.in + # -r requirements.in # celery -pytz==2024.2 +pytz==2025.1 # via - # -r Back-End/requirements.in + # -r requirements.in # django-loki # drf-yasg pyyaml==6.0.2 # via - # -r Back-End/requirements.in + # -r requirements.in # drf-yasg -redis==5.1.1 +qrcode==8.2 + # via -r requirements.in +redis==5.2.1 # via - # -r Back-End/requirements.in + # -r requirements.in # channels-redis # django-redis -requests==2.32.3 +requests>=2.32.4 # via - # -r Back-End/requirements.in + # -r requirements.in # coreapi # django-loki # django-oauth-toolkit -scikit-learn==1.5.2 - # via -r Back-End/requirements.in -scipy==1.14.1 +scikit-learn==1.6.1 + # via -r requirements.in +scipy==1.15.2 # via - # -r Back-End/requirements.in + # -r requirements.in # scikit-learn -service-identity==24.1.0 +service-identity==24.2.0 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted simplejson==3.20.1 # via - # -r Back-End/requirements.in + # -r requirements.in # django-rest-swagger -six==1.16.0 +six==1.17.0 # via - # -r Back-End/requirements.in + # -r requirements.in # python-dateutil -sqlparse==0.5.1 +sqlparse==0.5.3 # via - # -r Back-End/requirements.in + # -r requirements.in # django style==1.1.0 # via - # -r Back-End/requirements.in + # -r requirements.in # update threadpoolctl==3.5.0 # via - # -r Back-End/requirements.in + # -r requirements.in # scikit-learn -twisted[tls]==24.7.0 +twisted[tls]==24.11.0 # via - # -r Back-End/requirements.in + # -r requirements.in # daphne txaio==23.1.1 # via - # -r Back-End/requirements.in + # -r requirements.in # autobahn typing-extensions==4.12.2 # via - # -r Back-End/requirements.in + # -r requirements.in # jwcrypto + # minio # psycopg + # pyopenssl # twisted -tzdata==2024.2 +tzdata==2025.1 # via - # -r Back-End/requirements.in + # -r requirements.in # celery # kombu update==0.0.1 - # via -r Back-End/requirements.in + # via -r requirements.in uritemplate==4.1.1 # via - # -r Back-End/requirements.in + # -r requirements.in # coreapi # drf-yasg -urllib3==2.2.3 +urllib3>=2.5.0 # via - # -r Back-End/requirements.in + # -r requirements.in + # minio # requests vine==5.1.0 # via - # -r Back-End/requirements.in + # -r requirements.in # amqp # celery # kombu wcwidth==0.2.13 # via - # -r Back-End/requirements.in + # -r requirements.in # prompt-toolkit wheel==0.45.1 # via - # -r Back-End/requirements.in + # -r requirements.in # pip-tools -yarl==1.18.0 +yarl==1.18.3 # via - # -r Back-End/requirements.in + # -r requirements.in # aiohttp -zope-interface==7.1.0 +zope-interface==7.2 # via - # -r Back-End/requirements.in + # -r requirements.in # twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/Back-End/Dockers/notification_service/Notifications/Notifications/__init__.py b/Back-End/shared_logging.py similarity index 100% rename from Back-End/Dockers/notification_service/Notifications/Notifications/__init__.py rename to Back-End/shared_logging.py diff --git a/Back-End/task_user/Dockerfile b/Back-End/task_user/Dockerfile index e6a2350b..4841a6cb 100644 --- a/Back-End/task_user/Dockerfile +++ b/Back-End/task_user/Dockerfile @@ -1,5 +1,5 @@ # Use a stable Python version -FROM python:3.11-alpine AS base +FROM python:3.13-alpine AS base RUN apk add --no-cache postgresql-libs bash @@ -16,6 +16,9 @@ COPY --from=builder /usr/local /usr/local COPY . /app WORKDIR /app +RUN mkdir -p /app/staticfiles +RUN mkdir -p /app/static + COPY ./entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/Back-End/task_user/allrequirements.txt b/Back-End/task_user/allrequirements.txt index a51123e6..9dcce772 100644 --- a/Back-End/task_user/allrequirements.txt +++ b/Back-End/task_user/allrequirements.txt @@ -8,9 +8,9 @@ aiohappyeyeballs==2.4.3 # via # -r Back-End/requirements.in # aiohttp -aiohttp==3.11.8 +aiohttp>=3.12.14 # via -r Back-End/requirements.in -aiosignal==1.3.1 +aiosignal>=1.4.0 # via # -r Back-End/requirements.in # aiohttp @@ -139,7 +139,7 @@ django-redis==5.4.0 # via -r Back-End/requirements.in django-rest-swagger==2.2.0 # via -r Back-End/requirements.in -djangorestframework==3.15.2 +djangorestframework>=3.16.0 # via # -r Back-End/requirements.in # django-rest-swagger @@ -244,8 +244,8 @@ propcache==0.2.0 # -r Back-End/requirements.in # aiohttp # yarl -psycopg==3.2.5 -psycopg-binary==3.2.5 +psycopg==3.2.9 +psycopg-binary==3.2.9 # via -r Back-End/requirements.in pyasn1==0.6.1 # via @@ -291,7 +291,7 @@ redis==5.1.1 # -r Back-End/requirements.in # channels-redis # django-redis -requests==2.32.3 +requests>=2.32.4 # via # -r Back-End/requirements.in # coreapi @@ -353,7 +353,7 @@ uritemplate==4.1.1 # -r Back-End/requirements.in # coreapi # drf-yasg -urllib3==2.2.3 +urllib3>=2.5.0 # via # -r Back-End/requirements.in # requests diff --git a/Back-End/task_user/db.sqlite3 b/Back-End/task_user/db.sqlite3 deleted file mode 100644 index 903b38ee..00000000 Binary files a/Back-End/task_user/db.sqlite3 and /dev/null differ diff --git a/Back-End/task_user/entrypoint.sh b/Back-End/task_user/entrypoint.sh index 03657501..d4f0172c 100644 --- a/Back-End/task_user/entrypoint.sh +++ b/Back-End/task_user/entrypoint.sh @@ -2,14 +2,21 @@ set -e +# collect static files +python manage.py collectstatic --noinput + # migrations at startup -python manage.py makemigrations user_app task_app -python manage.py migrate +python manage.py makemigrations user_app --noinput +python manage.py migrate --noinput # For development if [ "$DEBUG" = "True" ]; then python manage.py runserver 0.0.0.0:8000 else # For production use Gunicorn (WSGI server) - gunicorn task_user.wsgi:application --bind 0.0.0.0:8000 || { echo "Gunicorn failed to start"; exit 1; } + gunicorn --bind 0.0.0.0:8000 task_user.wsgi:application \ + --log-level=debug \ + --capture-output \ + --access-logfile - \ + --error-logfile - || { echo "Gunicorn failed to start"; exit 1; } fi \ No newline at end of file diff --git a/Back-End/task_user/manage.py b/Back-End/task_user/manage.py old mode 100755 new mode 100644 diff --git a/Back-End/task_user/requirements.txt b/Back-End/task_user/requirements.txt index 94d9bbe1..4a17a260 100644 --- a/Back-End/task_user/requirements.txt +++ b/Back-End/task_user/requirements.txt @@ -1,26 +1,26 @@ -aiohappyeyeballs==2.4.4 -aiohttp==3.11.9 -aiosignal==1.3.1 +aiohappyeyeballs==2.5.0 +aiohttp>=3.12.14 +aiosignal>=1.4.0 amqp==5.3.1 asgiref==3.8.1 async-timeout==5.0.1 -attrs==24.2.0 +attrs==25.1.0 billiard==4.2.1 celery==5.4.0 -certifi==2024.8.30 +certifi==2025.1.31 cffi==1.17.1 -charset-normalizer==3.4.0 -click==8.1.7 +charset-normalizer==3.4.1 +click==8.1.8 click-didyoumean==0.3.1 click-plugins==1.1.1 click-repl==0.3.0 -cryptography==44.0.0 -Django==5.1.3 -django-cors-headers==4.6.0 -django-filter==24.3 +cryptography==44.0.2 +django==5.2.4 +django-cors-headers==4.7.0 +django-filter==25.1 django-oauth-toolkit==3.0.1 django-redis==5.4.0 -djangorestframework==3.15.2 +djangorestframework>=3.16.0 frozenlist==1.5.0 idna==3.10 jwcrypto==1.5.6 @@ -28,23 +28,26 @@ kombu==5.4.2 multidict==6.1.0 oauthlib==3.2.2 prompt_toolkit==3.0.48 -propcache==0.2.1 -psycopg==3.2.5 -psycopg-binary==3.2.5 +propcache==0.3.0 +psycopg==3.2.9 +psycopg-binary==3.2.9 pycparser==2.22 python-dateutil==2.9.0.post0 -redis==5.2.0 -requests==2.32.3 -six==1.16.0 -sqlparse==0.5.2 +redis==5.2.1 +requests>=2.32.4 +six==1.17.0 +sqlparse==0.5.3 typing_extensions==4.12.2 -tzdata==2024.2 -urllib3==2.2.3 +tzdata==2025.1 +urllib3>=2.5.0 vine==5.1.0 wcwidth==0.2.13 yarl==1.18.3 -djangorestframework-simplejwt==5.4.0 +djangorestframework-simplejwt==5.5.0 # via -r Back-End/requirements.in -drf-yasg==1.21.8 +drf-yasg==1.21.9 # via -r Back-End/requirements.in -gunicorn==21.2.0 \ No newline at end of file +gunicorn==23.0.0 +pillow==11.1.0 +django-prometheus==2.3.1 +prometheus-client==0.21.1 diff --git a/Back-End/task_user/task_app/__init__.py b/Back-End/task_user/task_app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Back-End/task_user/task_app/admin.py b/Back-End/task_user/task_app/admin.py deleted file mode 100644 index 44ea1c77..00000000 --- a/Back-End/task_user/task_app/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin -from django.contrib.auth import get_user_model -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -# Register your models here. - -User = get_user_model() - -@receiver(post_migrate) -def create_superuser(sender, **kwargs): - if not User.objects.filter(email='pasquale@example.com').exists(): - User.objects.create_superuser(username='pasquale', email='pasquale@example.com', password='123') - print('Superuser created successfully.') \ No newline at end of file diff --git a/Back-End/task_user/task_app/apps.py b/Back-End/task_user/task_app/apps.py deleted file mode 100644 index b56cd6e0..00000000 --- a/Back-End/task_user/task_app/apps.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.apps import AppConfig - -class TaskAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'task_app' - - def ready(self): - try: - # from .authentications import register_self , user_register_self - from .admin import create_superuser - #user_register_self() - #print("User login successful") - # register_self() - except Exception as e: - # Optionally log the exception - print(f"Error during registration: {e}") - pass \ No newline at end of file diff --git a/Back-End/task_user/task_app/authentications.py b/Back-End/task_user/task_app/authentications.py deleted file mode 100644 index 1d4dedfb..00000000 --- a/Back-End/task_user/task_app/authentications.py +++ /dev/null @@ -1,141 +0,0 @@ -import requests -from oauth2_provider.models import AccessToken -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed -from django.conf import settings -from datetime import datetime, timedelta -from task_user.settings import oauth2_settings -import json - -def login_self(): - login_url = 'http://localhost:8000/login/login' - data = { - 'email': 'chat@chat.me', - 'password' : 'chat_password', - } - - response = requests.post(login_url, json=data) - - if response.status_code != 200: - raise Exception('Failed to login user') - return response.json() - -def user_register_self(): - register_url = 'http://localhost:8000/login/register' - data = { - 'email': 'chat@chat.me', - 'username': 'my_chat', - 'password': 'chat_password', - } - response = requests.post(register_url, json=data) - - if response.status_code == 400: - login_self() - elif response.status_code != 201: - raise Exception('Failed to register user') - else: - login_self() - - -def register_self(): - csrf_url = 'http://localhost:8000/login/get_csrf_token' - register_url = 'http://localhost:8000/login/Serviceregister' - - data = { - 'name': 'Chat_' + datetime.strftime(datetime.now(), '%Y-%m-%d:%H%M%S'), - 'service_password': oauth2_settings['SERVICE_PASSWORD'], - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'redirect_uris': 'http://localhost:8000', - 'client_id': oauth2_settings['CLIENT_ID'], - 'client_secret': oauth2_settings['CLIENT_SECRET'], - } - - session = requests.Session() - csrf_resp = session.get(csrf_url) - # print("Response cookies:", json.dumps(dict(csrf_resp.cookies), indent=4)) - # print("Response headers:", json.dumps(dict(csrf_resp.headers), indent=4)) - - if 'csrftoken' not in csrf_resp.cookies: - raise Exception('CSRF token not found in response cookies') - - csrf_token = csrf_resp.cookies['csrftoken'] - - access_token = oauth2_settings['TOKEN'] - headers = { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrf_token, - 'Authorization': 'Bearer ' + access_token, - } - - response = session.post(register_url, json=data, headers=headers) - if response.status_code != 200: - raise Exception('Failed to register application') - try: - app_data = response.json() - except json.JSONDecodeError: - raise Exception('Failed to parse JSON response') - oauth2_settings['CLIENT_ID'] = app_data['client_id'] - oauth2_settings['CLIENT_SECRET'] = app_data['client_secret'] - - -#auth classes -import requests -from django.conf import settings -from django.core.cache import cache -from django.contrib.auth.models import AnonymousUser -from rest_framework.authentication import BaseAuthentication -from rest_framework.exceptions import AuthenticationFailed - -from django.contrib.auth import get_user_model - -class TokenAuthentication(BaseAuthentication): - def authenticate(self, request): - # Get token from Authorization header - token = request.META.get('HTTP_AUTHORIZATION') - if not token: - return None # No token provided; DRF will treat it as an anonymous request - - token = token.replace("Bearer ", "") - user = self.get_user_from_token(token) - - if user is not None: - return (user, token) # Return tuple (user, auth) as required by DRF - else: - raise AuthenticationFailed("Invalid or inactive token") - - def get_user_from_token(self, token): - # Check the cache for user - user = cache.get(token) - if user: - return user - - # Token introspection request - introspection_url = oauth2_settings['OAUTH2_INTROSPECTION_URL'] - client_id = oauth2_settings['CLIENT_ID'] - client_secret = oauth2_settings['CLIENT_SECRET'] - - try: - response = requests.post(introspection_url, data={ - 'token': token, - 'client_id': client_id, - 'client_secret': client_secret, - }) - - if response.status_code == 200: - data = response.json() - if data.get('active'): - user_id = data.get('user_id') - User = get_user_model() - try: - user = User.objects.get(id=user_id) - # Cache user for token expiry duration (default to 5 min if not provided) - expires_in = data.get('expires_in', 300) - cache.set(token, user, timeout=expires_in) - return user - except User.DoesNotExist: - pass - except requests.RequestException as e: - raise AuthenticationFailed(f"Token introspection failed: {str(e)}") - - return None # Default to None if authentication fails \ No newline at end of file diff --git a/Back-End/task_user/task_app/celery_task.py b/Back-End/task_user/task_app/celery_task.py deleted file mode 100644 index 7c53767a..00000000 --- a/Back-End/task_user/task_app/celery_task.py +++ /dev/null @@ -1,7 +0,0 @@ -from celery import shared_task -from .models import * -import requests - -@shared_task -def task_notify(): - pass \ No newline at end of file diff --git a/Back-End/task_user/task_app/dictionaries.py b/Back-End/task_user/task_app/dictionaries.py deleted file mode 100644 index 9ae332c3..00000000 --- a/Back-End/task_user/task_app/dictionaries.py +++ /dev/null @@ -1,9 +0,0 @@ -TASK_CATEGORIES = { - "00": "NONE", - "SP": "Sport", - "ED": "Education", - "HE": "Health", - "AR": "Art", - "SS": "Social Skills", - "MD": "Mindset", -} \ No newline at end of file diff --git a/Back-End/task_user/task_app/models.py b/Back-End/task_user/task_app/models.py deleted file mode 100644 index 12c6e0e7..00000000 --- a/Back-End/task_user/task_app/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models -from django.conf import settings -from .dictionaries import TASK_CATEGORIES -from user_app.models import UserProfile - -# Create your models here. -# class Categories(models.Model): -# id = models.AutoField(primary_key=True) -# name = models.CharField(max_length=255, unique=True) -# description = models.TextField() - -class Tasks(models.Model): - id = models.AutoField(primary_key=True, unique=True) - author = models.ForeignKey(UserProfile, on_delete=models.SET_NULL, null=True, related_name="creator") - name = models.CharField(max_length=255) - description = models.TextField() - duration = models.DurationField() - exp = models.PositiveIntegerField() - category = models.CharField(max_length=3, choices=TASK_CATEGORIES, default='00') - previous_task = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, related_name='sequence') - # previous_task = models.ManyToManyField("self", null=True, related_name="father") - # next_task = models.ForeignKey('Tasks', on_delete=models.SET(0)) - -class Progresses(models.Model): - id = models.AutoField(primary_key=True, unique=True) - task = models.ForeignKey(Tasks, on_delete=models.CASCADE, related_name="job") - user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="joiner") - rate = models.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = models.DateTimeField(auto_now_add=True) - last_modified = models.DateTimeField(auto_now=True) - finish_date = models.DateTimeField(null=True, default=None) diff --git a/Back-End/task_user/task_app/serializer.py b/Back-End/task_user/task_app/serializer.py deleted file mode 100644 index 083e90e1..00000000 --- a/Back-End/task_user/task_app/serializer.py +++ /dev/null @@ -1,98 +0,0 @@ -from rest_framework import serializers -from .models import Tasks, Progresses -from user_app.models import UserProfile -from .dictionaries import TASK_CATEGORIES -import datetime - -class TasksSerializer(serializers.ModelSerializer): - author = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()) - name = serializers.CharField(max_length=255) - description = serializers.CharField() - duration = serializers.DurationField() - exp = serializers.IntegerField() - category = serializers.ChoiceField(choices=TASK_CATEGORIES, required=True) - previous_task = serializers.PrimaryKeyRelatedField(queryset=Tasks.objects.all(), required=False) - - def validate_name(self, value): - if len(str(value)) < 1: - raise serializers.ValidationError('name is not valid') - return value - - def validate_description(self, value): - if len(str(value)) < 1: - raise serializers.ValidationError('description is not valid') - return value - - def validate_exp(self, value): - if int(value) <= 0: - raise serializers.ValidationError('exp is not valid') - return value - - class Meta: - model = Tasks - fields = '__all__' - -class ProgressesSerializer(serializers.ModelSerializer): - task = serializers.PrimaryKeyRelatedField(queryset=Tasks.objects.all(), many=False) - user = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all(), many=False) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - def validate_rate(self, value): - if float(value) < 0: - raise serializers.ValidationError("rate is not valid") - return value - - def validate(self, data): - if Progresses.objects.filter(task=data['task'], user=data['user']): - raise serializers.ValidationError("user is already registered to this task") - t = data['task'] - if t.previous_task is None: - return data - if Progresses.objects.filter(task=t.previous_task, user=data['user']): - if Progresses.objects.get(task=t.previous_task, user=data['user']).rate >= 100: - return data - raise serializers.ValidationError("user must complete previous task before join this one") - - class Meta: - model = Progresses - fields = '__all__' - -class ProgressesReadSerializer(serializers.ModelSerializer): - task = TasksSerializer() - user = serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all(), many=False) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=0) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - class Meta: - model = Progresses - fields = '__all__' - -class ProgressManageSerializer(serializers.ModelSerializer): - task = serializers.PrimaryKeyRelatedField(read_only=True) - user = serializers.PrimaryKeyRelatedField(read_only=True) - rate = serializers.DecimalField(max_digits=6, decimal_places=3, required=True) - begin_date = serializers.DateTimeField(read_only=True) - last_modified = serializers.DateTimeField(read_only=True) - finish_date = serializers.DateTimeField(read_only=True) - - def validate_rate(self, value): - if float(value) < 0: - raise serializers.ValidationError("rate is not valid") - return value - - def validate(self, data): - try: - if data['rate'] >= 100: - data['finish_date'] = datetime.datetime.now() - return data - except KeyError: - raise serializers.ValidationError("rate is not provided") - - class Meta: - model = Progresses - fields = '__all__' diff --git a/Back-End/task_user/task_app/start_tables.py b/Back-End/task_user/task_app/start_tables.py deleted file mode 100644 index 4a7dfd59..00000000 --- a/Back-End/task_user/task_app/start_tables.py +++ /dev/null @@ -1,74 +0,0 @@ -from .models import Tasks -from user_app.models import Avatars, UserProfile -from django.db.models.signals import post_migrate -from django.dispatch import receiver - -def CreateTasks(**kwargs): - # if not Avatars.objects.all(): - # a = Avatars.objects.create( - # name='default avatar' - # ) - # a.save() - # if not Users.objects.all(): - # u = Users.objects.create( - # user_id=1, - # first_name="mario", - # last_name="rossi", - # birth_date="2000-1-1", - # bio="bio dei miei ciglioni", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - # u = Users.objects.create( - # user_id=2, - # first_name="vincenzo", - # last_name="bianchi", - # birth_date="1999-11-4", - # bio="bio dei miei ciglioni", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - # u = Users.objects.create( - # user_id=3, - # staff=True, - # first_name="guglielmo", - # last_name="duranti", - # birth_date="1900-11-4", - # bio="ma ti fanno anche da uomo", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - # if not Tasks.objects.all(): - # t = Tasks.objects.create( - # author=Users.objects.get(user_id=3), - # name='walk', - # description='walk at least 10k step each day', - # duration='7 00:00:00', - # exp=1000, - # category='SP' - # ) - # t.save() - # t = Tasks.objects.create( - # author=Users.objects.get(user_id=3), - # name='run', - # description='improve your body health step by step', - # duration='30 00:00:00', - # exp=2000, - # category='SP', - # previous_task=Tasks.objects.get(id=1) - # ) - # t.save() - # t = Tasks.objects.create( - # author=Users.objects.get(user_id=3), - # name='drink water', - # description='drink 2l of water a day', - # duration='7 00:00:00', - # exp=500, - # category='HE' - # ) - # t.save() - -@receiver(post_migrate) -def CreateTasksSignal(sender, **kwargs): - CreateTasks() - print('done') \ No newline at end of file diff --git a/Back-End/task_user/task_app/tests.py b/Back-End/task_user/task_app/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/Back-End/task_user/task_app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Back-End/task_user/task_app/urls.py b/Back-End/task_user/task_app/urls.py deleted file mode 100644 index 4b7b910f..00000000 --- a/Back-End/task_user/task_app/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('task', views.TaskGen.as_view(), name='task_gen'), - path('task//', views.TaskManage.as_view(), name='task_manage'), - path('progress', views.ProgressGen.as_view(), name='progress_gen'), - path('progress//', views.ProgressDelete.as_view(), name='progress_manage'), - path('progress/&/', views.ProgressManage.as_view(), name='user_progress'), - path('ubt', views.GetUsersByTask.as_view(), name='users_by_task'), - path('tbu', views.GetTasksByUser.as_view(), name='task_by_user'), - # path('tasks_by_category', views.GetTasksByCategory.as_view(), name='tasks_by_category'), - # path('gufet', views.GetUsersForEachTask.as_view(), name='gufet'), - # path('gtfeu', views.GetTasksForEachUser.as_view(), name='gtfeu') -] \ No newline at end of file diff --git a/Back-End/task_user/task_app/views.py b/Back-End/task_user/task_app/views.py deleted file mode 100644 index 117fa789..00000000 --- a/Back-End/task_user/task_app/views.py +++ /dev/null @@ -1,113 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from rest_framework import permissions, status, generics, filters -from rest_framework.views import APIView -from rest_framework.response import Response -from .models import Tasks, Progresses -from .serializer import TasksSerializer, ProgressesSerializer, ProgressManageSerializer, ProgressesReadSerializer -from user_app.models import UserProfile -from django_filters.rest_framework import DjangoFilterBackend - -class MultipleFieldLookupMixin: - """ - Apply this mixin to any view or viewset to get multiple field filtering - based on a `lookup_fields` attribute, instead of the default single field filtering. - """ - def get_object(self): - queryset = self.get_queryset() # Get the base queryset - queryset = self.filter_queryset(queryset) # Apply any filter backends - filter = {} - for field in self.lookup_fields: - if self.kwargs.get(field): # Ignore empty fields. - filter[field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter) # Lookup the object - self.check_object_permissions(self.request, obj) - return obj - -class TaskGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = TasksSerializer - lookup_fields = ['author__user_id', 'category', 'duration', 'exp'] - - def get_queryset(self): - queryset = Tasks.objects.all() - if self.request.method == 'GET': - user_id = self.request.query_params.get('user_id') - if user_id: - My_progress = Progresses.objects.filter(user_id=user_id) - queryset = queryset.exclude(id__in=[x.task.id for x in My_progress]) - return queryset - return Tasks.objects.all() - -class TaskManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = TasksSerializer - # lookup_field = 'id' - lookup_url_kwarg = 'id' - queryset = Tasks.objects.all() - -class ProgressGen(generics.ListCreateAPIView): - permission_classes = (permissions.AllowAny,) - filter_backends = [DjangoFilterBackend] - filterset_fields = ['user__user_id', 'task__id'] - queryset = Progresses.objects.all() - - def get_serializer_class(self): - if self.request.method == 'POST': - return ProgressesSerializer - return ProgressesReadSerializer - - def get_queryset(self): - queryset = super().get_queryset() - user_id = self.request.query_params.get('user_id', None) - if user_id is not None: - queryset = queryset.filter(user__user_id=user_id) - return queryset - -class ProgressDelete(generics.DestroyAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = ProgressesSerializer - # lookup_field = 'id' - lookup_url_kwarg = 'id' - queryset = Progresses.objects.all() - -class ProgressManage(MultipleFieldLookupMixin, generics.RetrieveUpdateAPIView): - permission_classes = (permissions.AllowAny,) - serializer_class = ProgressManageSerializer - lookup_fields = ['task', 'user'] - queryset = Progresses.objects.all() - -class GetUsersByTask(APIView): - permission_classes = (permissions.AllowAny,) - def get(self, request): - prog = Progresses.objects.all() - tsk = Tasks.objects.all() - ans = dict() - for x in tsk: - row = [] - for y in prog: - if y.task.id == x.id: - row.append(y.user.account_id) - ans[x.id] = row - if len(ans) < 1: - return Response({ - 'error': 'cannot provide a valid view' - }, status=status.HTTP_400_BAD_REQUEST) - return Response(ans) - -class GetTasksByUser(APIView): - permission_classes = (permissions.AllowAny,) - def get(self, request): - prog = Progresses.objects.all() - usr = UserProfile.objects.all() - ans = dict() - for x in usr: - row = [] - for y in prog: - if y.user.id == x.id: - row.append(y.task.id) - ans[x.id] = row - if len(ans) < 1: - return Response({ - 'error': 'cannot provide a valid view' - }, status=status.HTTP_400_BAD_REQUEST) - return Response(ans) diff --git a/Back-End/task_user/task_user/settings.py b/Back-End/task_user/task_user/settings.py index 534b892b..87b02cde 100644 --- a/Back-End/task_user/task_user/settings.py +++ b/Back-End/task_user/task_user/settings.py @@ -10,20 +10,17 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ +from datetime import timedelta from pathlib import Path from celery.schedules import crontab import os - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent API_KEY = os.getenv('API_KEY', '123') -# Aggiungi queste impostazioni -FORCE_SCRIPT_NAME = '/api/user' - def arise(exception): - raise(exception) + raise(exception) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -34,100 +31,130 @@ def arise(exception): # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', True) -def ensure_scheme(url): - if url and not url.startswith(('http://', 'https://')): - return f"http://{url}" - return url - +# Simple microservices definition Microservices = { - 'Login': ensure_scheme(os.getenv('LOGIN_SERVICE', 'http://localhost:8000')), - 'Chat': ensure_scheme(os.getenv('CHAT_SERVICE', 'http://localhost:8001')), - 'Users': ensure_scheme(os.getenv('USERS_SERVICE', 'http://localhost:8002')), - 'Notifications': ensure_scheme(os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003')), - 'Pong': ensure_scheme(os.getenv('PONG_SERVICE', 'http://localhost:8004')), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), + 'Pong': os.getenv('PONG_URL', 'http://localhost:8004'), } K8S_ALLOWED_HOSTS = os.environ.get('K8S_ALLOWED_HOSTS', '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16').split(',') -ALLOWED_HOSTS = [ - 'localhost', - 'localhost:3000', - '127.0.0.1', - '[::1]', - 'trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + K8S_ALLOWED_HOSTS + [ - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] - - -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] -# CORS_ALLOW_ALL_ORIGINS = True +# Get service hosts from environment +K8S_SERVICE_HOSTS_RAW = os.getenv('K8S_SERVICE_HOSTS', '').split(',') if os.getenv('K8S_SERVICE_HOSTS') else [] + +# For ALLOWED_HOSTS: hostnames only (strip schemes/ports) +K8S_SERVICE_HOSTS_CLEAN = [] +for host in K8S_SERVICE_HOSTS_RAW: + if host: + # Remove http:// or https:// if present + clean_host = host.replace('http://', '').replace('https://', '') + # Remove port if present + if ':' in clean_host: + clean_host = clean_host.split(':')[0] + K8S_SERVICE_HOSTS_CLEAN.append(clean_host) + +# For CORS: full URLs with schemes +K8S_SERVICE_HOSTS_WITH_SCHEME = [f"http://{host}" for host in K8S_SERVICE_HOSTS_RAW if host] + +K8S_SERVICE_HOSTS = K8S_SERVICE_HOSTS_CLEAN + +ALLOWED_HOSTS = ['*'] +# 'localhost', +# 'localhost:3000', +# '127.0.0.1', +# '[::1]', +# 'trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS + +# CORS_ALLOWED_ORIGINS = [ +# 'http://localhost:3000', +# 'http://localhost', +# 'http://127.0.0.1', +# 'http://[::1]', +# 'https://trascendence.42firenze.it', +# Microservices['Login'], +# Microservices['Chat'], +# Microservices['Users'], +# Microservices['Notifications'], +# Microservices['Pong'], +# ] + K8S_SERVICE_HOSTS_WITH_SCHEME +CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CSRF_TRUSTED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost', - 'http://127.0.0.1', - 'http://[::1]', - 'https://trascendence.42firenze.it', - Microservices['Login'], - Microservices['Chat'], - Microservices['Users'], - Microservices['Notifications'], - Microservices['Pong'], -] + 'http://localhost:3000', + 'http://localhost', + 'http://127.0.0.1', + 'http://[::1]', + 'https://trascendence.42firenze.it', + # Aggiungi IP privati per HTTPS + 'https://10.0.2.15', # L'IP specifico della tua VM + # O meglio ancora, patterns per tutti gli IP privati: + 'http://10.*', + 'https://10.*', + 'http://192.168.*', + 'https://192.168.*', + 'http://172.16.*', + 'https://172.16.*', + 'http://172.17.*', + 'https://172.17.*', + 'http://172.18.*', + 'https://172.18.*', + # Continua per tutti i range 172.16.x.x fino a 172.31.x.x + 'http://localhost:8443', + 'http://127.0.0.1:8443', + 'http://10.0.2.15:8443', + 'http://10.0.2.15.xip.io:8443', + 'http://10.11.*.xip.io:8443', + Microservices['Login'], + Microservices['Chat'], + Microservices['Users'], + Microservices['Notifications'], + Microservices['Pong'], +] + K8S_SERVICE_HOSTS_WITH_SCHEME CORS_ALLOW_HEADERS = [ - 'accept', - 'accept-encoding', - 'authorization', - 'content-type', - 'dnt', - 'origin', - 'user-agent', - 'x-csrftoken', - 'x-requested-with', + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', ] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'rest_framework_simplejwt', - 'django_filters', - 'oauth2_provider', - 'task_app', - 'user_app', - 'corsheaders', + 'django_prometheus', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'django_filters', + 'oauth2_provider', + 'drf_yasg', + 'user_app', + 'corsheaders', ] MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', # Add at the top + 'user_app.middleware.HealthCheckMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -135,7 +162,8 @@ def ensure_scheme(url): 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', # Add at the end ] ROOT_URLCONF = 'task_user.urls' @@ -158,40 +186,49 @@ def ensure_scheme(url): WSGI_APPLICATION = 'task_user.wsgi.application' - # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('POSTGRES_DB', 'usertask_db'), - 'USER': os.getenv('POSTGRES_USER', 'pasquale'), - 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), - 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), - 'PORT': os.getenv('POSTGRES_PORT', '5435'), - }, - 'backup': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), - } -} + +# Use SQLite for tests, PostgreSQL otherwise +import sys +if 'test' in sys.argv: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'usertask_db'), + 'USER': os.getenv('POSTGRES_USER', 'pasquale'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD', '123'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5435'), + }, + 'backup': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(BASE_DIR / 'db.sqlite3'), + } + } import secrets oauth2_settings = { - 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', - 'CLIENT_ID': secrets.token_urlsafe(32), - 'CLIENT_SECRET': secrets.token_urlsafe(64), - 'TOKEN': '', - 'REFRESH_TOKEN': '', - 'EXPIRES': '', - 'token_type': '', - 'scope': '', - 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password + 'OAUTH2_INTROSPECTION_URL': 'http://localhost:8000/o/introspect/', + 'CLIENT_ID': secrets.token_urlsafe(32), + 'CLIENT_SECRET': secrets.token_urlsafe(64), + 'TOKEN': '', + 'REFRESH_TOKEN': '', + 'EXPIRES': '', + 'token_type': '', + 'scope': '', + 'SERVICE_PASSWORD': '123', ## TODO: Change this to a more secure password } - # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -219,7 +256,6 @@ def ensure_scheme(url): # }, # } - # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -234,7 +270,13 @@ def ensure_scheme(url): # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Changed from '/static/' + +# Add this to define where Django should look for static files +#STATICFILES_DIRS = [ +# os.path.join(BASE_DIR, "static"), +#] # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -243,16 +285,252 @@ def ensure_scheme(url): REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], - 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], - 'DEFAULT_AUTHENTICATION_CLASSES': ['user_app.middleware.JWTAuth'], + 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], + 'DEFAULT_AUTHENTICATION_CLASSES': ['user_app.middleware.JWTAuth'], } ADMIN = { - 'username': os.getenv('ADMIN_USERNAME', 'admin'), - 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), - 'password': os.getenv('ADMIN_PASSWORD', 'admin'), + 'username': os.getenv('ADMIN_USERNAME', 'admin'), + 'email': os.getenv('ADMIN_EMAIL', 'admin@admin.com'), + 'password': os.getenv('ADMIN_PASSWORD', 'admin'), } -MEDIA_ROOT = BASE_DIR / 'media/' -# MEDIA_URL = 'http://localhost:8000/' \ No newline at end of file +# Redis Configuration +REDIS_HOST = os.getenv('REDIS_HOST', 'my-umbrella-redis-chart-service.redis-namespace.svc.cluster.local') +REDIS_PORT = os.getenv('REDIS_PORT', '6379') +REDIS_CACHE_DB = os.getenv('REDIS_CACHE_DB', '3') # Task_user: DB 3 + +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'KEY_PREFIX': 'taskuser:', # Service prefix for data isolation + } + } +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, + 'ALGORITHM': 'HS256', + # 'ISSUER': 'login', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', +} + +# Media and Static Files Configuration +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Ensure media directory exists +os.makedirs(MEDIA_ROOT, exist_ok=True) + +# ELK-Optimized Logging Configuration for user service +# This configuration provides standardized logging for the ELK Stack + +import os +from datetime import datetime + +# Logging environment configuration +USE_JSON_LOGGING = os.getenv('USE_JSON_LOGGING', 'true').lower() == 'true' +USE_FILE_LOGGING = os.getenv('USE_FILE_LOGGING', 'false').lower() == 'true' +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + +# Create logs directory for file logging (if enabled) +if USE_FILE_LOGGING: + LOG_DIR = os.path.join(BASE_DIR, 'logs') + os.makedirs(LOG_DIR, exist_ok=True) + ERROR_LOG = os.path.join(LOG_DIR, f'error_{datetime.now().strftime("%Y-%m-%d")}.log') + INFO_LOG = os.path.join(LOG_DIR, f'info_{datetime.now().strftime("%Y-%m-%d")}.log') +else: + ERROR_LOG = None + INFO_LOG = None + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'json_kubernetes': { + 'format': '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "module": "%(module)s", "message": "%(message)s", "pathname": "%(pathname)s", "lineno": %(lineno)d, "funcName": "%(funcName)s", "process": %(process)d, "thread": %(thread)d, "service": "user"}', + 'datefmt': '%Y-%m-%dT%H:%M:%S.%fZ', + 'style': '%', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + # Primary handler for Kubernetes - JSON logs to stdout + 'console_json': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json_kubernetes', + }, + # Development console handler + 'console_verbose': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + # Optional file handlers (controlled by USE_FILE_LOGGING) + 'file_error': { + 'level': 'ERROR', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': ERROR_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and ERROR_LOG else { + 'level': 'ERROR', + 'class': 'logging.NullHandler', + }, + 'file_info': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': INFO_LOG or '/dev/null', + 'when': 'midnight', + 'backupCount': 7, + 'formatter': 'json_kubernetes', + } if USE_FILE_LOGGING and INFO_LOG else { + 'level': 'INFO', + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + 'formatter': 'verbose', + }, + }, + 'loggers': { + # Root logger + '': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + # Django core loggers + 'django': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.request': { + 'handlers': (['console_json'] if USE_JSON_LOGGING else ['console_verbose']) + ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'django.security': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + # ASGI/WebSocket loggers (for chat and pong services) + 'daphne': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'channels': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'websockets': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + # Service-specific loggers + 'user': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_user': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Additional app-specific loggers + 'task_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'user_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'pong_app': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'my_notifications': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + 'api_docs': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'DEBUG' if os.getenv('DEBUG', 'False').lower() == 'true' else 'INFO', + 'propagate': False, + }, + # Third-party library loggers + 'redis': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + 'celery': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'oauth2_provider': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'INFO', + 'propagate': False, + }, + 'corsheaders': { + 'handlers': ['console_json'] if USE_JSON_LOGGING else ['console_verbose'], + 'level': 'WARNING', + 'propagate': False, + }, + }, +} diff --git a/Back-End/task_user/task_user/urls.py b/Back-End/task_user/task_user/urls.py index b0b3a075..5b16e8dc 100644 --- a/Back-End/task_user/task_user/urls.py +++ b/Back-End/task_user/task_user/urls.py @@ -15,10 +15,139 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import path, include, re_path +from django.conf import settings +from django.conf.urls.static import static +from django.http import JsonResponse, HttpResponse +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="User & Task Management API", + default_version='v1', + description="API for user profiles, friendships, and task management", + terms_of_service="https://www.example.com/terms/", + contact=openapi.Contact(email="admin@trascendence.local"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +# Custom view that serves Swagger UI with JWT authentication +def custom_swagger_view(request): + html = """ + + + User & Task Management API Documentation + + + + + + +
+ + + + +""" + return HttpResponse(html) urlpatterns = [ path('admin/', admin.site.urls), - path('task/', include('task_app.urls')), path('user/', include('user_app.urls')), + path('', include('django_prometheus.urls')), # Add metrics endpoint + + # Swagger/API documentation with JWT support + path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', custom_swagger_view, name='schema-swagger-ui'), + re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/schema/', schema_view.without_ui(cache_timeout=0), name='schema-json-api'), + + # Health check endpoint + path('health/', lambda request: JsonResponse({'status': 'healthy'}), name='health'), ] + +# Serve media files in development +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/Back-End/task_user/task_user/wsgi.py b/Back-End/task_user/task_user/wsgi.py index 1fbba796..c6bbdf5f 100644 --- a/Back-End/task_user/task_user/wsgi.py +++ b/Back-End/task_user/task_user/wsgi.py @@ -13,4 +13,18 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_user.settings') +from django.core.wsgi import get_wsgi_application + +class PrefixMiddleware: + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + path = environ.get('PATH_INFO', '') + if path.startswith(self.prefix): + environ['PATH_INFO'] = path[len(self.prefix):] or '/' + return self.app(environ, start_response) + application = get_wsgi_application() +application = PrefixMiddleware(application, '/api/user') \ No newline at end of file diff --git a/Back-End/task_user/user_app/apps.py b/Back-End/task_user/user_app/apps.py index 63daa16a..25cd67fd 100644 --- a/Back-End/task_user/user_app/apps.py +++ b/Back-End/task_user/user_app/apps.py @@ -8,7 +8,8 @@ def ready(self): try: # from .authentications import register_self , user_register_self from .admin import create_superuser - + from .friend_status_pub import FriendStatusPublisher + FriendStatusPublisher().start_publisher() except Exception as e: # Optionally log the exception print(f"Error during registration: {e}") diff --git a/Back-End/task_user/user_app/authentications.py b/Back-End/task_user/user_app/authentications.py index 36ccd80a..0f3f2771 100644 --- a/Back-End/task_user/user_app/authentications.py +++ b/Back-End/task_user/user_app/authentications.py @@ -134,7 +134,7 @@ def register_self(): # user_id = data.get('user_id') # User = get_user_model() # try: -# user = User.objects.get(id=user_id) +# user = User.objects.get(user_id=user_id) # # Cache user for token expiry duration (default to 5 min if not provided) # expires_in = data.get('expires_in', 300) # cache.set(token, user, timeout=expires_in) diff --git a/Back-End/task_user/user_app/friend_status_pub.py b/Back-End/task_user/user_app/friend_status_pub.py new file mode 100644 index 00000000..7f301e69 --- /dev/null +++ b/Back-End/task_user/user_app/friend_status_pub.py @@ -0,0 +1,119 @@ +import redis +import json +import threading +import time +import logging +from django.conf import settings +from django.db.models import Q +import os + +logger = logging.getLogger('django') + +class FriendStatusPublisher: + def __init__(self): + self.redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=int(os.getenv('REDIS_ONLINE_STATUS_DB', '0')), + decode_responses=True + ) + self.pubsub = self.redis_client.pubsub() + self.running = False + self.publisher_thread = None + + def start_publisher(self): + """Start listening for global status changes""" + if not self.running: + self.running = True + self.publisher_thread = threading.Thread( + target=self._listen_for_global_status, + daemon=True + ) + self.publisher_thread.start() + logger.info("๐Ÿ”” Friend status publisher started") + + def stop_publisher(self): + """Stop the publisher""" + self.running = False + if self.pubsub: + self.pubsub.close() + logger.info("๐Ÿ”• Friend status publisher stopped") + + def _listen_for_global_status(self): + """Listen for global user status changes from notification service""" + try: + # Subscribe to global status changes + self.pubsub.subscribe('user_status_global') + logger.info("๐Ÿ“ก Listening for global user status changes") + + for message in self.pubsub.listen(): + if not self.running: + break + + if message['type'] == 'message': + try: + data = json.loads(message['data']) + self._handle_global_status_change(data) + except Exception as e: + logger.error(f"Error processing global status message: {e}") + + except Exception as e: + logger.error(f"Error in friend status publisher: {e}") + finally: + self.pubsub.close() + + def _handle_global_status_change(self, data): + """When a user's status changes globally, notify their friends""" + user_id = data.get('user_id') + is_online = data.get('is_online') + + if not user_id: + return + + try: + # Get user's friends (this stays in user service) + friend_ids = self._get_user_friends(user_id) + + if friend_ids: + # Publish to each friend's personal channel + for friend_id in friend_ids: + self.redis_client.publish( + f'friend_status_{friend_id}', + json.dumps({ + 'type': 'friend_status_change', + 'friend_user_id': user_id, + 'is_online': is_online, + 'timestamp': time.time() + }) + ) + + logger.info(f"๐Ÿ“ก Notified {len(friend_ids)} friends about user {user_id} going {'online' if is_online else 'offline'}") + + except Exception as e: + logger.error(f"Error handling global status change for user {user_id}: {e}") + + def _get_user_friends(self, user_id): + """Get friends list - this stays internal to user service""" + try: + from .models import Friendships + + # Get accepted friendships for this user + friendships = Friendships.objects.filter( + Q(user_1_id=user_id) | Q(user_2_id=user_id), + accepted=True + ) + + friend_ids = [] + for friendship in friendships: + # Get the other user's ID + friend_id = friendship.user_2_id if friendship.user_1_id == user_id else friendship.user_1_id + friend_ids.append(friend_id) + + return friend_ids + + except Exception as e: + logger.error(f"Error getting friends for user {user_id}: {e}") + return [] + +# Global instance +friend_status_publisher = FriendStatusPublisher() \ No newline at end of file diff --git a/Back-End/task_user/user_app/image_storage.py b/Back-End/task_user/user_app/image_storage.py new file mode 100644 index 00000000..fef98592 --- /dev/null +++ b/Back-End/task_user/user_app/image_storage.py @@ -0,0 +1,98 @@ +import os +import logging +from datetime import datetime +from django.conf import settings +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile + +# Get dedicated logger for image storage operations +logger = logging.getLogger('image_storage') + +class ImageStorageService: + """Service class for handling local image storage""" + + _instance = None + + @classmethod + def get_instance(cls): + """Singleton pattern to reuse the service instance""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + """Initialize the image storage service""" + self.media_root = settings.MEDIA_ROOT + self.media_url = settings.MEDIA_URL + + # Ensure avatars directory exists + self.avatars_dir = os.path.join(self.media_root, 'avatars') + os.makedirs(self.avatars_dir, exist_ok=True) + + logger.info(f"Image storage service initialized. Media root: {self.media_root}") + + def upload_avatar(self, user_id, file_obj, filename=None): + """Upload user avatar to local storage""" + try: + if not filename: + extension = os.path.splitext(file_obj.name)[1].lower() + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + filename = f"avatar_{user_id}_{timestamp}{extension}" + + # Ensure filename has proper extension + if not any(filename.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.gif']): + extension = os.path.splitext(file_obj.name)[1].lower() + if extension: + filename += extension + else: + filename += '.jpg' # Default extension + + # Create the relative path for storage + relative_path = os.path.join('avatars', filename) + + # Save the file using Django's default storage + saved_path = default_storage.save(relative_path, file_obj) + + # Generate the URL for the saved file + file_url = f"{self.media_url}{saved_path}" + + logger.info(f"Avatar uploaded successfully for user {user_id}: {saved_path}") + + return True, file_url, saved_path + + except Exception as e: + logger.error(f"Error uploading avatar for user {user_id}: {str(e)}", exc_info=True) + return False, str(e), None + + def delete_avatar(self, file_path): + """Delete an avatar file from local storage""" + try: + if file_path and default_storage.exists(file_path): + default_storage.delete(file_path) + logger.info(f"Avatar deleted successfully: {file_path}") + return True + else: + logger.warning(f"Avatar file not found for deletion: {file_path}") + return False + except Exception as e: + logger.error(f"Error deleting avatar {file_path}: {str(e)}", exc_info=True) + return False + + def get_avatar_path(self, file_url): + """Extract the file path from a URL for deletion purposes""" + if file_url and file_url.startswith(self.media_url): + return file_url[len(self.media_url):] + return None + + def list_user_avatars(self, user_id): + """List all avatar files for a specific user""" + try: + avatars_path = 'avatars' + if default_storage.exists(avatars_path): + files = default_storage.listdir(avatars_path)[1] # Get files, not directories + user_avatars = [f for f in files if f.startswith(f'avatar_{user_id}_')] + return user_avatars + return [] + except Exception as e: + logger.error(f"Error listing avatars for user {user_id}: {str(e)}", exc_info=True) + return [] diff --git a/Back-End/task_user/user_app/middleware.py b/Back-End/task_user/user_app/middleware.py index 0eb05583..ce5af9d2 100644 --- a/Back-End/task_user/user_app/middleware.py +++ b/Back-End/task_user/user_app/middleware.py @@ -1,5 +1,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.cache import cache +from django.db import OperationalError, connections +from django.http import JsonResponse from rest_framework_simplejwt.tokens import AccessToken from rest_framework_simplejwt.authentication import JWTAuthentication from .models import UserProfile @@ -11,279 +13,177 @@ logger = logging.getLogger('django') class ServiceAuthentication(authentication.BaseAuthentication): - """ - Authentication class for inter-service communication. - Uses API keys instead of channels-based auth. - """ - def authenticate(self, request): - # Only apply this authentication to specific endpoints - if not request.path.endswith('/task/api/endpoint/') or request.method != 'POST': - return None - - # Check for API key - api_key = request.headers.get('X-API-KEY') - if not api_key or api_key != settings.API_KEY: - logger.warning(f"Invalid API key attempt: {api_key}") - raise exceptions.AuthenticationFailed('Invalid API key') - - # Return anonymous user for service-to-service authentication - return (AnonymousUser(), None) + """ + Authentication class for inter-service communication. + Uses API keys instead of channels-based auth. + """ + def authenticate(self, request): + # Only apply this authentication to specific endpoints + if not request.path.endswith('/task/api/endpoint/') or request.method != 'POST': + return None + + # Check for API key + api_key = request.headers.get('X-API-KEY') + if not api_key or api_key != settings.API_KEY: + logger.warning(f"Invalid API key attempt: {api_key}") + raise exceptions.AuthenticationFailed('Invalid API key') + + # Return anonymous user for service-to-service authentication + return (AnonymousUser(), None) def get_user_from_token(token): - """ - Synchronous version of token validation (no channels dependency) - """ - try: - # Decode the token - access_token = AccessToken(token) - user_id = access_token['user_id'] - - # Check if the user is already cached - user = cache.get(token) - if user: - return user - - # Retrieve the user from the database - try: - user = UserProfile.objects.get(id=user_id) - # Cache the user with a timeout (e.g., 5 minutes) - cache.set(token, user, timeout=300) - return user - except UserProfile.DoesNotExist: - return AnonymousUser() - except Exception as e: - return AnonymousUser() + """ + Synchronous version of token validation (no channels dependency) + """ + try: + # Decode the token + access_token = AccessToken(token) + user_id = access_token['user_id'] + + # Check if the user is already cached + user = cache.get(token) + if user: + return user + # Retrieve the user from the database + try: + user = UserProfile.objects.get(user_id=user_id) + # Cache the user with a timeout (e.g., 5 minutes) + cache.set(token, user, timeout=300) + return user + except UserProfile.DoesNotExist: + return AnonymousUser() + except Exception as e: + return AnonymousUser() class JWTAuth(JWTAuthentication): - """ - Enhanced JWT authentication class with caching support and improved error handling. - """ - def authenticate(self, request): - # Skip authentication for specific paths if needed - if request.path.endswith('/task/some_endpoint/') and request.method == 'POST': - logger.debug(f"Skipping JWT auth for {request.path}") - return (AnonymousUser(), None) - - # Check for cached authentication result first - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if auth_header and auth_header.startswith('Bearer '): - token = auth_header.replace('Bearer ', '') - - # Try to get from cache first - cache_key = f"jwt_auth_{token}" - cached_result = cache.get(cache_key) - - if cached_result is not None: - logger.debug("Using cached JWT authentication result") - if cached_result == "anonymous": - return None - return cached_result - - try: - # Call parent class to validate token - auth_result = super().authenticate(request) - - # Cache the result (None results don't need caching) - if auth_result: - # Cache successful authentications for 5 minutes - cache.set(cache_key, auth_result, timeout=300) - logger.debug(f"JWT auth successful for user {auth_result[0]}") - return auth_result - else: - # Cache negative results for a shorter time - cache.set(cache_key, "anonymous", timeout=60) - return None - - except Exception as e: - logger.warning(f"JWT authentication failed: {str(e)}") - return None - - return super().authenticate(request) - - -class APIKeyPermission(BasePermission): - """ - Simple API key permission class - """ - def has_permission(self, request, view): - api_key = request.headers.get('X-API-KEY') - if api_key: - return api_key == settings.API_KEY - return False - -# class JWTAuthMiddleware(BaseMiddleware): -# """ -# JWT Authentication middleware for WebSocket connections. -# Extracts the JWT token from query parameters or headers and authenticates the user. -# """ -# async def __call__(self, scope, receive, send): -# # Skip authentication for specific paths if needed -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# # Try to get token from query string first -# query_string = parse_qs(scope["query_string"].decode()) -# token = None - -# if 'token' in query_string: -# # Token from query param: ws://example.com/ws/chat/1/?token= -# token = query_string['token'][0] -# else: -# # Try to get token from headers (less common for WebSockets but possible) -# headers = dict(scope['headers']) -# auth_header = headers.get(b'authorization') -# if auth_header: -# token_str = auth_header.decode() -# if token_str.startswith('Bearer '): -# token = token_str[7:] # Remove 'Bearer ' prefix - -# if token: -# # Get user from token -# try: -# scope['user'] = await self.get_user_from_jwt(token) -# logger.info(f"WebSocket authenticated user: {scope['user']}") -# except Exception as e: -# logger.error(f"WebSocket JWT authentication failed: {str(e)}") -# scope['user'] = AnonymousUser() -# else: -# scope['user'] = AnonymousUser() + """ + Enhanced JWT authentication class with caching support and improved error handling. + """ + user_id_claim = 'user_id' + + def authenticate(self, request): + # Skip authentication for specific paths if needed + if request.path.endswith('/chat/new_user/') and request.method == 'POST': + logger.debug(f"Skipping JWT auth for {request.path}") + return None + + logger.debug(f"Headers in request: {request.META}") + # Check for cached authentication result first + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if not auth_header or not auth_header.startswith('Bearer '): + return None + + token = auth_header.replace('Bearer ', '') + if not token: + return None -# return await super().__call__(scope, receive, send) + # Try to get from cache first + cache_key = f"jwt_auth_{token}" + cached_result = cache.get(cache_key) -# @database_sync_to_async -# def get_user_from_jwt(self, token): -# try: -# # Decode the JWT token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] + if cached_result is not None: + logger.debug("Using cached JWT authentication result") + if cached_result == "anonymous": + return None + return cached_result -# # Check if the user is already cached -# user = cache.get(f"jwt_ws_user_{token}") -# if user: -# return user + # If not in cache, validate the token using the more robust method from WebSocket middleware + try: + # Directly try to decode the JWT token first (like in WebSocket middleware) + access_token = AccessToken(token) + user_id = access_token.get('user_id') + + if not user_id: + logger.warning("No user_id claim found in token") + cache.set(cache_key, "anonymous", timeout=60) + return None -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(f"jwt_ws_user_{token}", user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# logger.error(f"JWT token validation failed: {str(e)}") -# return AnonymousUser() - -# def JWTAuthMiddlewareStack(inner): -# """ -# Helper function that returns a JWT auth middleware wrapped with AuthMiddlewareStack. -# """ -# return JWTAuthMiddleware(AuthMiddlewareStack(inner)) - -# class TokenAuthMiddleware(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# query_string = parse_qs(scope["query_string"].decode()) -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) -# try: -# token = query_string.get("token") -# if token: -# scope["user"] = await get_user_from_token(token[0]) -# else: -# scope["user"] = AnonymousUser() -# except Exception as e: -# scope["user"] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# def TokenAuthMiddlewareStack(inner): -# return TokenAuthMiddleware(AuthMiddlewareStack(inner)) - -# class TokenAuthMiddlewareHTTP(BaseMiddleware): -# async def __call__(self, scope, receive, send): -# if scope['path'] == '/chat/new_user/': -# scope['user'] = AnonymousUser() -# return await super().__call__(scope, receive, send) - -# headers = dict(scope['headers']) -# token = headers.get(b'authorization') -# if token: -# token = token.decode().replace("Bearer ", "") -# scope['user'] = await self.get_user_from_token(token) -# else: -# scope['user'] = AnonymousUser() - -# return await super().__call__(scope, receive, send) - -# @database_sync_to_async -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] + # Check if user exists in database + try: + user = UserProfile.objects.get(user_id=user_id) + # Successfully authenticated + auth_result = (user, token) + cache.set(cache_key, auth_result, timeout=300) + logger.debug(f"JWT auth successful for user {user}") + return auth_result + except UserProfile.DoesNotExist: + logger.warning(f"User ID {user_id} from token not found in UserProfile") + cache.set(cache_key, "anonymous", timeout=60) + return None + + except Exception as e: + # Log the error but don't cache exceptions + logger.warning(f"JWT authentication failed: {str(e)}") + logger.warning(f"Token details: {token[:10]}...{token[-10:] if len(token) > 20 else ''}") + try: + # Try to extract some debug info without validation + decoded = AccessToken(token, verify=False) + user_id = decoded.get('user_id') + exp = decoded.get('exp', 'unknown') + iat = decoded.get('iat', 'unknown') + logger.warning(f"Token debug info - User ID: {user_id}, Expires: {exp}, Issued: {iat}") + if user_id: + logger.warning(f"User exists: {UserProfile.objects.filter(user_id=user_id).exists()}") + except Exception as inner_e: + logger.warning(f"Token decode failed: {str(inner_e)}") + + return None + + def get_user(self, validated_token): + """ + Returns user from validated token for DRF compatibility + """ + try: + user_id = validated_token[self.user_id_claim] -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user - -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() - - -# class TokenAuthPermission(BasePermission): -# def get_user_from_token(self, token): -# try: -# # Decode the token -# access_token = AccessToken(token) -# user_id = access_token['user_id'] + # Cerca l'utente nel modello UserProfile + user = UserProfile.objects.get(user_id=user_id) -# # Check if the user is already cached -# user = cache.get(token) -# if user: -# return user - -# # Retrieve the user from the database -# try: -# user = UserProfile.objects.get(id=user_id) -# # Cache the user with a timeout (e.g., 5 minutes) -# cache.set(token, user, timeout=300) -# return user -# except UserProfile.DoesNotExist: -# return AnonymousUser() -# except Exception as e: -# return AnonymousUser() - -# def has_permission(self, request, view): -# if request.path == '/chat/new_user/' and request.method == 'POST': -# request.user = AnonymousUser() -# return True -# token = request.META.get('HTTP_AUTHORIZATION') -# if token: -# token = token.replace("Bearer ", "") -# user = self.get_user_from_token(token) -# if user and not isinstance(user, AnonymousUser): -# request.user = user -# return True -# return False - -# from django.conf import settings -# from rest_framework.permissions import BasePermission - -# class APIKeyPermission(BasePermission): -# def has_permission(self, request, view): -# api_key = request.headers.get('X-API-KEY') -# if api_key: -# return api_key == settings.API_KEY -# return False \ No newline at end of file + return user + except UserProfile.DoesNotExist: + logger.warning(f"User ID {user_id} from token not found in UserProfile") + return None + except Exception as e: + logger.error(f"Error getting user from token: {str(e)}") + return None +class APIKeyPermission(BasePermission): + """ + Simple API key permission class + """ + def has_permission(self, request, view): + api_key = request.headers.get('X-API-KEY') + if api_key: + return api_key == settings.API_KEY + return False + +class HealthCheckMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Allow health check endpoint regardless of host + if request.path.endswith('/health'): # Liveness probe + return JsonResponse({'status': 'ok'}) + elif request.path.endswith('/ready'): # Readiness probe + db_ready = False + try: + # Check the default database connection + db_conn = connections['default'] + db_conn.cursor() # Attempt to get a cursor to check connectivity + # Optionally, you could execute a very simple query: + # with db_conn.cursor() as cursor: + # cursor.execute("SELECT 1") + # cursor.fetchone() + db_ready = True + except OperationalError: + logger.error("Readiness probe: Database connection failed.") + # db_ready remains False + + if db_ready: + return JsonResponse({'status': 'ready', 'database': 'ok'}) + else: + return JsonResponse({'status': 'unready', 'database': 'unavailable'}, status=503) + + return self.get_response(request) \ No newline at end of file diff --git a/Back-End/task_user/user_app/models.py b/Back-End/task_user/user_app/models.py index 3ff9cfcb..27485a13 100644 --- a/Back-End/task_user/user_app/models.py +++ b/Back-End/task_user/user_app/models.py @@ -1,28 +1,104 @@ +import os from django.db import models +from django.db.models import Q from django.conf import settings - -class Avatars(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=255) - image = models.URLField(max_length=200, default='https://drive.google.com/file/d/1MDi_OPO_HtWyKTmI_35GQ4KjA7uh0Z9U/view?usp=drive_link') - # image = models.ImageField(upload_to="avatar/", null=True) +def avatar_upload_path(instance, filename): + """Generate upload path for avatar files""" + # Extract file extension + ext = filename.split('.')[-1] + # Generate filename as: avatar__. + filename = f'avatar_{instance.user.user_id}_{filename}' + return os.path.join('avatars', filename) class UserProfile(models.Model): staff = models.BooleanField(default=False) user_id = models.IntegerField(unique=True, primary_key=True) + email = models.EmailField(max_length=255, unique=True, null=True) + username = models.CharField(max_length=255, unique=True, default="LVL1_noob") first_name = models.CharField(max_length=255, null=True, default="") last_name = models.CharField(max_length=255, null=True, default="") birth_date = models.DateField(null=True, default=None, blank=True) bio = models.TextField(default="", null=True, blank=True) exp = models.IntegerField(default=0) level = models.IntegerField(default=0) - avatar = models.ForeignKey(Avatars, default=1, on_delete=models.SET(1), related_name="picture") + current_avatar_url = models.URLField(max_length=500, default='https://drive.google.com/file/d/1MDi_OPO_HtWyKTmI_35GQ4KjA7uh0Z9U/view?usp=drive_link', blank=True) last_modified = models.DateTimeField(auto_now=True) + has_two_factor_auth = models.BooleanField(default=False) + + + friends = models.ManyToManyField( + 'self', + through='Friendships', + through_fields=('user_1', 'user_2'), + symmetrical=False, # Allow user_1 to be friends with user_2 without user_2 being friends with user_1 + related_name='related_to' + ) + blocked_users = models.ManyToManyField('self', related_name='blocked_by', symmetrical=False, blank=True) + + def block_user(self, user): + self.blocked_users.add(user) + + def unblock_user(self, user): + self.blocked_users.remove(user) + + def is_blocked(self, user): + return self.blocked_users.filter(pk=user.pk).exists() + +class Avatars(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=255) + image = models.ImageField(upload_to=avatar_upload_path, max_length=500) + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='avatars', null=True, blank=True, default=None) + is_current = models.BooleanField(default=False) + last_modified = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + # If this is marked as current avatar, update the user's avatar URL + if self.is_current: + # Unset any other current avatars for this user + Avatars.objects.filter(user=self.user, is_current=True).exclude(pk=self.pk).update(is_current=False) + # Update the user's current avatar URL to the full URL + if self.image: + self.user.current_avatar_url = f"{settings.MEDIA_URL}{self.image.name}" + self.user.save(update_fields=['current_avatar_url']) + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + # Delete the actual file when the model instance is deleted + if self.image: + if os.path.isfile(self.image.path): + os.remove(self.image.path) + super().delete(*args, **kwargs) + + def get_image_url(self): + """Get the full URL for the image""" + if self.image: + return f"{settings.MEDIA_URL}{self.image.name}" + return None + class Friendships(models.Model): id = models.AutoField(primary_key=True) user_1 = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='u1') user_2 = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='u2') accepted = models.BooleanField(default=False) - last_modified = models.DateTimeField(auto_now=True) \ No newline at end of file + last_modified = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('user_1', 'user_2') + + @staticmethod + def get_friends(user): + # Get friendships where user is either user_1 or user_2 and accepted=True + return UserProfile.objects.filter( + Q(u1__user_2=user, u1__accepted=True) | + Q(u2__user_1=user, u2__accepted=True) + ) + + @staticmethod + def are_friends(user1, user2): + return Friendships.objects.filter( + (Q(user_1=user1, user_2=user2) | Q(user_1=user2, user_2=user1)), + accepted=True + ).exists() \ No newline at end of file diff --git a/Back-End/task_user/user_app/notification.py b/Back-End/task_user/user_app/notification.py index 687e19c4..e21bf285 100644 --- a/Back-End/task_user/user_app/notification.py +++ b/Back-End/task_user/user_app/notification.py @@ -2,10 +2,10 @@ import os, aiohttp Microservices = { - 'Login': os.getenv('LOGIN_SERVICE', 'http://localhost:8000'), - 'Chat': os.getenv('CHAT_SERVICE', 'http://localhost:8001'), - 'Users': os.getenv('USERS_SERVICE', 'http://localhost:8002'), - 'Notifications': os.getenv('NOTIFICATIONS_SERVICE', 'http://localhost:8003'), + 'Login': os.getenv('LOGIN_URL', 'http://localhost:8000'), + 'Chat': os.getenv('CHAT_URL', 'http://localhost:8001'), + 'Users': os.getenv('USER_URL', 'http://localhost:8002'), + 'Notifications': os.getenv('NOTIFICATIONS_URL', 'http://localhost:8003'), 'Personal' : "Self", } @@ -19,7 +19,7 @@ class BaseNotification(models.Model): id = models.AutoField(primary_key=True) Sender = models.CharField(max_length=200, choices=[(key, key) for key in Microservices.keys()]) - message = models.TextField() + message = models.JSONField() class Meta: abstract = True diff --git a/Back-End/task_user/user_app/online_status.py b/Back-End/task_user/user_app/online_status.py new file mode 100644 index 00000000..4411e4fb --- /dev/null +++ b/Back-End/task_user/user_app/online_status.py @@ -0,0 +1,48 @@ +import redis +import os +from django.conf import settings + +class OnlineStatusService: + def __init__(self): + # Use shared Redis DB for online status + self.redis_client = redis.Redis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + db=int(os.getenv('REDIS_ONLINE_STATUS_DB', '0')), # Shared DB + decode_responses=True + ) + + def is_user_online(self, user_id): + """Fast Redis lookup for user online status""" + try: + return bool(self.redis_client.exists(f"online:{user_id}")) + except Exception: + return False + + def get_multiple_users_online_status(self, user_ids): + """Batch check online status for multiple users""" + try: + online_status = {} + if not user_ids: + return online_status + + pipe = self.redis_client.pipeline() + for user_id in user_ids: + pipe.exists(f"online:{user_id}") + results = pipe.execute() + + for user_id, is_online in zip(user_ids, results): + online_status[user_id] = bool(is_online) + return online_status + except Exception: + return {user_id: False for user_id in user_ids} + + def get_online_users_count(self): + """Get total count of online users""" + try: + return self.redis_client.scard("online_users") + except Exception: + return 0 + +# Create a singleton instance +online_status_service = OnlineStatusService() diff --git a/Back-End/task_user/user_app/serializer.py b/Back-End/task_user/user_app/serializer.py index ab65d385..b05f4fda 100644 --- a/Back-End/task_user/user_app/serializer.py +++ b/Back-End/task_user/user_app/serializer.py @@ -5,34 +5,70 @@ class AvatarsSerializer(serializers.ModelSerializer): name = serializers.CharField(max_length=255) image = serializers.ImageField() + image_url = serializers.SerializerMethodField() class Meta: model = Avatars - fields = '__all__' + fields = ['id', 'name', 'image', 'image_url', 'user', 'is_current', 'last_modified'] + + def get_image_url(self, obj): + """Get the full URL for the image""" + return obj.get_image_url() class UsersSerializer(serializers.ModelSerializer): - staff = serializers.BooleanField(default=False) - first_name = serializers.CharField(max_length=255, default="") - last_name = serializers.CharField(max_length=255, default="") - birth_date = serializers.DateField(default=None) - bio = serializers.CharField(default="") - exp = serializers.IntegerField(default=0) - level = serializers.IntegerField(default=0) - # avatar = serializers.PrimaryKeyRelatedField(queryset=Avatars.objects.all(), many=False, default=Avatars.objects.get(id=1)) - #TODO avatar = serializers.PrimaryKeyRelatedField(queryset=Avatars.objects.all(), many=False, default=Avatars.objects.get(id=1)) - last_modified = serializers.DateTimeField(read_only=True) - + current_avatar = serializers.SerializerMethodField() + class Meta: model = UserProfile fields = '__all__' + + def get_current_avatar(self, obj): + """Get the current avatar object for the user""" + try: + current_avatar = Avatars.objects.filter(user=obj, is_current=True).first() + if current_avatar: + return AvatarsSerializer(current_avatar).data + return None + except Exception: + return None +class UserNotificationSerializer(serializers.ModelSerializer): + """Lightweight serializer for user data in notifications""" + class Meta: + model = UserProfile + fields = ['user_id', 'username', 'first_name', 'last_name', 'current_avatar_url', 'level'] class FriendshipsSerializer(serializers.ModelSerializer): - # user_1 = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - # user_2 = serializers.PrimaryKeyRelatedField(queryset=Users.objects.all(), many=False) - # accepted = serializers.BooleanField(read_only=True) - # last_modified = serializers.DateTimeField(read_only=True) + friend_info = serializers.SerializerMethodField() + + class Meta: + model = Friendships + fields = ['id', 'accepted', 'friend_info'] + + def get_friend_info(self, obj): + # Get the current user from context + request = self.context.get('request') + if not request or not request.user: + return None + current_user = request.user + + # Determine who is the friend (not the current user) + if obj.user_1 == current_user: + friend = obj.user_2 + else: + friend = obj.user_1 + + data = { + 'user_id': friend.user_id, + 'username': friend.username, + 'email': friend.email, + 'sent_by': UsersSerializer(obj.user_1).data + } + + return data + +class BlockUserSerializer(serializers.Serializer): + user_id = serializers.IntegerField() class Meta: - model = Friendships - fields = '__all__' + fields = ['user_id'] diff --git a/Back-End/task_user/user_app/tests.py b/Back-End/task_user/user_app/tests.py index 7ce503c2..c6569275 100644 --- a/Back-End/task_user/user_app/tests.py +++ b/Back-End/task_user/user_app/tests.py @@ -1,3 +1,41 @@ -from django.test import TestCase +from django.test import TestCase, Client -# Create your tests here. +class UserAppURLTests(TestCase): + def setUp(self): + self.client = Client() + + def test_avatar_endpoint(self): + response = self.client.get('/user/avatar') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_user_endpoint(self): + response = self.client.get('/user/user') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_me_endpoint(self): + response = self.client.get('/user/me') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_friend_endpoint(self): + response = self.client.get('/user/friend') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_addfriend_endpoint(self): + response = self.client.get('/user/addfriend') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_levelup_endpoint(self): + response = self.client.get('/user/levelup') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_block_endpoint(self): + response = self.client.get('/user/block') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_update_2fa_status_endpoint(self): + response = self.client.get('/user/user/update-2fa/') + self.assertIn(response.status_code, [200, 401, 403]) + + def test_search_user_endpoint(self): + response = self.client.get('/user/search') + self.assertIn(response.status_code, [200, 401, 403]) diff --git a/Back-End/task_user/user_app/urls.py b/Back-End/task_user/user_app/urls.py index 0902fc17..f95c105c 100644 --- a/Back-End/task_user/user_app/urls.py +++ b/Back-End/task_user/user_app/urls.py @@ -1,12 +1,16 @@ from django.urls import path -from . import views +from rest_framework import views +from .views import OnlineStatusView, UserGen, UserManage, FriendList, LevelUp, AddFriend, BlockUser, AvatarManager, Update2FAStatus, UserSearch urlpatterns = [ - path('avatar', views.AvatarGen.as_view(), name='avatar_gen'), - path('avatar//', views.AvatarManage.as_view(), name='avatar_manage'), - path('user', views.UserGen.as_view(), name='user_gen'), - path('user//', views.UserManage.as_view(), name='user_manage'), - path('friend', views.FriendList.as_view(), name='friend_list'), - path('addfriend', views.AddFriend.as_view(), name='add_friend'), - path('levelup', views.LevelUp.as_view(), name='level_up'), + path('avatar', AvatarManager.as_view(), name='avatar_gen'), + path('user', UserGen.as_view(), name='user_gen'), + path('me', UserManage.as_view(), name='user_manage'), + path('friend', FriendList.as_view(), name='friend_list'), + path('addfriend', AddFriend.as_view(), name='add_friend'), + path('levelup', LevelUp.as_view(), name='level_up'), + path('block', BlockUser.as_view(), name='block_user'), + path('user/update-2fa/', Update2FAStatus.as_view(), name='update_2fa_status'), + path('search', UserSearch.as_view(), name='search_user'), + path('online-status', OnlineStatusView.as_view(), name='online_status'), ] \ No newline at end of file diff --git a/Back-End/task_user/user_app/views.py b/Back-End/task_user/user_app/views.py index e7149bda..ceb6f226 100644 --- a/Back-End/task_user/user_app/views.py +++ b/Back-End/task_user/user_app/views.py @@ -1,4 +1,5 @@ from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.models import AnonymousUser from rest_framework.views import APIView from rest_framework import permissions, status, generics from rest_framework.response import Response @@ -10,6 +11,40 @@ import asyncio from .middleware import ServiceAuthentication , JWTAuth from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.filters import SearchFilter +from rest_framework.pagination import PageNumberPagination +from .online_status import online_status_service + +class IsAuthenticatedUserProfile(permissions.BasePermission): + """ + Permesso personalizzato per il modello UserProfile. + Verifica semplicemente se l'utente รจ autenticato (non รจ AnonymousUser). + """ + def has_permission(self, request, view): + return request.user is not None and not isinstance(request.user, AnonymousUser) + +class IsOwnUserProfile(permissions.BasePermission): + """ + Permesso che verifica se l'utente sta accedendo ai propri dati. + Da usare per le richieste che manipolano dati utente. + """ + def has_permission(self, request, view): + # Verifica prima se l'utente รจ autenticato + if not IsAuthenticatedUserProfile().has_permission(request, view): + return False + + # Per le viste che usano l'ID utente nell'URL + user_id = view.kwargs.get('user_id') + if user_id and str(request.user.user_id) == str(user_id): + return True + + # Per le richieste che usano l'ID utente nei parametri query + user_id_param = request.query_params.get('user_id') + if user_id_param and str(request.user.user_id) == str(user_id_param): + return True + + return False class MultipleFieldLookupMixin: """ @@ -17,7 +52,7 @@ class MultipleFieldLookupMixin: based on a `lookup_fields` attribute, instead of the default single field filtering. """ def get_object(self): - queryset = self.get_queryset() # Get the base queryset + queryset = self.get_queryset() queryset = self.filter_queryset(queryset) # Apply any filter backends filter = {} for field in self.lookup_fields: @@ -27,16 +62,17 @@ def get_object(self): self.check_object_permissions(self.request, obj) return obj -class AvatarGen(generics.ListCreateAPIView): - permission_classes = (permissions.IsAuthenticated,) - serializer_class = AvatarsSerializer - queryset = Avatars.objects.all() +# class AvatarGen(generics.ListCreateAPIView): +# permission_classes = (IsAuthenticatedUserProfile,) +# serializer_class = AvatarsSerializer +# queryset = Avatars.objects.all() -class AvatarManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.IsAuthenticated,) - serializer_class = AvatarsSerializer - lookup_url_kwarg = 'id' - queryset = Avatars.objects.all() +# class AvatarManage(generics.RetrieveUpdateDestroyAPIView): +# permission_classes = (IsAuthenticatedUserProfile,) +# serializer_class = AvatarsSerializer +# lookup_url_kwarg = 'id' +# queryset = Avatars.objects.all() +# cazzo class UserGen(generics.ListCreateAPIView): serializer_class = UsersSerializer @@ -46,32 +82,166 @@ class UserGen(generics.ListCreateAPIView): def get_permissions(self): if self.request.method == 'POST': - print("POST here") self.permission_classes = [] self.authentication_classes = [ServiceAuthentication] else: - self.permission_classes = (permissions.IsAuthenticated,) + self.permission_classes = (IsAuthenticatedUserProfile,) self.authentication_classes = [JWTAuth] return super().get_permissions() class UserManage(generics.RetrieveUpdateDestroyAPIView): - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] serializer_class = UsersSerializer - lookup_url_kwarg = 'user_id' queryset = UserProfile.objects.all() + + def get_object(self): + return self.request.user -class FriendList(generics.ListAPIView): - permissions_classes = (permissions.IsAuthenticated,) - serializer_class = FriendshipsSerializer - filter_backends = [DjangoFilterBackend] - filterset_fields = ['user_1__user_id', 'user_2__user_id', 'accepted'] - queryset = Friendships.objects.all() +class UserSearchPagination(PageNumberPagination): + """Custom pagination for user search results.""" + page_size = 10 # Number of users per page + page_size_query_param = 'page_size' # Allow client to override page size + max_page_size = 50 # Maximum allowed page size + page_query_param = 'page' # URL parameter name for page number + +class UserSearch(generics.ListAPIView): + """ + Search for users by username or email with partial matching. + + This endpoint allows authenticated users to search for other users in the system. + The search is performed on both username and email fields using partial matching. + Current user and blocked users are automatically excluded from search results. + + Query Parameters: + search (str): Search term to match against username or email. + Supports partial matching (case-insensitive). + Example: ?search=john will match "john_doe" and "john@example.com" + page (int): Page number for pagination (default: 1) + page_size (int): Number of results per page (default: 10, max: 50) + + Returns: + 200 OK: Paginated list of users matching the search criteria + { + "count": 45, + "next": "http://localhost:8000/api/users/search/?search=john&page=2", + "previous": null, + "results": [ + { + "user_id": 1, + "username": "john_doe", + "email": "john@example.com", + "level": 5, + "exp": 250, + ... + } + ] + } + + 401 Unauthorized: User is not authenticated + + Examples: + GET /api/users/search/?search=john + GET /api/users/search/?search=admin&page=2&page_size=20 + GET /api/users/search/?search=@gmail.com&page_size=5 + + Notes: + - Search is case-insensitive + - Minimum search term length: 1 character + - Current user is excluded from results + - Blocked users are excluded from results + - Results are ordered by username for consistent pagination + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + serializer_class = UsersSerializer + filter_backends = [SearchFilter] + search_fields = ['username', 'email'] + queryset = UserProfile.objects.all() + pagination_class = UserSearchPagination + def get_queryset(self): + """Exclude current user and blocked users from search results.""" queryset = super().get_queryset() - user_id = self.request.query_params.get('user_id') - if user_id: - queryset = queryset.filter(Q(user_1__user_id=user_id) | Q(user_2__user_id=user_id)) - return queryset + + # Exclude current user from search results + if self.request.user: + queryset = queryset.exclude(user_id=self.request.user.user_id) + + # Exclude blocked users if the relationship exists + if hasattr(self.request.user, 'blocked_users'): + blocked_user_ids = self.request.user.blocked_users.values_list('user_id', flat=True) + queryset = queryset.exclude(user_id__in=blocked_user_ids) + + # Order by username for consistent pagination + return queryset.order_by('username') + + def list(self, request, *args, **kwargs): + """Override list to add online status to search results""" + response = super().list(request, *args, **kwargs) + + # Get user IDs from results and check their online status + if response.data and 'results' in response.data: + user_ids = [user['user_id'] for user in response.data['results']] + online_statuses = online_status_service.get_multiple_users_online_status(user_ids) + + # Add online status to each user + for user in response.data['results']: + user['is_online'] = online_statuses.get(user['user_id'], False) + + return response + +class FriendList(generics.ListAPIView): + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + serializer_class = FriendshipsSerializer + + def get_queryset(self): + user = self.request.user + + status_filter = self.request.query_params.get('status') + + # Base query - all relationships involving current user + queryset = Friendships.objects.filter( + Q(user_1=user) | Q(user_2=user) + ) + + # Filter by acceptance status if requested + if status_filter == 'accepted': + queryset = queryset.filter(accepted=True) + elif status_filter == 'pending': + queryset = queryset.filter(accepted=False) + + return queryset + + def list(self, request, *args, **kwargs): + """Override list to add online status to friend_info""" + # Get the serialized data + response = super().list(request, *args, **kwargs) + + # Extract friend IDs from the friend_info + friend_ids = [] + for friendship_data in response.data: + friend_info = friendship_data.get('friend_info') + if friend_info and 'user_id' in friend_info: + friend_ids.append(friend_info['user_id']) + + # Batch check online status + online_statuses = {} + if friend_ids: + try: + online_statuses = online_status_service.get_multiple_users_online_status(friend_ids) + except Exception as e: + print(f"Error getting online statuses: {e}") + + # Add online status to each friend_info + for friendship_data in response.data: + friend_info = friendship_data.get('friend_info') + if friend_info and 'user_id' in friend_info: + friend_id = friend_info['user_id'] + friend_info['is_online'] = online_statuses.get(friend_id, False) + + return response class LevelUp(APIView): """ Use this endpoint to add exp to a user. @@ -80,7 +250,7 @@ class LevelUp(APIView): user_id (int): The id of the user. exp (int): The amount of experience points to add. """ - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticatedUserProfile,) def post(self, request): user_id = request.data.get('user_id') @@ -94,83 +264,105 @@ def post(self, request): user.exp = 0 user.save() return Response({'message': 'user level up'}, status=status.HTTP_200_OK) - -from .notification import Microservices class AddFriend(APIView): """ Use this endpoint to send a friend request, accept a friend request or delete a friendship. + methods: + - POST: Send a friend request to another user. + - PATCH: Accept a friend request from another user. + - DELETE: Delete a friendship with another user. + Args: - user_1 (int): The id of the user sending the request. - user_2 (int): The id of the user receiving the request. + receiver (int): The id of the user receiving the request. """ - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] def post(self, request): - serializer = FriendshipsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - u1 = UserProfile.objects.get(user_id=serializer.data['user_1']) - u2 = UserProfile.objects.get(user_id=serializer.data['user_2']) - if Friendships.objects.filter(user_1=u1, user_2=u2) or Friendships.objects.filter(user_1=u2, user_2=u1): - if Friendships.objects.get(user_1=u1, user_2=u2).accepted: + try: + u1 = request.user + u2 = get_object_or_404(UserProfile, user_id=request.data.get('receiver')) + if Friendships.objects.filter(user_1=u1, user_2=u2) or Friendships.objects.filter(user_1=u2, user_2=u1): + # Check if already friends + friendship = Friendships.objects.filter(user_1=u1, user_2=u2).first() or Friendships.objects.filter(user_1=u2, user_2=u1).first() + if friendship and friendship.accepted: + return Response({ + 'info': 'users are already friends' + }, status=status.HTTP_200_OK) return Response({ - 'info': 'users are already friends' + 'info': 'friend request is pending' }, status=status.HTTP_200_OK) + if u1.user_id == u2.user_id: + return Response({ + 'error': 'You cannot send a friend request to yourself, lmao!' + }, status=status.HTTP_400_BAD_REQUEST) + fs = Friendships.objects.create( + user_1=u1, + user_2=u2, + accepted=False, + ) + fs.save() + notifi = ImmediateNotification.objects.create( + Sender="Users", + message={ + 'type': 'friend_request', + 'data': UserNotificationSerializer(u1).data + }, + user_id=u2.user_id, + group_id=None, + ) + SendNotificationSync(notifi) return Response({ - 'info': 'friend request is pending' + 'info': 'friend request sent' }, status=status.HTTP_200_OK) - fs = Friendships.objects.create( - user_1=u1, - user_2=u2 - ) - fs.save() - notifi = ImmediateNotification.objects.create( - Sender="Users", - message=f'Friend request from {u1.first_name} {u1.last_name}', - user_id=u2.user_id, - group_id=None, - ) - SendNotificationSync(notifi) - return Response({ - 'info': 'friend request sent' - }, status=status.HTTP_200_OK) + except Exception as e: + return Response({ + 'error': str(e) + }, status=status.HTTP_400_BAD_REQUEST) def patch(self, request): try: - serializer = FriendshipsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - u1 = UserProfile.objects.get(user_id=serializer.data['user_1']) - u2 = UserProfile.objects.get(user_id=serializer.data['user_2']) - if Friendships.objects.filter(user_1=u1, user_2=u2): - fs = Friendships.objects.get(user_1=u1, user_2=u2) - fs.accepted = True - fs.save() - notifi = ImmediateNotification.objects.create( - Sender="Users", - message=f'{u2.first_name} {u2.last_name} accepted your friend request', - user_id=u1.user_id, - group_id=None, - ) - SendNotificationSync(notifi) - return Response({ - 'info': 'friend request accepted' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'error': 'friend request not found' - }, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: + u1 = get_object_or_404(UserProfile, user_id=request.data.get('receiver')) + u2 = request.user + # Find the friendship in either direction + friendship = Friendships.objects.filter( + user_1=u1, user_2=u2 + ).first() + if friendship: + friendship.accepted = True + friendship.save() + # Send notification + notifi = ImmediateNotification.objects.create( + Sender="Users", + message={ 'type': 'friend_request_accepted', 'data': UserNotificationSerializer(u2).data }, + user_id=u1.user_id, + group_id=None, + ) + SendNotificationSync(notifi) + return Response({ + 'info': 'friend request accepted' + }, status=status.HTTP_200_OK) + else: return Response({ - 'error': str(e) + 'error': 'friend request not found' }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({ + 'error': str(e) + }, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): try: - serializer = FriendshipsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - u1 = UserProfile.objects.get(user_id=serializer.data['user_1']) - u2 = UserProfile.objects.get(user_id=serializer.data['user_2']) + u1 = request.user + u2 = get_object_or_404(UserProfile, user_id=request.data.get('receiver')) + if u1.user_id == u2.user_id: + return Response({ + 'error': 'You cannot delete a friendship with yourself, lmao!' + }, status=status.HTTP_400_BAD_REQUEST) + # Find the friendship in either direction + # This will find the friendship regardless of who is user_1 or user_2 friendship = Friendships.objects.filter( (Q(user_1=u1) & Q(user_2=u2)) | (Q(user_1=u2) & Q(user_2=u1)) ).first() @@ -178,14 +370,10 @@ def delete(self, request): friendship.delete() notifi = ImmediateNotification.objects.create( Sender="Users", - message=f'deleted friendship with {u2.user_id}', - user_id=u1.user_id, - group_id=None, - ) - SendNotificationSync(notifi) - notifi = ImmediateNotification.objects.create( - Sender="Users", - message=f'deleted friendship with {u1.user_id}', + message={ + 'type': 'friendship_deleted', + 'data': UserNotificationSerializer(u1).data + }, user_id=u2.user_id, group_id=None, ) @@ -199,5 +387,312 @@ def delete(self, request): }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: return Response({ - 'error': str(e) + 'errors': str(e) }, status=status.HTTP_400_BAD_REQUEST) + + +class BlockUser(APIView): + """ Use this endpoint to block a user. + + post: + Blocks a user. + + Parameters: + user_id (int): The id of the user to block. + + Response: + - 200 OK: User blocked successfully. + - 400 Bad Request: Invalid request data. + - 403 Forbidden: User is trying to block themselves. + - 404 Not Found: User not found. + + + delete: + Unblocks a user. + + Parameters: + user_id (int): The id of the user to unblock. + + Response: + - 200 OK: User unblocked successfully. + - 400 Bad Request: Invalid request data. + - 403 Forbidden: User is trying to unblock themselves. + - 404 Not Found: User not found. + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + def post(self, request): + user_id = request.data.get('user_id') + if not user_id: + return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) + if user_id == request.user.user_id: + return Response({'error': 'You cannot block yourself'}, status=status.HTTP_403_FORBIDDEN) + user_to_block = get_object_or_404(UserProfile, user_id=user_id) + request.user.block_user(user_to_block) + return Response({'message': 'User blocked'}, status=status.HTTP_200_OK) + + def delete(self, request): + user_id = request.data.get('user_id') + if not user_id: + return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) + if user_id == request.user.user_id: + return Response({'error': 'You cannot unblock yourself'}, status=status.HTTP_403_FORBIDDEN) + user_to_unblock = get_object_or_404(UserProfile, user_id=user_id) + request.user.unblock_user(user_to_unblock) + return Response({'message': 'User unblocked'}, status=status.HTTP_200_OK) + + def get(self, request): + blocked_users = request.user.blocked_users.all() + serializer = BlockUserSerializer(blocked_users, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class AvatarManager(APIView): + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + parser_classes = (MultiPartParser, FormParser) + + def post(self, request): + try: + if 'image' not in request.FILES: + return Response({'error': 'image is required'}, status=status.HTTP_400_BAD_REQUEST) + + image = request.FILES['image'] + avatar_name = request.data.get('name', f'Avatar_for_{request.user.username}') + user = request.user + + valid_types = ['image/jpeg', 'image/png', 'image/gif'] + if image.content_type not in valid_types: + return Response({'error': 'Invalid image type'}, status=status.HTTP_400_BAD_REQUEST) + if image.size > 10 * 1024 * 1024: + return Response({'error': 'Image size too large (max 10MB)'}, status=status.HTTP_400_BAD_REQUEST) + + # Create the avatar object first to use the upload_to function + avatar = Avatars( + user=user, + name=avatar_name, + is_current=True + ) + + # Save the image using Django's ImageField + avatar.image = image + avatar.save() + + return Response({ + 'message': 'Avatar uploaded successfully', + 'avatar': avatar.get_image_url(), + 'avatar_id': avatar.id + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def get(self, request): + avatar_id = request.query_params.get('avatar_id') + if avatar_id: + avatar = get_object_or_404(Avatars, id=avatar_id) + serializer = AvatarsSerializer(avatar) + return Response(serializer.data, status=status.HTTP_200_OK) + else: + # Return all avatars for the user + avatars = Avatars.objects.filter(user=request.user) + serializer = AvatarsSerializer(avatars, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def delete(self, request): + """Delete an avatar""" + try: + avatar_id = request.data.get('avatar_id') + if not avatar_id: + return Response({'error': 'avatar_id is required'}, status=status.HTTP_400_BAD_REQUEST) + + avatar = get_object_or_404(Avatars, id=avatar_id, user=request.user) + + # If this was the current avatar, we need to handle that + was_current = avatar.is_current + + # Delete the avatar (this will also delete the file due to our model's delete method) + avatar.delete() + + # If this was the current avatar, set a default or another avatar as current + if was_current: + # Try to find another avatar to set as current + other_avatar = Avatars.objects.filter(user=request.user).first() + if other_avatar: + other_avatar.is_current = True + other_avatar.save() + else: + # Reset to default avatar URL + request.user.current_avatar_url = 'https://drive.google.com/file/d/1MDi_OPO_HtWyKTmI_35GQ4KjA7uh0Z9U/view?usp=drive_link' + request.user.save(update_fields=['current_avatar_url']) + + return Response({'message': 'Avatar deleted successfully'}, status=status.HTTP_200_OK) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class Update2FAStatus(APIView): + """ + View to enable/disable two-factor authentication flag for a user. + Used by the login microservice to update the 2FA status after verification. + + POST: + Enable/disable 2FA for a specific user + + Parameters: + user_id (int): The ID of the user to update + enabled (bool): Whether 2FA should be enabled or disabled + + Returns: + 200 OK: Status updated successfully + 400 Bad Request: Invalid parameters + 404 Not Found: User not found + """ + # Allow both service authentication and JWT authentication + authentication_classes = [ServiceAuthentication] + permission_classes = [] + + def post(self, request): + user_id = request.data.get('user_id') + enabled = request.data.get('enabled') + + if user_id is None or enabled is None: + return Response( + {'error': 'Both user_id and enabled fields are required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + # Convert enabled to boolean if it comes as string + if isinstance(enabled, str): + enabled = enabled.lower() == 'true' + + user = get_object_or_404(UserProfile, user_id=user_id) + user.has_two_factor_auth = enabled + user.save(update_fields=['has_two_factor_auth']) + + return Response( + { + 'message': f'2FA {"enabled" if enabled else "disabled"} for user {user_id}', + 'user_id': user_id, + 'has_two_factor_auth': enabled + }, + status=status.HTTP_200_OK + ) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class OnlineStatusView(APIView): + """ + Quick API endpoint to check online status of users. + + GET: Check online status of multiple users + Query Parameters: + user_ids: Comma-separated list of user IDs (e.g., "1,2,3") + + POST: Check online status of specific users + Body: + { + "user_ids": [1, 2, 3] + } + + Returns: + { + "users": [ + {"user_id": 1, "is_online": true}, + {"user_id": 2, "is_online": false}, + {"user_id": 3, "is_online": true} + ], + "summary": { + "total_users": 3, + "online_count": 2, + "offline_count": 1 + } + } + """ + permission_classes = (IsAuthenticatedUserProfile,) + authentication_classes = [JWTAuth] + + def get(self, request): + """Check online status via query parameters""" + user_ids_str = request.query_params.get('user_ids', '') + if not user_ids_str: + return Response({'error': 'user_ids parameter required'}, status=400) + + try: + user_ids = [int(uid.strip()) for uid in user_ids_str.split(',') if uid.strip()] + if not user_ids: + return Response({'error': 'No valid user_ids provided'}, status=400) + + online_statuses = online_status_service.get_multiple_users_online_status(user_ids) + + # Format response as list of user objects + users = [] + online_count = 0 + + for user_id in user_ids: + is_online = online_statuses.get(user_id, False) + users.append({ + 'user_id': user_id, + 'is_online': is_online + }) + if is_online: + online_count += 1 + + return Response({ + 'users': users, + 'summary': { + 'total_users': len(user_ids), + 'online_count': online_count, + 'offline_count': len(user_ids) - online_count + } + }) + + except ValueError: + return Response({'error': 'Invalid user_ids format. Use comma-separated integers.'}, status=400) + except Exception as e: + return Response({'error': f'Server error: {str(e)}'}, status=500) + + def post(self, request): + """Check online status via POST body""" + user_ids = request.data.get('user_ids', []) + if not user_ids: + return Response({'error': 'user_ids required'}, status=400) + + if not isinstance(user_ids, list): + return Response({'error': 'user_ids must be a list'}, status=400) + + try: + # Ensure all user_ids are integers + user_ids = [int(uid) for uid in user_ids] + + online_statuses = online_status_service.get_multiple_users_online_status(user_ids) + + # Format response as list of user objects + users = [] + online_count = 0 + + for user_id in user_ids: + is_online = online_statuses.get(user_id, False) + users.append({ + 'user_id': user_id, + 'is_online': is_online + }) + if is_online: + online_count += 1 + + return Response({ + 'users': users, + 'summary': { + 'total_users': len(user_ids), + 'online_count': online_count, + 'offline_count': len(user_ids) - online_count + } + }) + + except ValueError: + return Response({'error': 'All user_ids must be valid integers'}, status=400) + except Exception as e: + return Response({'error': f'Server error: {str(e)}'}, status=500) \ No newline at end of file diff --git a/Front-End/.dockerignore b/Front-End/.dockerignore new file mode 100644 index 00000000..f4757f7a --- /dev/null +++ b/Front-End/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +build +.git +.env +*.md \ No newline at end of file diff --git a/Front-End/Dockerfile b/Front-End/Dockerfile index 6d8fd17a..2741d21a 100644 --- a/Front-End/Dockerfile +++ b/Front-End/Dockerfile @@ -1,16 +1,37 @@ -FROM node:hydrogen-alpine - +FROM node:lts-alpine WORKDIR /app +# Copy package files and install dependencies COPY package*.json ./ -COPY public ./public -COPY index.html ./ -COPY .serve.json ./ +RUN npm install -RUN npm install --only=production +# Copy source files +COPY . . -RUN npm install -g serve +# Fix permissions for node user +RUN mkdir -p /app/.vite && \ + chmod -R 777 /app +# Expose Vite dev server port EXPOSE 3000 -CMD ["npm", "start"] \ No newline at end of file +# Run as non-root user + +USER node + +# Dev command with proper arguments and force new fs instance +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000", "--force"] + +# Production build stage +# FROM node:lts-alpine AS build +# WORKDIR /app +# COPY package*.json ./ +# RUN npm install +# COPY . . +# RUN npm run build + +# Production runtime +# FROM nginx:alpine AS prod +# COPY --from=build /app/dist /usr/share/nginx/html +# EXPOSE 80 +# CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Front-End/TODO b/Front-End/TODO index 3841b8ac..1c3f5823 100644 --- a/Front-End/TODO +++ b/Front-End/TODO @@ -1,39 +1,9 @@ TODO: -- controllare start_table se parte tutto anche con i commenti (va cancellato il database) -va commentato questo in start_tables.py task_user/task_app: - # if not Users.objects.all(): - # u = Users.objects.create( - # user_id=1, - # first_name="mario", - # last_name="rossi", - # birth_date="2000-1-1", - # bio="bio dei miei ciglioni", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - # u = Users.objects.create( - # user_id=2, - # first_name="vincenzo", - # last_name="bianchi", - # birth_date="1999-11-4", - # bio="bio dei miei ciglioni", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - # u = Users.objects.create( - # user_id=3, - # staff=True, - # first_name="guglielmo", - # last_name="duranti", - # birth_date="1900-11-4", - # bio="ma ti fanno anche da uomo", - # avatar=Avatars.objects.get(id=1) - # ) - # u.save() - probabilmente se alla registrazione metti un username gia usato, poi non ti registrera' correttamente' - testare gruppi con piu persone e addChat aggiungendo piu persone insieme (apri profilo ospite) per ora se un nuovo utente si registra entra in tutte le chat - i primi utenti registrati non si registrano correttamente - implementa i TODO in pongContainer.js (backend) +- le chat moch aprono il websocket 5 DONE: @@ -46,4 +16,7 @@ DONE: - errore backend quando si accetta un amicizia (PATCH http://localhost:8002/user/addfriend 400 (Bad Request) handleFriendRequest @ notification.js:16 onclick @ home:1Understand this errorAI - notification.js:33 Error in server response: {error: 'no running event loop'}error: "no running event loop"[[Prototype]]: Objectconstructor: ฦ’ Object()hasOwnProperty: ฦ’ hasOwnProperty()isPrototypeOf: ฦ’ isPrototypeOf()propertyIsEnumerable: ฦ’ propertyIsEnumerable()toLocaleString: ฦ’ toLocaleString()toString: ฦ’ toString()valueOf: ฦ’ valueOf()__defineGetter__: ฦ’ __defineGetter__()__defineSetter__: ฦ’ __defineSetter__()__lookupGetter__: ฦ’ __lookupGetter__()__lookupSetter__: ฦ’ __lookupSetter__()__proto__: (...)get __proto__: ฦ’ __proto__()set __proto__: ฦ’ __proto__()) \ No newline at end of file + notification.js:33 Error in server response: {error: 'no running event loop'}error: "no running event loop"[[Prototype]]: Objectconstructor: ฦ’ Object()hasOwnProperty: ฦ’ hasOwnProperty()isPrototypeOf: ฦ’ isPrototypeOf()propertyIsEnumerable: ฦ’ propertyIsEnumerable()toLocaleString: ฦ’ toLocaleString()toString: ฦ’ toString()valueOf: ฦ’ valueOf()__defineGetter__: ฦ’ __defineGetter__()__defineSetter__: ฦ’ __defineSetter__()__lookupGetter__: ฦ’ __lookupGetter__()__lookupSetter__: ฦ’ __lookupSetter__()__proto__: (...)get __proto__: ฦ’ __proto__()set __proto__: ฦ’ __proto__()) +- controllare start_table se parte tutto anche con i commenti (va cancellato il database) +va commentato questo in start_tables.py task_user/task_app: + # if not Users.objects.all():... \ No newline at end of file diff --git a/Front-End/demo-message-handlers.js b/Front-End/demo-message-handlers.js new file mode 100644 index 00000000..1d42ab8d --- /dev/null +++ b/Front-End/demo-message-handlers.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node + +/** + * DEMO: Enhanced WebSocket Message Handler System + * This script demonstrates how easy it is to add new message types + */ + +// Mock the required functions for demo +function showNotificationToast(message, type) { + console.log(`๐Ÿ”” ${type.toUpperCase()}: ${message}`); +} + +function getFriends() { console.log("๐Ÿ“ฑ Refreshing friends list..."); } +function updateChatList() { console.log("๐Ÿ’ฌ Updating chat list..."); } +function renderFriendRequest() { console.log("๐Ÿ‘ฅ Rendering friend requests..."); } + +let messageHistory = []; + +// Import the message handler registry (simplified for demo) +const MESSAGE_HANDLERS = { + 'friend_request': (msg) => console.log("๐Ÿ‘ค Friend request handler"), + 'friend_accepted': (msg) => console.log("โœ… Friend accepted handler"), + 'chat_message': (msg) => console.log("๐Ÿ’ฌ Chat message handler"), + 'game_invitation': (msg) => console.log("๐ŸŽฎ Game invitation handler"), + 'default': (msg) => console.log("โ“ Default handler") +}; + +// Enhanced getMessageType function +function getMessageType(message) { + if (!message || typeof message !== "object") return "default"; + + if (message.message && typeof message.message === "object") { + return message.message.type || "default"; + } + + if (message.type) return message.type; + + if (typeof message.message === "string") { + const stringMessage = message.message.toLowerCase(); + const stringTypeMapping = { + 'accepted your friend request': 'friend_accepted', + 'game invitation': 'game_invitation', + 'chat room': 'chat_room_created' + }; + + for (const [pattern, type] of Object.entries(stringTypeMapping)) { + if (stringMessage.includes(pattern)) return type; + } + return "string_message"; + } + + return "default"; +} + +// Dynamic handler registration +function registerMessageHandler(messageType, handlerFunction) { + MESSAGE_HANDLERS[messageType] = handlerFunction; + console.log(`โœ… Registered new handler: ${messageType}`); +} + +// Message processor +function processMessage(message) { + const messageType = getMessageType(message); + const handler = MESSAGE_HANDLERS[messageType] || MESSAGE_HANDLERS['default']; + + console.log(`๐Ÿ”„ Processing message type: ${messageType}`); + handler(message); +} + +// DEMO STARTS HERE +console.log("๐Ÿš€ DEMO: Enhanced WebSocket Message Handler System\n"); + +console.log("๐Ÿ“‹ Current registered handlers:"); +console.log(Object.keys(MESSAGE_HANDLERS).join(", ")); +console.log(""); + +// Demo 1: Add a custom payment handler +console.log("๐Ÿ’ฐ Demo 1: Adding custom payment handler"); +registerMessageHandler('payment_notification', function(message) { + const amount = message.data?.amount || 'unknown'; + const currency = message.data?.currency || 'USD'; + showNotificationToast(`Payment received: ${amount} ${currency}`, 'success'); +}); + +// Demo 2: Add a custom weather handler +console.log("๐ŸŒค๏ธ Demo 2: Adding custom weather handler"); +registerMessageHandler('weather_alert', function(message) { + const condition = message.data?.condition || 'unknown'; + const location = message.data?.location || 'your area'; + showNotificationToast(`Weather alert: ${condition} in ${location}`, 'warning'); +}); + +console.log("\n๐Ÿ“จ Testing various message types:\n"); + +// Test different message formats +const testMessages = [ + { + name: "Structured friend request", + message: { + message: { type: 'friend_request', data: { user_id: 123, username: 'alice' } } + } + }, + { + name: "Direct type game invitation", + message: { + type: 'game_invitation', + data: { game_type: 'pong', inviter: 'bob' } + } + }, + { + name: "Custom payment notification", + message: { + type: 'payment_notification', + data: { amount: 100, currency: 'EUR' } + } + }, + { + name: "Custom weather alert", + message: { + type: 'weather_alert', + data: { condition: 'Storm warning', location: 'Milan' } + } + }, + { + name: "Legacy string message", + message: { message: "accepted your friend request" } + }, + { + name: "Unknown message type", + message: { type: 'unknown_type', data: { test: 'data' } } + } +]; + +testMessages.forEach((test, index) => { + console.log(`${index + 1}. ${test.name}:`); + processMessage(test.message); + console.log(""); +}); + +console.log("๐Ÿ“Š Final registered handlers:"); +console.log(Object.keys(MESSAGE_HANDLERS).join(", ")); + +console.log("\n๐ŸŽ‰ Demo completed! The system successfully:"); +console.log(" โœ… Processed multiple message formats"); +console.log(" โœ… Routed messages to correct handlers"); +console.log(" โœ… Added custom handlers dynamically"); +console.log(" โœ… Handled unknown message types gracefully"); + +console.log("\n๐Ÿ’ก To add a new handler in your code:"); +console.log(" registerMessageHandler('your_type', yourHandlerFunction);"); diff --git a/Front-End/index.html b/Front-End/index.html index f0558964..0098bad8 100644 --- a/Front-End/index.html +++ b/Front-End/index.html @@ -1,22 +1,22 @@ - + + Progetto Frontend - + - - - - + + + + - - + +
@@ -42,6 +43,6 @@ - + \ No newline at end of file diff --git a/Front-End/old-frontend/.gitignore b/Front-End/old-frontend/.gitignore deleted file mode 100644 index 4d29575d..00000000 --- a/Front-End/old-frontend/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/Front-End/old-frontend/README.md b/Front-End/old-frontend/README.md deleted file mode 100644 index 58beeacc..00000000 --- a/Front-End/old-frontend/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can't go back!** - -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. - -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/Front-End/old-frontend/package-lock.json b/Front-End/old-frontend/package-lock.json deleted file mode 100644 index 128f96fe..00000000 --- a/Front-End/old-frontend/package-lock.json +++ /dev/null @@ -1,19283 +0,0 @@ -{ - "name": "react-test", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "react-test", - "version": "0.1.0", - "license": "ISC", - "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/icons-material": "^6.1.9", - "@mui/joy": "^5.0.0-beta.48", - "@mui/lab": "^6.0.0-beta.17", - "@mui/material": "^6.1.9", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "bootstrap": "^5.3.3", - "framer-motion": "^11.12.0", - "lucide-react": "^0.456.0", - "react": "^18.3.1", - "react-bootstrap": "^2.10.5", - "react-dom": "^18.3.1", - "react-router-dom": "^6.26.2", - "react-scripts": "^5.0.1", - "react-use-websocket": "^4.9.0", - "web-vitals": "^2.1.4" - }, - "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.14.5", - "eslint": "^8.57.1", - "prettier": "^3.3.3", - "prettier-eslint": "^16.3.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", - "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz", - "integrity": "sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "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==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "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" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "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.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "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==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", - "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "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==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "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==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "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==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", - "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" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "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==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", - "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", - "dev": true, - "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" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", - "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", - "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz", - "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-flow": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz", - "integrity": "sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", - "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", - "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", - "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", - "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz", - "integrity": "sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-react-display-name": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/plugin-transform-react-jsx-development": "^7.25.9", - "@babel/plugin-transform-react-pure-annotations": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" - }, - "node_modules/@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==" - }, - "node_modules/@csstools/postcss-cascade-layers": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", - "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", - "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", - "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", - "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", - "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", - "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", - "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", - "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "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" - }, - "engines": { - "node": "^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.10" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "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.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/@emotion/cache": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", - "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/react": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", - "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" - }, - "node_modules/@emotion/styled": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz", - "integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", - "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", - "dependencies": { - "@floating-ui/utils": "^0.2.8" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.12", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", - "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.8" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", - "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead" - }, - "node_modules/@isaacs/cliui": { - "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" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "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" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "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" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "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" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "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" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/console/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/core": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/core/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/core/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/environment": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/environment/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/environment/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/fake-timers/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/globals/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/expect": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-diff": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-matcher-utils": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/reporters/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/test-result": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/test-sequencer": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/@jest/transform/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dependencies": { - "@jest/schemas": "^29.6.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" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" - }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", - "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.8.tgz", - "integrity": "sha512-DARxShbBsWz6azwkTmv05rR7rJfcd9cXFsQtbcr24A+5esQBSnK2N3cbg/izlvuMGxonfWaysz/ae+6Ij9RAHQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.9.tgz", - "integrity": "sha512-AzlhIT51rdjkZ/EcUV2dbhNkNSUHIqCnNoUxodpiTw8buyAUBd+qnxg5OBSuPpun/ZEdSSB8Q7Uyh6zqjiMsEQ==", - "dependencies": { - "@babel/runtime": "^7.26.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^6.1.9", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/joy": { - "version": "5.0.0-beta.48", - "resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.48.tgz", - "integrity": "sha512-OhTvjuGl9I5IvpBr0BQyDehIW/xb2yteW6YglHJMdOb/279nItn76X1NBtPV9ImldNlBjReGwvpOXmBTTGER9w==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.16.1", - "@mui/system": "^5.16.1", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.1", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab": { - "version": "6.0.0-beta.17", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.17.tgz", - "integrity": "sha512-Ls1pIuYi5D9wq9mUwncky6CWokd6CCqQDCxXbm0TP0e7ksU5DcCPUZXBmTWQgbkldLu14aUXbJHyts63L0rycQ==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/base": "5.0.0-beta.63", - "@mui/system": "^6.1.9", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.9", - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material": "^6.1.9", - "@mui/material-pigment-css": "^6.1.9", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/base": { - "version": "5.0.0-beta.63", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.63.tgz", - "integrity": "sha512-W6aIqKP9X8VUX0KhSnYWo2+5C7MnKV1IhYVd517L/apvfkVq5KaTdlnxSBVwnaWt46whayVgQ/9KXwUVCXp6+w==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@floating-ui/react-dom": "^2.1.1", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.9", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/private-theming": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.9.tgz", - "integrity": "sha512-7aum/O1RquBYhfwL/7egDyl9GqJgPM6hoJDFFBbhF6Sgv9yI9v4w3ArKUkuVvR0CtVj4NXRVMKEioh1bjUzvuA==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/styled-engine": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.9.tgz", - "integrity": "sha512-xynSLlJRxHLzSfQaiDjkaTx8LiFb9ByVa7aOdwFnTxGWFMY1F+mkXwAUY4jDDE+MAxkWxlzzQE0wOohnsxhdQg==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/system": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.9.tgz", - "integrity": "sha512-8x+RucnNp21gfFYsklCaZf0COXbv3+v0lrVuXONxvPEkESi2rwLlOi8UPJfcz6LxZOAX3v3oQ7qw18vnpgueRg==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.9", - "@mui/styled-engine": "^6.1.9", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/utils": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.9.tgz", - "integrity": "sha512-N7uzBp7p2or+xanXn3aH2OTINC6F/Ru/U8h6amhRZEev8bJhKN86rIDIoxZZ902tj+09LXtH83iLxFMjMHyqNA==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.9.tgz", - "integrity": "sha512-NwqIN0bdsgzSbZd5JFcC+2ez0XW/XNs8uiV2PDHrqQ4qf/FEasFJG1z6g8JbCN0YlTrHZekVb17X0Fv0qcYJfQ==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.1.9", - "@mui/system": "^6.1.9", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.9", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^18.3.1", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.9", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.9.tgz", - "integrity": "sha512-TWqj7b1w5cmSz4H/uf+y2AHxAH4ldPR7D2bz0XVyn60GCAo/zRbRPx7cF8gTs/i7CiYeHzV6dtat0VpMwOtolw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material/node_modules/@mui/private-theming": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.9.tgz", - "integrity": "sha512-7aum/O1RquBYhfwL/7egDyl9GqJgPM6hoJDFFBbhF6Sgv9yI9v4w3ArKUkuVvR0CtVj4NXRVMKEioh1bjUzvuA==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.9.tgz", - "integrity": "sha512-xynSLlJRxHLzSfQaiDjkaTx8LiFb9ByVa7aOdwFnTxGWFMY1F+mkXwAUY4jDDE+MAxkWxlzzQE0wOohnsxhdQg==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.9.tgz", - "integrity": "sha512-8x+RucnNp21gfFYsklCaZf0COXbv3+v0lrVuXONxvPEkESi2rwLlOi8UPJfcz6LxZOAX3v3oQ7qw18vnpgueRg==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.9", - "@mui/styled-engine": "^6.1.9", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/utils": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.9.tgz", - "integrity": "sha512-N7uzBp7p2or+xanXn3aH2OTINC6F/Ru/U8h6amhRZEev8bJhKN86rIDIoxZZ902tj+09LXtH83iLxFMjMHyqNA==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.8.tgz", - "integrity": "sha512-3Vl9yFVLU6T3CFtxRMQTcJ60Ijv7wxQi4yjH92+9YXcsqvVspeIYoocqNoIV/1bXGYfyWu5zrCmwQVHaGY7bug==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.8", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.8.tgz", - "integrity": "sha512-OFdgFf8JczSRs0kvWGdSn0ZeXxWrY0LITDPJ/nAtLEvUUTyrlFaO4il3SECX8ruzvf1VnAxHx4M/4mX9oOn9yA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.8.tgz", - "integrity": "sha512-L32TaFDFpGIi1g6ysRtmhc9zDgrlxDXu3NlrGE8gAsQw/ziHrPdr0PNr20O0POUshA1q14W4dNZ/z0Nx2F9lhA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.8", - "@mui/styled-engine": "^5.16.8", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.8", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.8.tgz", - "integrity": "sha512-P/yb7BSWallQUeiNGxb+TM8epHteIUC8gzNTdPV2VfKhVY/EnGliHgt5np0GPkjQ7EzwDi/+gBevrAJtf+K94A==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", - "@types/prop-types": "^15.7.12", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "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" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "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" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "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" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", - "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", - "dependencies": { - "ansi-html": "^0.0.9", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <5.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", - "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@remix-run/router": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", - "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.1.tgz", - "integrity": "sha512-qghR21ynHiUrpcIkKCoKYB+3rJtezY5Y7ikrwradCL+7hZHdQ2Ozc5ffxtpmpahoAGgc31gyXaSx2sXXaThmqA==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@popperjs/core": "^2.11.8", - "@react-aria/ssr": "^3.5.0", - "@restart/hooks": "^0.5.0", - "@types/warning": "^3.0.3", - "dequal": "^2.0.3", - "dom-helpers": "^5.2.0", - "uncontrollable": "^8.0.4", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - } - }, - "node_modules/@restart/ui/node_modules/@restart/hooks": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.0.tgz", - "integrity": "sha512-wS+h6IusJCPjTkmOOrRZxIPICD/mtFA3PRZviutoM23/b7akyDGfZF/WS+nIFk27u7JDhPE2+0GBdZxjSqHZkg==", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui/node_modules/uncontrollable": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", - "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", - "peerDependencies": { - "react": ">=16.14.0" - } - }, - "node_modules/@rollup/plugin-babel": { - "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" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "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" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "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" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "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" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", - "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" - }, - "node_modules/@sinonjs/commons": { - "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" - } - }, - "node_modules/@sinonjs/fake-timers": { - "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" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "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" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-preset": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-svgo": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/webpack": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", - "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" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@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": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@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": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "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": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.12", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", - "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@types/express": { - "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": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", - "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "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==" - }, - "node_modules/@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==" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" - }, - "node_modules/@types/q": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", - "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==" - }, - "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" - }, - "node_modules/@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==" - }, - "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" - }, - "node_modules/@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": "*" - } - }, - "node_modules/@types/serve-index": { - "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": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "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": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" - }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", - "dependencies": { - "@types/jest": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" - }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" - }, - "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" - }, - "node_modules/@typescript-eslint/experimental-utils": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead" - }, - "node_modules/accepts": { - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "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" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "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" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/agent-base": { - "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" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "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" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-escapes": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", - "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-html-community": { - "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==", - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "node_modules/anymatch": { - "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" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/aria-query": { - "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" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz", - "integrity": "sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/ast-types-flow": { - "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==" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", - "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/babel-jest": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-jest/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/babel-loader": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", - "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.4", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "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" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-plugin-istanbul": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "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" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/babel-plugin-named-asset-import": { - "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==", - "peerDependencies": { - "@babel/core": "^7.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "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==" - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app": { - "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" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/batch": { - "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==" - }, - "node_modules/bfj": { - "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" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "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.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "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" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "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" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "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" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "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==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-api": { - "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" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001684", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", - "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/check-types": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", - "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "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" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==" - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, - "node_modules/clean-css": { - "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" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui": { - "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" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/coa": { - "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" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/node_modules/ansi-styles": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/chalk": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/color-convert": { - "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" - } - }, - "node_modules/coa/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/coa/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/coa/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/supports-color": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" - }, - "node_modules/combined-stream": { - "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" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/common-tags": { - "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==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/compressible": { - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.0.2", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "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" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confusing-browser-globals": { - "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==" - }, - "node_modules/connect-history-api-fallback": { - "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==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/core-js": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", - "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", - "dependencies": { - "browserslist": "^4.24.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.39.0.tgz", - "integrity": "sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-blank-pseudo": { - "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" - }, - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-declaration-sorter": { - "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==", - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "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" - }, - "bin": { - "css-has-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "bin": { - "css-prefers-color-scheme": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select-base-adapter": { - "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==" - }, - "node_modules/css-tree": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" - }, - "node_modules/cssdb": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", - "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ] - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "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==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" - }, - "node_modules/cssstyle": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" - }, - "node_modules/data-urls": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-is": { - "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==" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "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" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, - "node_modules/detect-port-alt": { - "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" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "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" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/dns-packet": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" - }, - "node_modules/dom-converter": { - "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" - } - }, - "node_modules/dom-helpers": { - "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" - } - }, - "node_modules/dom-serializer": { - "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" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "deprecated": "Use your platform's native DOMException instead", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/domhandler": { - "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" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "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" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "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" - } - }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.67", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz", - "integrity": "sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==" - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "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" - } - }, - "node_modules/error-stack-parser": { - "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" - } - }, - "node_modules/es-abstract": { - "version": "1.23.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", - "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "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==" - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", - "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.3", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "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" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "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.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "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.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "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.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-react-app": { - "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" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/eslint-plugin": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/parser": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/scope-manager": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/typescript-estree": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/visitor-keys": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-react-app/node_modules/eslint-plugin-jest": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-config-react-app/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-import-resolver-node": { - "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" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-flowtype": { - "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" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "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.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", - "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.1.0", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "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" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-testing-library": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "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==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-webpack-plugin": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/supports-color": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/eslint/node_modules/globals": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "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" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "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" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/fast-deep-equal": { - "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==" - }, - "node_modules/fast-glob": { - "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" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "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==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "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" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "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" - } - }, - "node_modules/file-entry-cache": { - "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" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-loader": { - "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" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/filelist": { - "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" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "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" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "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" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/find-cache-dir": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "node_modules/find-up": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "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" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "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" - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "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" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "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" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", - "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.12.0.tgz", - "integrity": "sha512-gZaZeqFM6pX9kMVti60hYAa75jGpSsGYWAHbBfIkuHN7DkVHVkxSxeNYnrGmHuM0zPkWTzQx10ZT+fDjn7N4SA==", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "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" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "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==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "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==" - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "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" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "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==" - }, - "node_modules/global-modules": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" - }, - "node_modules/gzip-size": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" - }, - "node_modules/harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hoist-non-react-statics": { - "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" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/hpack.js": { - "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" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "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" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "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" - } - }, - "node_modules/html-encoding-sniffer": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ] - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "node_modules/html-minifier-terser": { - "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" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", - "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" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" - }, - "node_modules/http-errors": { - "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" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" - }, - "node_modules/http-proxy": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", - "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" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" - }, - "node_modules/identity-obj-proxy": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invariant": { - "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" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-async-function": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", - "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "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==" - }, - "node_modules/is-regex": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", - "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" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest": { - "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" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-circus": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-circus/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/expect": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-diff": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-cli": { - "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" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-cli/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-cli/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-config": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-config/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-config/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-each/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-environment-jsdom/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-environment-node/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-haste-map/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-haste-map/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-jasmine2/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/expect": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-diff": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-matcher-utils": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-leak-detector": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "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": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-mock/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-resolve/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-resolve/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-runner/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-runtime/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-snapshot/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/expect": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-diff": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-validate/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watch-typeahead": { - "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" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "jest": "^27.0.0 || ^28.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/schemas": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" - }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { - "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==", - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format": { - "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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length": { - "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" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", - "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/jest-watcher": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@jest/types": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-watcher/node_modules/jest-util": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-worker": { - "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" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "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" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "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" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-parse-even-better-errors": { - "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==" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "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==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "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==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "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" - } - }, - "node_modules/jsonpath": { - "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" - } - }, - "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "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" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "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" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==" - }, - "node_modules/language-tags": { - "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" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "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" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/loglevel-colored-level-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", - "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "loglevel": "^1.4.1" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/loose-envify": { - "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" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "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" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.456.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.456.0.tgz", - "integrity": "sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "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" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mz": { - "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" - } - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "node_modules/natural-compare-lite": { - "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==" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/no-case": { - "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_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "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" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.15.tgz", - "integrity": "sha512-NWK6i6f70uRJgsqyNTNMQ/JbCzd8zQoFnOKdZpXqfyq0YFBQgCTFjDIXfAmkEJyV+/GzSrCCrz2iVJmJK9OX8w==" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" - }, - "node_modules/on-finished": { - "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" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "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==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "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" - } - }, - "node_modules/onetime": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "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" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, - "node_modules/param-case": { - "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" - } - }, - "node_modules/parent-module": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "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" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "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" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "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" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-browser-comments": { - "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==", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "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" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "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" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-color-hex-alpha": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-colormin": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-custom-media": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-custom-properties": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-custom-selectors": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-env-function": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-image-set-function": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-import": { - "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" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "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==", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/postcss-loader": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", - "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-values": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-normalize": { - "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": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "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==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-opacity-percentage": { - "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==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-ordered-values": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-overflow-shorthand": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-page-break": { - "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==", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-preset-env": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-reduce-initial": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "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==", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "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" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/postcss-svgo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "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" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-eslint": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", - "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", - "dev": true, - "dependencies": { - "@typescript-eslint/parser": "^6.7.5", - "common-tags": "^1.4.0", - "dlv": "^1.1.0", - "eslint": "^8.7.0", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.0", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^3.0.1", - "pretty-format": "^29.7.0", - "require-relative": "^0.8.7", - "typescript": "^5.2.2", - "vue-eslint-parser": "^9.1.0" - }, - "engines": { - "node": ">=16.10.0" - }, - "peerDependencies": { - "prettier-plugin-svelte": "^3.0.0", - "svelte-eslint-parser": "*" - }, - "peerDependenciesMeta": { - "prettier-plugin-svelte": { - "optional": true - }, - "svelte-eslint-parser": { - "optional": true - } - } - }, - "node_modules/prettier-eslint/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prettier-eslint/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/prettier-eslint/node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "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" - } - }, - "node_modules/pretty-format": { - "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" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/process-nextick-args": { - "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==" - }, - "node_modules/promise": { - "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" - } - }, - "node_modules/prompts": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "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" - } - }, - "node_modules/prop-types-extra": { - "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" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, - "node_modules/prop-types-extra/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/proxy-addr": { - "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" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "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==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.14.0.tgz", - "integrity": "sha512-Syk1bnf6fRZ9wQs03AtKJHcM12cKbOLo9L8JtCCdYj5/DTsHmTyXM4BK5ouWeG2P6kZ4nmFvuNTdtaqfobCOCg==", - "dependencies": { - "punycode": "^2.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/raf": { - "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" - } - }, - "node_modules/randombytes": { - "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" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-app-polyfill": { - "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" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-app-polyfill/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "node_modules/react-bootstrap": { - "version": "2.10.6", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.6.tgz", - "integrity": "sha512-fNvKytSp0nHts1WRnRBJeBEt+I9/ZdrnhIjWOucEduRNvFRU1IXjZueDdWnBiqsTSJ7MckQJi9i/hxGolaRq+g==", - "dependencies": { - "@babel/runtime": "^7.24.7", - "@restart/hooks": "^0.4.9", - "@restart/ui": "^1.9.0", - "@types/react-transition-group": "^4.4.6", - "classnames": "^2.3.2", - "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.5", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "@types/react": ">=16.14.8", - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dev-utils": { - "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" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-error-overlay": { - "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==" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/react-lifecycles-compat": { - "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==" - }, - "node_modules/react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", - "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", - "dependencies": { - "@remix-run/router": "1.21.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", - "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", - "dependencies": { - "@remix-run/router": "1.21.0", - "react-router": "6.28.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "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.1", - "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.1", - "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" - }, - "bin": { - "react-scripts": "bin/react-scripts.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - }, - "peerDependencies": { - "react": ">= 16", - "typescript": "^3.2.1 || ^4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/react-scripts/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/react-transition-group": { - "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" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/react-use-websocket": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.11.1.tgz", - "integrity": "sha512-39e8mK2a2A1h8uY3ePF45b2q0vwMOmaEy7J5qEhQg4n7vYa5oDLmqutG36kZQgAQ/3KCZS0brlGRbbZJ0+zfKQ==" - }, - "node_modules/read-cache": { - "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" - } - }, - "node_modules/readable-stream": { - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recursive-readdir": { - "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" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/redent": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz", - "integrity": "sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "which-builtin-type": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", - "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", - "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "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" - } - }, - "node_modules/require-directory": { - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/resolve": { - "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" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url-loader": { - "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" - }, - "engines": { - "node": ">=8.9" - }, - "peerDependencies": { - "rework": "1.0.1", - "rework-visit": "1.0.0" - }, - "peerDependenciesMeta": { - "rework": { - "optional": true - }, - "rework-visit": { - "optional": true - } - } - }, - "node_modules/resolve-url-loader/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/resolve-url-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/resolve-url-loader/node_modules/postcss": { - "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" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "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" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "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" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/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==" - }, - "node_modules/sass-loader": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "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" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" - }, - "node_modules/selfsigned": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "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" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "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" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "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" - } - }, - "node_modules/source-list-map": { - "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==" - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "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" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/source-map-support": { - "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" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead" - }, - "node_modules/spdy": { - "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" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "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" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" - }, - "node_modules/stack-utils": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "node_modules/static-eval": { - "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" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "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" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/levn": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string_decoder": { - "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" - } - }, - "node_modules/string-length": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" - }, - "node_modules/string-width": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-object": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-comments": { - "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==", - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "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==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/stylehacks": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "node_modules/sucrase": { - "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" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "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" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "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" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/ansi-styles": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/argparse": { - "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" - } - }, - "node_modules/svgo/node_modules/chalk": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/color-convert": { - "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" - } - }, - "node_modules/svgo/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/svgo/node_modules/css-select": { - "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" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/dom-serializer": { - "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" - } - }, - "node_modules/svgo/node_modules/domutils": { - "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" - } - }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "node_modules/svgo/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/svgo/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/js-yaml": { - "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" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/svgo/node_modules/nth-check": { - "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" - } - }, - "node_modules/svgo/node_modules/supports-color": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/symbol-tree": { - "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==" - }, - "node_modules/tailwindcss": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", - "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link": { - "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" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "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==" - }, - "node_modules/test-exclude": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "node_modules/thenify": { - "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" - } - }, - "node_modules/thenify-all": { - "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" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" - }, - "node_modules/to-regex-range": { - "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" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "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==" - }, - "node_modules/tsconfig-paths": { - "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" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "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" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/tsutils": { - "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" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "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" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/type-check": { - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", - "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray-to-buffer": { - "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" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uncontrollable": { - "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" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, - "node_modules/underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "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" - } - }, - "node_modules/url-parse": { - "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" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/util.promisify": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "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" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "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" - } - }, - "node_modules/warning": { - "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" - } - }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "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" - } - }, - "node_modules/web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" - }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "engines": { - "node": ">=10.4" - } - }, - "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "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.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "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.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-manifest-plugin": { - "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" - }, - "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^4.44.2 || ^5.47.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { - "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" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-sources": { - "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==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "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" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/websocket-driver": { - "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" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-encoding": { - "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" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "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" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, - "node_modules/whatwg-url": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/which": { - "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" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.0.tgz", - "integrity": "sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==", - "dependencies": { - "call-bind": "^1.0.7", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.1.0", - "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.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", - "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-background-sync": { - "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" - } - }, - "node_modules/workbox-broadcast-update": { - "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" - } - }, - "node_modules/workbox-build": { - "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" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "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" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/workbox-build/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/workbox-build/node_modules/fs-extra": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/workbox-build/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/workbox-build/node_modules/source-map": { - "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" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-build/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/workbox-build/node_modules/webidl-conversions": { - "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==" - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "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" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==" - }, - "node_modules/workbox-expiration": { - "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" - } - }, - "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", - "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-navigation-preload": { - "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" - } - }, - "node_modules/workbox-precaching": { - "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" - } - }, - "node_modules/workbox-range-requests": { - "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" - } - }, - "node_modules/workbox-recipes": { - "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" - } - }, - "node_modules/workbox-routing": { - "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" - } - }, - "node_modules/workbox-strategies": { - "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" - } - }, - "node_modules/workbox-streams": { - "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" - } - }, - "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==" - }, - "node_modules/workbox-webpack-plugin": { - "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" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "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" - } - }, - "node_modules/workbox-window": { - "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" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "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" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "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==" - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/Front-End/old-frontend/package.json b/Front-End/old-frontend/package.json deleted file mode 100644 index 3cb4302f..00000000 --- a/Front-End/old-frontend/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "react-test", - "version": "0.1.0", - "private": true, - "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@mui/icons-material": "^6.1.9", - "@mui/joy": "^5.0.0-beta.48", - "@mui/lab": "^6.0.0-beta.17", - "@mui/material": "^6.1.9", - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "bootstrap": "^5.3.3", - "framer-motion": "^11.12.0", - "lucide-react": "^0.456.0", - "react": "^18.3.1", - "react-bootstrap": "^2.10.5", - "react-dom": "^18.3.1", - "react-router-dom": "^6.26.2", - "react-scripts": "^5.0.1", - "react-use-websocket": "^4.9.0", - "web-vitals": "^2.1.4" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).", - "main": "index.js", - "author": "", - "license": "ISC", - "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.14.5", - "eslint": "^8.57.1", - "prettier": "^3.3.3", - "prettier-eslint": "^16.3.0" - } -} diff --git a/Front-End/old-frontend/public/favicon.ico b/Front-End/old-frontend/public/favicon.ico deleted file mode 100644 index a11777cc..00000000 Binary files a/Front-End/old-frontend/public/favicon.ico and /dev/null differ diff --git a/Front-End/old-frontend/public/index.html b/Front-End/old-frontend/public/index.html deleted file mode 100644 index ed1fb151..00000000 --- a/Front-End/old-frontend/public/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - React App - - - -
- - - diff --git a/Front-End/old-frontend/public/logo192.png b/Front-End/old-frontend/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/Front-End/old-frontend/public/logo192.png and /dev/null differ diff --git a/Front-End/old-frontend/public/logo512.png b/Front-End/old-frontend/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/Front-End/old-frontend/public/logo512.png and /dev/null differ diff --git a/Front-End/old-frontend/public/manifest.json b/Front-End/old-frontend/public/manifest.json deleted file mode 100644 index 080d6c77..00000000 --- a/Front-End/old-frontend/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/Front-End/old-frontend/public/robots.txt b/Front-End/old-frontend/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/Front-End/old-frontend/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/Front-End/old-frontend/src/App.css b/Front-End/old-frontend/src/App.css deleted file mode 100644 index 8b137891..00000000 --- a/Front-End/old-frontend/src/App.css +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Front-End/old-frontend/src/App.jsx b/Front-End/old-frontend/src/App.jsx deleted file mode 100644 index f2525bb0..00000000 --- a/Front-End/old-frontend/src/App.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; -import './App.css'; -import Navbar from './components/Navbar/Navbar'; -import Login from './components/Login/Login'; -import Register from './components/Register/Register'; -import Home from './components/Home/Home'; -import ExternalRedirect from './components/ExternalRedirect/ExternalRedirect'; - -function App() { - return ( - -
- - } /> - } /> - } /> - } /> - } /> - -
-
- ); -} - -export default App; \ No newline at end of file diff --git a/Front-End/old-frontend/src/App.test.js b/Front-End/old-frontend/src/App.test.js deleted file mode 100644 index 1f03afee..00000000 --- a/Front-End/old-frontend/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/Front-End/old-frontend/src/components/Button/Button.jsx b/Front-End/old-frontend/src/components/Button/Button.jsx deleted file mode 100644 index 79754b22..00000000 --- a/Front-End/old-frontend/src/components/Button/Button.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import "./index.css"; - -export default function Button({ onclick, text, type, className, image }) -{ - return ( - - ); -} diff --git a/Front-End/old-frontend/src/components/Button/index.css b/Front-End/old-frontend/src/components/Button/index.css deleted file mode 100644 index 2466d9f4..00000000 --- a/Front-End/old-frontend/src/components/Button/index.css +++ /dev/null @@ -1,21 +0,0 @@ -.button -{ - margin: 1px; - border-radius: 6px; - height: 40px; - width: 100px; - background-color: #007bff; - border-style: none; - box-shadow: rgba(0, 0, 0, .2) 0 3px 5px -1px,rgba(0, 0, 0, .14) 0 6px 10px 0,rgba(0, 0, 0, .12) 0 1px 18px 0; - padding: 2px 24px; - width: auto; -} - -.button:hover { - background-color: #0056b3; -} - -.button:active { - box-shadow: 0 4px 4px 0 rgb(60 64 67 / 30%), 0 8px 12px 6px rgb(60 64 67 / 15%); - outline: none; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/AddChat.css b/Front-End/old-frontend/src/components/Chat/AddChat.css deleted file mode 100644 index 64985cd3..00000000 --- a/Front-End/old-frontend/src/components/Chat/AddChat.css +++ /dev/null @@ -1,68 +0,0 @@ -.chat { - display: flex; - flex-direction: column; - border-radius: 8px; - justify-content: space-between; - background: #f5f5f5f0; - height: fit-content; -} - -.separated { - margin: 20px; /* Aggiunge uno spazio tra i div */ -} - -.clickable-div { - background: none; - border: none; - - cursor: pointer; - width: 100%; - text-align: left; -} - -.clickable-div .chat-header { - padding: 10px; - border-radius: 5px 5px 0 0; -} - -.expanded { - height: 400px; /* Altezza espansa del div */ -} - - - -.fixed-footer { - display: flex; - flex-direction: line; - padding: 10px; -} - -.chat-button { - padding: 20px; -} - -.add-chat { - padding: 16px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.add-chat input { - padding: 8px; - border: 1px solid var(--border); - border-radius: 4px; -} - -.add-chat button { - padding: 8px; - background-color: #3b82f6; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.add-chat button:hover { - background-color: #2563eb; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/AddChat.jsx b/Front-End/old-frontend/src/components/Chat/AddChat.jsx deleted file mode 100644 index 59dcaca8..00000000 --- a/Front-End/old-frontend/src/components/Chat/AddChat.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import "./AddChat.css"; -import React, { useState } from "react"; -import Input from "../Input/Input.jsx"; -import Button from "../Button/Button.jsx"; -import { getCookie } from "../Cookie.jsx"; - -export default function AddChat() { - const [isExpanded, setIsExpanded] = useState(false); - const [room_name, setRoomName] = useState(""); - const [room_description, setRoomDescription] = useState(""); - const [user_ids, setUsers] = useState(""); - const [creator] = useState(localStorage.getItem('user_id')); - - const handleExpand = () => { - setIsExpanded(!isExpanded); - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - let userIdsArray = user_ids.split(',').map(id => id.trim()).filter(id => id); - if (!userIdsArray.includes(creator)) { - userIdsArray.push(creator); - } - - console.log('Utenti:', userIdsArray); - - try { - const response = await fetch('http://localhost:8001/chat/chat_rooms/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': getCookie('csrftoken'), - 'Authorization': `Bearer ${localStorage.getItem('token')}`, - }, - body: JSON.stringify({ room_name, room_description, creator, users: userIdsArray }), - }); - - if (response.ok) { - const data = await response.json(); - console.log('Chat room creata:', data); - } else { - const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData); - } - } catch (error) { - console.error('Errore nella richiesta:', error); - } - - //post su path('create_channel_group/' room_name = request.data.get('room_name') - // user_ids = request.data.get('user_ids', []) - // try { - // const response = await fetch('http://localhost:8001/chat/create_channel_group/', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // 'X-CSRFToken': getCookie('csrftoken'), - // 'Authorization': `Bearer ${localStorage.getItem('token')}`, - // }, - // body: JSON.stringify({ room_id, user_ids }), - // }); - - // if (response.ok) { - // const data = await response.json(); - // console.log('Gruppo creato:', data); - // } else { - // const errorData = await response.json(); - // console.error('Errore nella risposta del server:', errorData); - // } - // } catch (error) { - // console.error('Errore nella richiesta:', error); - // } - }; - - {/*
- - - - -
*/} - - return ( -
- setRoomName(e.target.value)} - > - setRoomDescription(e.target.value)} - > - setUsers(e.target.value)} - > - -
- ); - {/*
-
- -
- {isExpanded && (*/} - {/*)} -
*/} -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/Chat.css b/Front-End/old-frontend/src/components/Chat/Chat.css deleted file mode 100644 index 220eea22..00000000 --- a/Front-End/old-frontend/src/components/Chat/Chat.css +++ /dev/null @@ -1,99 +0,0 @@ -.chat { - display: flex; - flex-direction: column; - border-radius: 8px; - justify-content: space-between; - background: #f5f5f5; - height: fit-content; -} - -.dates { - font-size: 12px; - display: flex; - justify-content: center; - padding-bottom: 10px; -} - -.separated { - margin: 20px; - /* Aggiunge uno spazio tra i div */ -} - -.clickable-div { - background: none; - border: none; - - cursor: pointer; - width: 100%; - text-align: left; -} - -.clickable-div .chat-header { - padding: 10px; - border-radius: 5px 5px 0 0; -} - -.expanded { - height: 600px; - /* Altezza espansa del div */ -} - -.scrollable-content { - height: 300px; - display: flex; - flex-direction: column; - flex: 1; - padding: 10px; - overflow-y: scroll; - /* Aggiunge una barra di scorrimento se il contenuto supera l'altezza massima */ -} - -.fixed-footer { - display: flex; - flex-direction: line; - padding: 10px; -} - -.chat-form { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; -} - -.chat-button { - padding: 20px; -} - -.chats-input { - display: flex; - align-items: center; - justify-content: space-between; - background-color: #f3f4f6; - padding: 16px; - gap: 16px; - } - - .chats-input input { - flex-grow: 1; - padding: 8px; - border: 1px solid var(--border); - border-radius: 4px; - } - - .chats-input button { - background-color: #3b82f6; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - } - - .chats-input button:hover { - background-color: #2563eb; - } \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/Chat.jsx b/Front-End/old-frontend/src/components/Chat/Chat.jsx deleted file mode 100644 index 55561d05..00000000 --- a/Front-End/old-frontend/src/components/Chat/Chat.jsx +++ /dev/null @@ -1,136 +0,0 @@ -import "./Chat.css"; -import React, { useState, useEffect, useRef } from "react"; -import useWebSocket from "react-use-websocket"; -import Input from "../Input/Input"; -import Button from "../Button/Button"; -import { ChatBubble } from "../ExpandableSidebar/ChatBubble"; -import { Send } from "lucide-react"; - -// to test the chat you need to create 2 new chat to have the roomID that exist (hardcoded in this file) - -export default function Chat({ roomID, isSingleChat }) { - console.log("Chat", roomID, isSingleChat); - const [message, setMessage] = useState(""); - const [chat, setChat] = useState([]); - const token = localStorage.getItem("token"); - const wsUrl = useRef(`ws://127.0.0.1:8001/ws/chat/${roomID}/?token=${token}`).current; - - const { sendMessage, lastMessage, readyState, getWebSocket } = useWebSocket( - wsUrl, - { - onOpen: () => { - console.log("WebSocket connection opened"); - }, - onClose: () => console.log("WebSocket connection closed"), - onError: (e) => console.error("WebSocket error:", e), - }, - ); - - useEffect(() => { - console.log("WebSocket URL:", wsUrl); - - if (lastMessage !== null) { - const data = JSON.parse(lastMessage.data); - console.log("Messaggio ricevuto:", data); - setChat((prevChat) => [...prevChat, data]); - } - - return () => { - const socket = getWebSocket(); - //if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { - // console.log('Chiudendo la connessione WebSocket'); - // socket.close(); - //} - }; - }, [lastMessage, getWebSocket]); - - useEffect(() => { - const fetchMessages = async () => { - try { - const response = await fetch( - `http://127.0.0.1:8001/chat/chat_rooms/${roomID}/get_message/`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": localStorage.getItem("csrftoken"), - "Authorization": `Bearer ${token}`, - }, - } - ); - if (response.ok) { - const data = await response.json(); - setChat(data); - } else { - console.error( - "Errore nella risposta del server:", - response.statusText - ); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } - }; - - fetchMessages(); - }, [roomID]); - - const handleSendMessage = (e) => { - e.preventDefault(); - const socket = getWebSocket(); - console.log("Stato della connessione WebSocket:", socket.readyState); - if (socket && socket.readyState === WebSocket.OPEN) { - const messageData = { - type: "chat_message", - room_id: roomID, - message: message, - timestamp: new Date().toISOString(), - sender: localStorage.getItem("user_username"), - }; - sendMessage(JSON.stringify(messageData)); - setMessage(""); - } else { - alert("Connessione WebSocket non attiva"); - } - }; - - let lastDate = null; - - return ( -
-
-
- {chat.map((msg, index) => { - const msgDate = new Date(msg.timestamp).toLocaleDateString(); - const showDate = lastDate !== msgDate; - if (showDate) { - lastDate = msgDate; - } - return ( -
- {showDate &&
{msgDate}
} - -
- ); - })} -
-
-
- setMessage(e.target.value)} - placeholder="Type a message" - /> - -
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/Message/Message.css b/Front-End/old-frontend/src/components/Chat/Message/Message.css deleted file mode 100644 index 113b7e65..00000000 --- a/Front-End/old-frontend/src/components/Chat/Message/Message.css +++ /dev/null @@ -1,22 +0,0 @@ -.message { - display: flex; - align-items: flex-start; - margin: 10px 0; -} - -.message.personal { - justify-content: flex-end; -} - -.propic { - background-color: #5d5d5d; - width: 30px; - height: 30px; - min-width: 30px; - border-radius: 100%; - margin-right: 10px; -} - -p { - margin: 0; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/Message/Message.jsx b/Front-End/old-frontend/src/components/Chat/Message/Message.jsx deleted file mode 100644 index 5ddeb6a8..00000000 --- a/Front-End/old-frontend/src/components/Chat/Message/Message.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import "./Message.css"; -import React from "react"; - -export default function Message({ message }) { - const isPersonalMessage = message.personal_message === true; - - return ( -
- {!isPersonalMessage && (
)} -
-

{message.sender_name}

-

{message.text}

-
-
- ); -} diff --git a/Front-End/old-frontend/src/components/Chat/SideChats.css b/Front-End/old-frontend/src/components/Chat/SideChats.css deleted file mode 100644 index fb374c59..00000000 --- a/Front-End/old-frontend/src/components/Chat/SideChats.css +++ /dev/null @@ -1,15 +0,0 @@ -.sidechat { - width: 100%; - height: calc(100vh - 50px); - display: flex; - flex-direction: column; - justify-content: flex-start; - min-width: 300px; - max-width: 400px; - overflow-y: auto; -} - -.sidechat-input { - margin: 20px; - width: calc(100% - 65px); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Chat/SideChats.jsx b/Front-End/old-frontend/src/components/Chat/SideChats.jsx deleted file mode 100644 index 44003c53..00000000 --- a/Front-End/old-frontend/src/components/Chat/SideChats.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import "./SideChats.css"; -import React, {useEffect, useState } from "react"; -import Chat from "./Chat.jsx"; -import AddChat from "./AddChat.jsx"; -import Input from "../Input/Input.jsx"; -import { getCookie } from "../Cookie.jsx"; - -//restituisce tutte le chatrooms in cui l'utente รจ presente -const getChatRooms = async () => { - const user_id = localStorage.getItem("user_id"); - const token = localStorage.getItem('token'); - console.log("user_id in sidechat: ", user_id); - console.log("Token inviato nell'header Authorization:", token); - console.log(`http://localhost:8001/chat/chat_rooms/getchat/?users=${user_id}`); - try { - const response = await fetch( - `http://localhost:8001/chat/chat_rooms/getchat/?user_id=${user_id}`, - { - method: "GET", - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': getCookie('csrftoken'), - 'Authorization': `Bearer ${token}`, - }, - } - ); - - if (response.ok) { - const data = await response.json(); - console.log("Risposta dal server:", data); - localStorage.setItem("chat_rooms", JSON.stringify(data)); - return data; - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - return null; - } - } catch (error) { - console.error("Errore nella richiesta:", error); - return null; - } -}; - -export default function SideChats() { - const [chatRooms, setChatRooms] = useState([]); - - useEffect(() => { - const fetchChatRooms = async () => { - const data = await getChatRooms(); - if (data) { - const storedChatRooms = JSON.parse(localStorage.getItem("chat_rooms")) || []; - setChatRooms(storedChatRooms); - } - else { - console.log("Dati non ricevuti:", data); - setChatRooms([]); - } - }; - - fetchChatRooms(); - }, []); - - return ( -
- - - {chatRooms.map((room) => ( - - ))} -
- ); -} diff --git a/Front-End/old-frontend/src/components/Cookie.jsx b/Front-End/old-frontend/src/components/Cookie.jsx deleted file mode 100644 index 33641c77..00000000 --- a/Front-End/old-frontend/src/components/Cookie.jsx +++ /dev/null @@ -1,15 +0,0 @@ - -export const getCookie = (name) => { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; -}; \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.css b/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.css deleted file mode 100644 index 66349367..00000000 --- a/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.css +++ /dev/null @@ -1,85 +0,0 @@ -.chat-bubble { - display: flex; - align-items: flex-start; - margin-bottom: 10px; - max-width: 60%; - position: relative; -} - -.chat-bubble.true { - flex-direction: row-reverse; - align-self: flex-end; - margin-left: auto; -} - -.chat-bubble.false { - align-self: flex-start; - margin-right: auto; -} - -.avatar { - height: 40px; - width: 40px; - margin-right: 10px; - min-width: 40px; -} - -.chat-bubble.true .avatar { - margin-right: 0; - margin-left: 10px; -} - -.avatar-placeholder { - height: 100%; - width: 100%; - background-color: #d1d5db; - border-radius: 50%; -} - -.chat-content { - background-color: #f3f4f6; - padding: 10px; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - position: relative; -} - -.chat-content.true { - display:flex; - flex-direction: column; - align-items: flex-end; - -} - -.chat-bubble.true .chat-content { - border-top-right-radius: 0; -} - -.chat-bubble.false .chat-content { - border-top-left-radius: 0; -} - -.username { - font-weight: 600; - margin-bottom: 5px; -} - -.message { - margin-bottom: 5px; -} - -.date { - font-size: 0.75rem; - color: #6b7280; - text-align: right; -} - -.date.false { - text-align: left; -} - -.date.under { - text-align: center; - position: relative; - margin-top: 5px; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.jsx b/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.jsx deleted file mode 100644 index 7f3c07f3..00000000 --- a/Front-End/old-frontend/src/components/ExpandableSidebar/ChatBubble.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import "./ChatBubble.css"; - -export function ChatBubble({ sender, date, message, isSingleChat }) { - console.log("ChatBubble"); - console.log(sender); - console.log(date); - console.log(message); - console.log(isSingleChat); - const user_name = localStorage.getItem("user_username"); - console.log("user_name:", user_name); - const isSenderMe = user_name === sender; - console.log("isSenderMe:", isSenderMe); - - return ( -
- {!isSingleChat && !isSenderMe && ( -
-
-
{date}
-
- )} -
- {!isSingleChat && !isSenderMe &&
{sender}
} -
{message}
- {isSingleChat || isSenderMe &&
{date}
} -
-
- ); -} - -ChatBubble.propTypes = { - date: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - isSingleChat: PropTypes.bool.isRequired, -}; \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.css b/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.css deleted file mode 100644 index 6e95a303..00000000 --- a/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.css +++ /dev/null @@ -1,188 +0,0 @@ -.expandable-sidebar { - display: flex; - height: 100%; - overflow: hidden; -} - -.sidebar-nav { - flex-shrink: 0; - width: 60px; - background-color: var(--background); - border-right: 1px solid var(--border); - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - padding: 5px; - padding-top: 10px; - z-index: 20; -} - -.sidebar-button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 60px; - background-color: transparent; - border: none; - cursor: pointer; - transition: background-color 0.3s; - border-radius: 8px; -} - -.sidebar-button:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.sidebar-button.active { - background-color: rgba(59, 130, 246, 0.1); -} - -.sidebar-button .icon { - width: 24px; - height: 24px; - margin-bottom: 4px; -} - -.sidebar-button.active .icon { - color: #3b82f6; -} - -.button-label { - font-size: 10px; - color: #4b5563; - text-align: center; -} - -.sidebar-button.active .button-label { - color: #3b82f6; -} - -.chat-list { - background-color: none; - border-right: 1px solid var(--border); - width: 300px; - z-index: 10; - overflow-y: auto; - padding: 10px; - padding-left: 0; - scrollbar-width: none; - padding-bottom: 700px; /*TODO renderelo una variabile per calcolare max-height*/ -} - -.chat-list::-webkit-scrollbar { - display: none; /* Nascondi la scrollbar su WebKit (Chrome, Safari) */ -} - -.chat-item { - background-color: #f3f4f6; - color: #1f2937; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - margin-bottom: 8px; - overflow: hidden; - z-index: 12; -} - -.chat-item-header { - padding: 16px; - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - z-index: 12; - background: none; /* Rimuovi il background */ - border: none; /* Rimuovi il bordo */ -} - -.chat-item-header-content { - display: flex; - align-items: center; - width: 100%; -} - -.avatar { - height: 40px; - width: 40px; - margin-right: 10px; - min-width: 40px; -} - -.avatar-placeholder { - height: 100%; - width: 100%; - background-color: #d1d5db; - border-radius: 50%; -} - -.chat-item-info { - flex-grow: 1; -} - -.chat-item-name { - font-weight: 600; - z-index: 12; -} - -.chat-item-message { - font-size: 0.875rem; - color: var(--muted-foreground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - z-index: 12; -} - -.chat-item-icon { - display: flex; - align-items: center; -} - -.chat-item-content { - z-index: 12; -} - -.chat-messages { - border-top: 1px solid rgba(0, 0, 0, 0.08); /* Colore piรน chiaro */ - border-bottom: 1px solid rgba(0, 0, 0, 0.08); /* Colore piรน chiaro */ - width: calc(100% - 30px); /* Riduci la larghezza per non raggiungere i bordi */ - margin: 0 auto; /* Centra il contenuto */ - height: 240px; - overflow-y: auto; - } - -.chats-input { - display: flex; - align-items: center; - justify-content: space-between; - background-color: #f3f4f6; - padding: 16px; - gap: 16px; -} - -.chats-input input { - flex-grow: 1; - padding: 8px; - border: 1px solid var(--border); - border-radius: 4px; -} - -.chats-input button { - background-color: #3b82f6; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; -} - -.chats-input button:hover { - background-color: #2563eb; -} - diff --git a/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.jsx b/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.jsx deleted file mode 100644 index 5b20f5ab..00000000 --- a/Front-End/old-frontend/src/components/ExpandableSidebar/ExpandableSidebar.jsx +++ /dev/null @@ -1,357 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { - MessageCircle, - Users, - Shuffle, - ChevronDown, - ChevronUp, - ChevronRight, - ChevronLeft, - Plus, - Send, -} from "lucide-react"; -import Chat from "../Chat/Chat"; -import "./ExpandableSidebar.css"; -import AddChat from "../Chat/AddChat"; -import { ChatBubble } from "./ChatBubble"; - -const getChatRooms = async () => { - const user_id = localStorage.getItem("user_id"); - const token = localStorage.getItem('token'); - console.log("user_id in sidechat: ", user_id); - console.log("Token inviato nell'header Authorization:", token); - console.log(`http://localhost:8001/chat/chat_rooms/getchat/?users=${user_id}`); - try { - const response = await fetch( - `http://localhost:8001/chat/chat_rooms/getchat/?user_id=${user_id}`, - { - method: "GET", - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - } - ); - - if (response.ok) { - const data = await response.json(); - console.log("Risposta dal server:", data); - localStorage.setItem("chat_rooms", JSON.stringify(data)); - return data; - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - return null; - } - } catch (error) { - console.error("Errore nella richiesta:", error); - return null; - } -}; - -export function ExpandableSidebar() { - const [chats, setChats] = useState([]); - const [expandedChat, setExpandedChat] = useState(null); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); // chat partono da chiuse - const [areChatItemsVisible, setAreChatItemsVisible] = useState(true); - const [activeButton, setActiveButton] = useState(null); - const [newMessage, setNewMessage] = useState(""); - const chatRefs = useRef({}); - - const handleChatTypeClick = (type) => { - const firstChatOfType = chats.find((chat) => chat.type === type); - if (firstChatOfType && chatRefs.current[firstChatOfType.id]) { - chatRefs.current[firstChatOfType.id].scrollIntoView({ - behavior: "smooth", - }); - } - setActiveButton(type); - }; - - const isAddChat = (chat_id) => { - return chat_id === "0"; - }; - - const handleCloseSidebar = () => { - setAreChatItemsVisible(false); - setTimeout(() => { - setIsSidebarOpen(false); - }, 350); // dipende da quante chat ci sono e dalla durata delle loro animazioni, o anche da quante chat hai scorso - }; - - const handleOpenSidebar = () => { - setIsSidebarOpen(true); - setAreChatItemsVisible(true); - }; - - const handleAddSidebar = () => { - handleCloseSidebar(); - }; - - const handleSendMessage = (chatId) => { - if (newMessage.trim() !== "") { - console.log(`Sending message to chat ${chatId}: ${newMessage}`); - setNewMessage(""); - } - }; - - useEffect(() => { - const fetchChats = async () => { - const serverChats = await getChatRooms(); - const addGroupChat = [{ - id: "0", - name: "Crea", - lastMessage: "Hey, crea?", - type: "crea", - }]; - - const mockChats = [ - { - id: "5", - name: "Eve", - lastMessage: "Thanks for your help!", - type: "single", - }, - { - id: "6", - name: "Project Team", - lastMessage: "Meeting at 3 PM", - type: "group", - }, - { - id: "7", - name: "Project Team", - lastMessage: "Meeting at 3 PM", - type: "group", - }, - { - id: "8", - name: "Project Team", - lastMessage: "Meeting at 3 PM", - type: "group", - }, - { - id: "9", - name: "Project Team", - lastMessage: "Meeting at 3 PM", - type: "group", - }, - { - id: "10", - name: "Project Team", - lastMessage: "Meeting at 3 PM", - type: "group", - }, - { - id: "11", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - { - id: "12", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - { - id: "13", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - { - id: "14", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - { - id: "15", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - { - id: "16", - name: "Random User", - lastMessage: "Nice to meet you!", - type: "random", - }, - ]; - - if (serverChats) { - const serverChatsFormatted = serverChats.map(chat => ({ - id: chat.room_id.toString(), - name: chat.room_name, - lastMessage: chat.room_description, - type: "group", - })); - setChats([...addGroupChat, ...serverChatsFormatted, ...mockChats]); - } else { - setChats([...addGroupChat, ...mockChats]); - } - }; - - fetchChats(); - }, []); - - return ( -
- - - - {isSidebarOpen && ( - - - {areChatItemsVisible && - chats.map((chat, index) => ( - (chatRefs.current[chat.id] = el)} - > - {/*
- - - - -
*/} - {isAddChat(chat.id) ? ( - - ) : ( -
-
- setExpandedChat((prev) => - prev === chat.id ? null : chat.id - ) - } - > -
-
-
-
-
-
{chat.name}
-
- {chat.lastMessage} -
-
-
- {expandedChat !== chat.id ? ( - - ) : ( - - )} -
-
-
- - {expandedChat === chat.id && chat.id < 5 && ( - - - {/*
- {/* bubble messages */} - {/*
*/} - {/* - type="text" - placeholder="Type a message..." - value={newMessage} - onChange={(e) => - setNewMessage(e.target.value) - } - /> - -
- */} -
- )} -
-
- )} - - ))} - - - )} - -
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/ExternalRedirect/ExternalRedirect.jsx b/Front-End/old-frontend/src/components/ExternalRedirect/ExternalRedirect.jsx deleted file mode 100644 index 8114de21..00000000 --- a/Front-End/old-frontend/src/components/ExternalRedirect/ExternalRedirect.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { useEffect } from 'react'; - -export default function ExternalRedirect({ to }) { - useEffect(() => { - window.location.href = to; - }, [to]); - - return null; - }; \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Home/Home.jsx b/Front-End/old-frontend/src/components/Home/Home.jsx deleted file mode 100644 index dcd6ee15..00000000 --- a/Front-End/old-frontend/src/components/Home/Home.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useState } from "react"; -import "./index.css"; -import SideChats from "../Chat/SideChats"; -import Button from "../Button/Button"; -import Profile from "../Profile/Profile"; -import logo from "./logo.png"; -import settings from "./settings.png"; -import chatImg from "./chat.png"; -import propic from "../Profile/propic.jpeg"; -import close from "./close.png"; -import TaskAvaiable from "../TaskAvaiable/TaskAvaiable"; -import TaskActive from "../TaskActive/TaskActive"; -import { ExpandableSidebar } from "../ExpandableSidebar/ExpandableSidebar"; -import Grid from "@mui/material/Grid2"; -import Notification from "../Notification/Notification"; - -//import WebSocketComponent from '../WebSocket/WebSocket'; -// import { Nav, navbar } from 'react-bootstrap'; -// import 'bootstrap/dist/css/bootstrap.min.css'; - -const Home = () => { - const [isDivVisible, setIsDivVisible] = useState(true); - const [isProfileVisible, setIsProfileVisible] = useState(true); - const [activeChatType, setActiveChatType] = useState(null); - - const toggleDiv = () => { - setIsDivVisible(!isDivVisible); - }; - - const toggleProfile = () => { - setIsProfileVisible(!isProfileVisible); - }; - - return ( -
-
- logo - - {/* Aggiungi il contenuto della tua home page qui */} -
-
-
- -
- - {/*
- -
- {isDivVisible && } -*/} -
- {isProfileVisible && } -
- -
-
- -
-
- -
-
-
-
- ); -}; - -export default Home; diff --git a/Front-End/old-frontend/src/components/Home/chat.png b/Front-End/old-frontend/src/components/Home/chat.png deleted file mode 100644 index 6f0e78e0..00000000 Binary files a/Front-End/old-frontend/src/components/Home/chat.png and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Home/close.png b/Front-End/old-frontend/src/components/Home/close.png deleted file mode 100644 index 20ea4ff4..00000000 Binary files a/Front-End/old-frontend/src/components/Home/close.png and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Home/index.css b/Front-End/old-frontend/src/components/Home/index.css deleted file mode 100644 index 8a5d9bec..00000000 --- a/Front-End/old-frontend/src/components/Home/index.css +++ /dev/null @@ -1,121 +0,0 @@ -.home { - height: 100vh; - width: 100vw; - background: radial-gradient(circle at -200px 230px, #f2f2fa 0%, #00115080 85%, #001150 100%); - - display: flex; - align-items: flex-start; - flex-direction: column; - justify-content: flex-start; -} - -.navbar { - width: 100%; - height: 50px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.undernavbar { - flex: 1; - width: 100vw; - display: flex; - height: calc(100vh - 200px); - /*align-items: center; - justify-content: center;*/ -} - -.sidebar { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.content { - border-top-left-radius: 12px; - background-color: rgba(1, 25, 78, 0.05); - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - align-content: flex-start; - width: 100%; - height: calc(100% - 20px); - padding: 10px; - gap: 10px; - overflow-y: auto; -} - -.task-container { - flex: 1 1 45%; - min-width: 300px; - max-width: 100%; -} - -.box { - width: 100px; - height: 100%; - background-color:#f5f5f5f0;; - border-radius: 8px; - flex-grow: 1; - min-width: 400px; - max-width: 800px; - max-height: 600px -} - -.logo-image { - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - height: 100%; -} - -.propic-image { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; -} - -.propic-button { - background: none; - border: none; - padding: 0; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding: 2px; -} - -.chat-buttons { - background: none; - border: none; - padding: 0; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - width: 50px; - padding: 2px; -} - -.propic-button img { - display: block; -} - -.chat-image { - padding-top: 5px; - width: 25px; - height: 25px; -} - -.expandable-sidebar-container { - display: flex; - height: 100%; - background-color: none; - } \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Home/logo.png b/Front-End/old-frontend/src/components/Home/logo.png deleted file mode 100644 index cf5f7d35..00000000 Binary files a/Front-End/old-frontend/src/components/Home/logo.png and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Home/propic.jpeg b/Front-End/old-frontend/src/components/Home/propic.jpeg deleted file mode 100644 index c6f30739..00000000 Binary files a/Front-End/old-frontend/src/components/Home/propic.jpeg and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Home/settings.png b/Front-End/old-frontend/src/components/Home/settings.png deleted file mode 100644 index 7a855635..00000000 Binary files a/Front-End/old-frontend/src/components/Home/settings.png and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Input/Input.jsx b/Front-End/old-frontend/src/components/Input/Input.jsx deleted file mode 100644 index 50be7ce3..00000000 --- a/Front-End/old-frontend/src/components/Input/Input.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import './index.css'; - -export default function Input({ type, name, placeholder, value, onChange, className }) { - return ( - - ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Input/index.css b/Front-End/old-frontend/src/components/Input/index.css deleted file mode 100644 index 00609255..00000000 --- a/Front-End/old-frontend/src/components/Input/index.css +++ /dev/null @@ -1,32 +0,0 @@ -.input { - border: none; - border-bottom: 2px solid #007bff; - background: none; - padding: 10px; - padding-left: 15px; - font-weight: 700; - width: 75%; - transition: .2s; -} - -.input:active, -.input:focus, -.input:hover, -.date_input:active, -.date_input:focus, -.date_input:hover { - outline: none; - border-bottom-color: #0056b3; -} - -.date_input{ - border: none; - border-bottom: 2px solid #007bff; - background: none; - padding: 10px; - padding-left: 15px; - font-weight: 700; - width: 75%; - transition: .2s; - color: #757575; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Login/Login.css b/Front-End/old-frontend/src/components/Login/Login.css deleted file mode 100644 index b98bfbc3..00000000 --- a/Front-End/old-frontend/src/components/Login/Login.css +++ /dev/null @@ -1,69 +0,0 @@ -.login -{ - height: 100vh; - width: 100vw; - background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); - display: flex; - justify-content: center; - align-items: center; -} - -.login_box -{ - height: 600px; - width: 400px; - background-color: #00020527; - border-radius: 20px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.login_form -{ - display:flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; -} - -.empty -{ - height: 40px; -} - -/* .background{ - width: 430px; - height: 520px; - position: absolute; - transform: translate(-50%,-50%); - left: 50%; - top: 50%; -} - -.background .shape{ - position: relative; - height: 200px; - width: 200px; - position: absolute; - border-radius: 50%; -} -.shape:first-child{ - background: linear-gradient( - #1845ad, - #23a2f6 - ); - left: -80px; - top: -120px; -} -.shape:last-child{ - background: linear-gradient( - to right, - #ff512f, - #f09819 - ); - right: -70px; - bottom: -130px; -} */ \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Login/Login.jsx b/Front-End/old-frontend/src/components/Login/Login.jsx deleted file mode 100644 index f74fe272..00000000 --- a/Front-End/old-frontend/src/components/Login/Login.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import "./Login.css"; -import { Button, TextField } from "@mui/material"; -import { onHandleSubmit } from "./post"; - -export default function Login() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const navigate = useNavigate(); - - const handleSubmit = (e) => { - e.preventDefault(); - onHandleSubmit(e, email, password, navigate); //file post.jsx - }; - - const onRegisterClick = () => { - navigate("/register"); - }; - - return ( -
-
-

Login

-
-
- setEmail(e.target.value)} - variant="outlined" - fullWidth - margin="none" - size="small" - /> - setPassword(e.target.value)} - variant="outlined" - fullWidth - margin="none" - size="small" - /> -
-
- - - -
-
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Login/post.jsx b/Front-End/old-frontend/src/components/Login/post.jsx deleted file mode 100644 index a690d5ca..00000000 --- a/Front-End/old-frontend/src/components/Login/post.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import { getCookie } from '../Cookie.jsx'; - -// Funzione per ottenere i dati dell'utente -const handleGetUser = async (csrftoken) => { - try { - const response = await fetch('http://localhost:8000/login/user', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrftoken, - 'Authorization': `Bearer ${localStorage.getItem('token')}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - const { user, user_id } = data; - const { email, username } = user; - - localStorage.setItem('user_email', email); - localStorage.setItem('user_username', username); - localStorage.setItem('user_id', user_id); - console.log('User email:', email); - console.log('User username:', username); - console.log('User ID:', user_id); - } else { - const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData); - } - } catch (error) { - console.error('Errore nella richiesta:', error); - } -}; - -// Funzione per eseguire la richiesta di login -const loginUser = async (email, password, csrftoken, navigate) => { - try { - const response = await fetch('http://localhost:8000/login/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrftoken, - }, - body: JSON.stringify({ email, password }), - }); - - if (response.ok) { - const data = await response.json(); - console.log('Risposta dal server:', data); - localStorage.setItem('token', data.access_token); - return true; - } else { - const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData); - return false; - } - } catch (error) { - console.error('Errore nella richiesta:', error); - return false; - } -}; - -// Funzione per gestire l'evento di submit -export const onHandleSubmit = async (e, email, password, navigate) => { - e.preventDefault(); - if (email && password) { - console.log('Username:', email); - console.log('Password:', password); - const csrftoken = getCookie('csrftoken'); - const loginSuccess = await loginUser(email, password, csrftoken, navigate); - if (loginSuccess) { - await handleGetUser(csrftoken); // TODO gestire return false? - navigate('/home'); - } - } else { - console.log('Per favore, inserisci sia username che password.'); - } -}; diff --git a/Front-End/old-frontend/src/components/Navbar/Navbar.jsx b/Front-End/old-frontend/src/components/Navbar/Navbar.jsx deleted file mode 100644 index 2baf47d5..00000000 --- a/Front-End/old-frontend/src/components/Navbar/Navbar.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import "./index.css"; - -export default function Navbar() { - return ( -
-

hello

-
- ); -} diff --git a/Front-End/old-frontend/src/components/Navbar/index.css b/Front-End/old-frontend/src/components/Navbar/index.css deleted file mode 100644 index 6478367e..00000000 --- a/Front-End/old-frontend/src/components/Navbar/index.css +++ /dev/null @@ -1,4 +0,0 @@ -.prova -{ -color: blue; -} diff --git a/Front-End/old-frontend/src/components/Notification/Notification.css b/Front-End/old-frontend/src/components/Notification/Notification.css deleted file mode 100644 index e69de29b..00000000 diff --git a/Front-End/old-frontend/src/components/Notification/Notification.jsx b/Front-End/old-frontend/src/components/Notification/Notification.jsx deleted file mode 100644 index 1497fe61..00000000 --- a/Front-End/old-frontend/src/components/Notification/Notification.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import "./Notification.css"; -import React, { useState, useEffect, useRef } from "react"; -import useWebSocket, { ReadyState } from "react-use-websocket"; -import { TextField, Button, Box, Typography } from "@mui/material"; - -export default function Notification() { - const token = localStorage.getItem("token"); - const user_id = localStorage.getItem("user_id"); - const wsUrl = useRef(`ws://127.0.0.1:8003/ws/user_notifications/?token=${token}`).current; - const [messageHistory, setMessageHistory] = useState([]); - const [friendID, setFriendID] = useState(""); - const { lastMessage, readyState } = useWebSocket(wsUrl); - - useEffect(() => { - if (lastMessage !== null) { - setMessageHistory((prev) => prev.concat(lastMessage)); - console.log("Nuovo messaggio:", lastMessage.data); - alert(`New message: ${lastMessage.data}`); - } -}, [lastMessage]); - -const handleSendFriendRequest = async () => { - console.log("Friend ID:", friendID); - console.log("User ID:", user_id); - try { - const response = await fetch("http://localhost:8002/user/addfriend", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_1: user_id, - user_2: friendID, - }), - }); - - if (response.ok) { - const data = await response.json(); - console.log("Friend Request Sent:", data); - } else { - const errorData = await response.json(); - console.error("Error in server response:", errorData); - } -} catch (error) { - console.error("Error in request:", error); -} - -// Controlla lo stato della connessione WebSocket -if (readyState === ReadyState.OPEN) { - console.log("WebSocket URL:", wsUrl); - console.log("WebSocket connection is active"); - } else { - console.log("WebSocket connection is not active"); - } - }; - - const handleDeleteFriendRequest = async () => { - console.log("Friend ID:", friendID); - console.log("User ID:", user_id); - try { - const response = await fetch("http://localhost:8002/user/addfriend", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - user_1: user_id, - user_2: friendID, - }), - }); - - if (response.ok) { - const data = await response.json(); - console.log("Friend Request Deleted:", data); - } else { - const errorData = await response.json(); - console.error("Error in server response:", errorData); - } - } catch (error) { - console.error("Error in request:", error); - } - }; - - return ( - - - {messageHistory.map((message, index) => ( - {message.data} - ))} - - setFriendID(e.target.value)} - margin="none" - size="small" - /> - - - - ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Profile/Profile.css b/Front-End/old-frontend/src/components/Profile/Profile.css deleted file mode 100644 index 38593d2f..00000000 --- a/Front-End/old-frontend/src/components/Profile/Profile.css +++ /dev/null @@ -1,113 +0,0 @@ -.profile-card { - width: 100%; - background-color: white; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - border-radius: 8px; - overflow: hidden; - min-width: 300px; -} - -.profile-card-content { - display: flex; - flex-direction: row; - padding: 16px; - gap: 16px; -} - -.profile-card-content.column { - flex-direction: column; -} - -.profile-card-image-container { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; -} - -.profile-image-circle { - background-color: #d3d3d3; - border-radius: 50%; - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; -} - -.profile-card-image { - width: 70px; - height: 70px; - border-radius: 50%; -} - -.buttons -{ - display: flex; - flex-direction: row; - gap: 10px; -} - -.edit-button, .save-button { - background-color: white; - border: none; - cursor: pointer; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - border-radius: 50%; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; -} - -.edit-icon, .save-icon { - width: 20px; - height: 20px; -} - -.profile-card-details { - display: flex; - flex-direction: column; - flex: 1; - padding-top: 8px; -} - -.profile-form { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} - -.profile-form.column { - grid-template-columns: 1fr; -} - -.profile-form-group { - display: flex; - flex-direction: column; -} - -.profile-form-group label { - font-size: 14px; - font-weight: 500; - margin-bottom: 4px; -} - -.readonly-input .MuiInputBase-root { - background-color: #f5f5f5; - pointer-events: none; -} - -.readonly-input .MuiOutlinedInput-notchedOutline { - border-color: transparent; -} - -.readonly-input:hover .MuiOutlinedInput-notchedOutline { - border-color: transparent; -} - -.readonly-input.Mui-focused .MuiOutlinedInput-notchedOutline { - border-color: transparent; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Profile/Profile.jsx b/Front-End/old-frontend/src/components/Profile/Profile.jsx deleted file mode 100644 index 8706f822..00000000 --- a/Front-End/old-frontend/src/components/Profile/Profile.jsx +++ /dev/null @@ -1,285 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import "./Profile.css"; -import { TextField } from "@mui/material"; -import { Pencil, Save } from 'lucide-react'; - -const PatchProfile = async (name, surname, birthdate, bio) => { - const userID = localStorage.getItem("user_id"); - - try { - const response = await fetch(`http://localhost:8002/user/user/${userID}/`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - account_id: userID, - first_name: name, - last_name: surname, - birth_date: birthdate, - bio: bio, - }), - }); - - if (response.ok) { - const data = await response.json(); - console.log("Profile:", data); - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } -}; - -export default function Profile() { - const username = localStorage.getItem("user_username") || ""; - const email = localStorage.getItem("user_email") || ""; - const user_id = localStorage.getItem("user_id") || ""; - const [name, setName] = useState(""); - const [surname, setSurname] = useState(""); - const [birthdate, setBirthdate] = useState(""); - const [bio, setBio] = useState(""); - const [edit, setEdit] = useState(false); - - const [tempName, setTempName] = useState(""); - const [tempSurname, setTempSurname] = useState(""); - const [tempBirthdate, setTempBirthdate] = useState(""); - const [tempBio, setTempBio] = useState(""); - - const formRef = useRef(null); - - useEffect(() => { - const handleResize = () => { - if (formRef.current) { - if (formRef.current.offsetWidth < 600) { - formRef.current.classList.add('column'); - } else { - formRef.current.classList.remove('column'); - } - } - }; - - window.addEventListener('resize', handleResize); - handleResize(); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - - useEffect(() => { - const GetProfile = async () => { - const userID = localStorage.getItem("user_id"); - try { - const response = await fetch(`http://localhost:8002/user/user/${userID}/`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${localStorage.getItem("token")}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - const { first_name, last_name, birth_date, bio } = data; - setName(first_name || ""); - setSurname(last_name || ""); - setBirthdate(birth_date || ""); - setBio(bio || ""); - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } - }; - GetProfile(); - }, []); - - useEffect(() => { - if (edit) { - setTempName(name); - setTempSurname(surname); - setTempBirthdate(birthdate); - setTempBio(bio); - } - }, [edit, name, surname, birthdate, bio]); - - const handleSave = (e) => { - e.preventDefault(); - setName(tempName); - setSurname(tempSurname); - setBirthdate(tempBirthdate); - setBio(tempBio); - - localStorage.setItem("name", tempName); - localStorage.setItem("surname", tempSurname); - localStorage.setItem("birthdate", tempBirthdate); - localStorage.setItem("bio", tempBio); - PatchProfile(tempName, tempSurname, tempBirthdate, tempBio); - - setEdit(false); - }; - - return ( -
-
-
-
-
- -
-
- -
-
- -
-
- setTempBirthdate(e.target.value)} - slotProps={{ - input: { - readOnly: !edit, - style: { border: !edit ? 'none' : '' }, - }, - inputLabel: { - shrink: true, - } - }} - size="small" - variant="outlined" - fullWidth - className={!edit ? "readonly-input" : ""} - /> -
-
- setTempName(e.target.value)} - slotProps={{ - input: { - readOnly: !edit, - style: { border: !edit ? 'none' : '' } - } - }} - variant="outlined" - size="small" - fullWidth - className={!edit ? "readonly-input" : ""} - /> -
-
- setTempSurname(e.target.value)} - slotProps={{ - input: { - readOnly: !edit, - style: { border: !edit ? 'none' : '' } - } - }} - variant="outlined" - size="small" - fullWidth - className={!edit ? "readonly-input" : ""} - /> -
-
- setTempBio(e.target.value)} - slotProps={{ - input: { - readOnly: !edit, - style: { border: !edit ? 'none' : '' } - } - }} - variant="outlined" - size="small" - fullWidth - className={!edit ? "readonly-input" : ""} - multiline - rows={1} - /> -
-
-
-
-
- {/*Profile*/} -
-
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Profile/propic.jpeg b/Front-End/old-frontend/src/components/Profile/propic.jpeg deleted file mode 100644 index c6f30739..00000000 Binary files a/Front-End/old-frontend/src/components/Profile/propic.jpeg and /dev/null differ diff --git a/Front-End/old-frontend/src/components/Register/Register.css b/Front-End/old-frontend/src/components/Register/Register.css deleted file mode 100644 index 9cd2bbf1..00000000 --- a/Front-End/old-frontend/src/components/Register/Register.css +++ /dev/null @@ -1,9 +0,0 @@ -.register -{ - height: 100vh; - width: 100vw; - background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); - display: flex; - justify-content: center; - align-items: center; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Register/Register.jsx b/Front-End/old-frontend/src/components/Register/Register.jsx deleted file mode 100644 index b1945b2d..00000000 --- a/Front-End/old-frontend/src/components/Register/Register.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import "./Register.css"; -import { Button, TextField } from "@mui/material"; -import { onHandleSubmit } from "./post"; - -export default function Register() { - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const navigate = useNavigate(); - - const handleSubmit = (e) => { - e.preventDefault(); - onHandleSubmit(e, username, email, password, navigate); //file post.jsx - }; - - const onLoginClick = () => { - navigate("/login"); - }; - - return ( -
-
-

Register

-
-
- setUsername(e.target.value)} - variant="outlined" - fullWidth - margin="none" - size="small" - /> - setEmail(e.target.value)} - variant="outlined" - fullWidth - margin="none" - size="small" - /> - setPassword(e.target.value)} - variant="outlined" - fullWidth - margin="none" - size="small" - /> - - - -
-
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Register/post.jsx b/Front-End/old-frontend/src/components/Register/post.jsx deleted file mode 100644 index 0686b98c..00000000 --- a/Front-End/old-frontend/src/components/Register/post.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { getCookie } from "../Cookie.jsx"; - -export const onHandleSubmit = async (e, username, email, password, navigate) => { - e.preventDefault(); - if (email && password) { - console.log('Username:', username); - console.log('Username:', email); - console.log('Password:', password); - try { - const response = await fetch('http://localhost:8000/login/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': getCookie('csrftoken'), - }, - body: JSON.stringify({ username, email, password }), - }); - - if (response.ok) { - const data = await response.json(); - console.log('Risposta dal server:', data); - - // const { user, user_id } = data; - // const { email, username } = user; - - // await pingNewUser( user_id, username, email ); - navigate('/login'); - } else { - const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData.error); - if (errorData.error === "['email already in use']") { - alert('L\'email inserita รจ giร  in uso. Scegli un\'altra email.'); - } else if (errorData.error === "['weak password']") { - alert('La password deve contenere almeno 8 caratteri.'); - } else if (errorData.error === "['username already in use']") { - alert('Per favore, inserisci un nome utente valido.'); - } else { - alert('Si รจ verificato un errore. Per favore, riprova.'); - } - console.error('Errore nella risposta del server:', response.statusText); - } - } catch (error) { - console.error('Errore nella richiesta:', error); - } - } else { - console.log('Per favore, inserisci sia username che password.'); - } -}; - -// const pingNewUser = async (user_id, username, email) => { -// try { -// const response = await fetch('http://localhost:8001/chat/new_user/', { -// method: 'POST', -// headers: { -// 'Content-Type': 'application/json', -// 'X-CSRFToken': getCookie('csrftoken'), -// 'Authorization': `Bearer ${localStorage.getItem('token')}`, -// }, -// body: JSON.stringify({ user_id: user_id, username: username, email: email }), -// }); - -// if (response.ok) { -// const data = await response.json(); -// console.log('Risposta dal server:', data); -// } else { -// const errorData = await response.json(); -// console.error('Errore nella risposta del server:', errorData); -// } -// } -// catch (error) { -// console.error('Errore nella richiesta:', error); -// } -// }; \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/Task/Task.jsx b/Front-End/old-frontend/src/components/Task/Task.jsx deleted file mode 100644 index 8214ea4c..00000000 --- a/Front-End/old-frontend/src/components/Task/Task.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react"; -import Button from "@mui/joy/Button"; -import Card from "@mui/joy/Card"; -import CardContent from "@mui/joy/CardContent"; -import CardActions from "@mui/joy/CardActions"; -import CircularProgress from "@mui/joy/CircularProgress"; -import Typography from "@mui/joy/Typography"; -import SvgIcon from "@mui/joy/SvgIcon"; -import Box from "@mui/material/Box"; -import TaskActive from "../TaskActive/TaskActive"; -import TaskInfo from "../TaskInfo/TaskInfo"; - -export default function CardInvertedColors({ task, handleJoinTask }) { - const [open, setOpen] = React.useState(false); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( - - - - {task.name.toUpperCase()} - - - - - - - - - ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskActive/Task2.jsx b/Front-End/old-frontend/src/components/TaskActive/Task2.jsx deleted file mode 100644 index 755040c6..00000000 --- a/Front-End/old-frontend/src/components/TaskActive/Task2.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import * as React from 'react'; -import AspectRatio from '@mui/joy/AspectRatio'; -import Box from '@mui/joy/Box'; -import Button from '@mui/joy/Button'; -import Card from '@mui/joy/Card'; -import CardContent from '@mui/joy/CardContent'; -import Typography from '@mui/joy/Typography'; -import Sheet from '@mui/joy/Sheet'; -import CircularProgress from '@mui/joy/CircularProgress'; -import { getCookie } from "../Cookie.jsx"; - -export default function Task2({ id, task, onCancel }) { - const [progress, setProgress] = React.useState(Number(task.rate)); - - const handleCancel = () => { - if (onCancel) { - onCancel(id); - } - }; - - const handleCompleteTask = async (task_id, task_rate) => { - const user_id = localStorage.getItem("user_id"); - try { - const response = await fetch( - `http://localhost:8002/task/progress/${task_id}&${user_id}/`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ rate: task_rate }), - } - ); - if (response.ok) { - const data = await response.json(); - console.log("CompletedTask:", data); - //handleGetActiveTasks(); - } else { - const errorData = await response.json(); - console.error("Errore CompletedTask:", errorData); - } - } catch (error) { - console.error("Errore CompletedTask:", error); - } - }; - - const updateProgress = async (newProgress) => { - try { - const response = await fetch(`http://localhost:8002/task/progress/${id}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ rate: newProgress }), - }); - - if (!response.ok) { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } - }; - -// const handleProgressClick = () => { -// setProgress((prevProgress) => { -// const newProgress = Math.min(prevProgress + 10, 100); -// updateProgress(newProgress); -// return newProgress; -// }); -// }; - - return ( - - - - - - - - {task.task.name} {task.task.category} - - - - div': { flex: 1 }, - }} - > -
- - Time remaining - - {task.task.duration[0]} days -
-
- - EXP - - {task.task.exp} -
-
- - Progress - - {progress} -
-
- button': { flex: 1 } }}> - - - -
-
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskActive/TaskActive.css b/Front-End/old-frontend/src/components/TaskActive/TaskActive.css deleted file mode 100644 index e69de29b..00000000 diff --git a/Front-End/old-frontend/src/components/TaskActive/TaskActive.jsx b/Front-End/old-frontend/src/components/TaskActive/TaskActive.jsx deleted file mode 100644 index 0d2cc01a..00000000 --- a/Front-End/old-frontend/src/components/TaskActive/TaskActive.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import "./TaskActive.css"; -import React, { useEffect, useState } from "react"; -import { getCookie } from "../Cookie.jsx"; -import Task2 from "./Task2.jsx"; // Import Task2 component -import Box from "@mui/material/Box"; -import Button from '@mui/material/Button'; -import Typography from "@mui/material/Typography"; - -export default function TaskActive() { - const [tasks, setTask] = useState([]); - const user_id = localStorage.getItem("user_id"); - - const handleGetActiveTasks = async () => { - try { - const response = await fetch( - `http://localhost:8002/task/progress?user_id=${user_id}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - } - ); - - if (response.ok) { - const data = await response.json(); - setTask(data); - console.log("ActiveTask:", data); - } else { - const errorData = await response.json(); - console.error("Errore ActiveTask:", errorData); - } - } catch (error) { - console.error("Errore ActiveTask:", error); - } - }; - - // const handleCompleteTask = async (task_id) => { - // try { - // const response = await fetch(`http://localhost:8002/task/progress/${task_id}/`, { - // method: "DELETE", - // headers: { - // "Content-Type": "application/json", - // "X-CSRFToken": getCookie("csrftoken"), - // "Authorization": `Bearer ${localStorage.getItem("token")}`, - // }, - // }); - - // if (response.ok) { - // const data = await response.json(); - // console.log("CompletedTask:", data); - // handleGetActiveTasks(); - // } else { - // const errorData = await response.json(); - // console.error("Errore CompletedTask:", errorData); - // } - // } catch (error) { - // console.error("Errore CompletedTask:", error); - // } - // } - - const handleCompleteTask = async (task_id, task_rate) => { - const user_id = localStorage.getItem("user_id"); - try { - const response = await fetch( - `http://localhost:8002/task/progress/${task_id}&${user_id}/`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ rate: task_rate }), - } - ); - if (response.ok) { - const data = await response.json(); - console.log("CompletedTask:", data); - handleGetActiveTasks(); - } else { - const errorData = await response.json(); - console.error("Errore CompletedTask:", errorData); - } - } catch (error) { - console.error("Errore CompletedTask:", error); - } - }; - - useEffect(() => { - handleGetActiveTasks(); - }, []); - - return ( - - - - task active - - - {tasks.length > 0 ? ( - tasks.map((task, index) => ( - // Use Task2 component - )) - ) : ( -

No tasks active

- )} -
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskAvaiable/Task.css b/Front-End/old-frontend/src/components/TaskAvaiable/Task.css deleted file mode 100644 index aff3300d..00000000 --- a/Front-End/old-frontend/src/components/TaskAvaiable/Task.css +++ /dev/null @@ -1,11 +0,0 @@ -.taskbox2 { - display: flex; - width: 50px; - height: 100%; - background-color:#f5f5f5f0; - border-radius: 8px; - flex-grow: 1; - max-width: 800px; - max-height: 600px; - padding: 10px; -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskAvaiable/Task.jsx b/Front-End/old-frontend/src/components/TaskAvaiable/Task.jsx deleted file mode 100644 index 20c2e569..00000000 --- a/Front-End/old-frontend/src/components/TaskAvaiable/Task.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import "./Task.css"; -import React, { useEffect, useState } from "react"; -import { getCookie } from "../Cookie.jsx"; -import Box from "@mui/material/Box"; -import Card from '@mui/joy/Card'; -import CardContent from '@mui/joy/CardContent'; -import Typography from '@mui/joy/Typography'; -import Sheet from '@mui/joy/Sheet'; -import Button from '@mui/joy/Button'; -import CircularProgress from '@mui/joy/CircularProgress'; - -const joinTasks = async (task_id, user_id) => { - try { - const response = await fetch(`http://localhost:8002/task/progress`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ task: task_id, user: user_id }), - }); - - if (response.ok) { - const data = await response.json(); - console.log("Joined chat:", data); - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } -}; - -export default function TaskAvaiable(task) { - const user_id = localStorage.getItem("user_id"); - - const handleJoinTask = (task_id) => { - joinTasks(task_id, user_id); - }; - - return ( - - - - - {task.task.name} {task.task.category} - - - {task.task.description} - - div': { flex: 1 }, - }} - > -
- - Time remaining - - {task.task.duration[0]} days -
-
- - EXP - - {task.task.exp} -
-
- button': { flex: 1 } }}> - - -
-
-
- ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.css b/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.css deleted file mode 100644 index 6e829226..00000000 --- a/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.css +++ /dev/null @@ -1,9 +0,0 @@ -.taskbox { - width: 100px; - height: 100%; - background-color:#f5f5f5f0; - border-radius: 8px; - flex-grow: 1; - max-height: 600px; - padding: 10px; -} diff --git a/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.jsx b/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.jsx deleted file mode 100644 index 7b018ab8..00000000 --- a/Front-End/old-frontend/src/components/TaskAvaiable/TaskAvaiable.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import "./TaskAvaiable.css"; -import React, { useEffect, useState } from "react"; -import { getCookie } from "../Cookie.jsx"; -import Box from "@mui/material/Box"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemText from "@mui/material/ListItemText"; -import TabContext from "@mui/lab/TabContext"; -import TabPanel from "@mui/lab/TabPanel"; -import Button from '@mui/material/Button'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import Grow from '@mui/material/Grow'; -import Typography from '@mui/material/Typography'; -import Task from "./Task.jsx"; - -const joinTasks = async (task_id, user_id) => { - console.log("Joining task:", task_id, user_id); - try { - const response = await fetch(`http://localhost:8002/task/progress`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ task: task_id, user: user_id }), - }); - - if (response.ok) { - const data = await response.json(); - console.log("Joined chat:", data); - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } -}; - -export default function TaskAvaiable() { - const [tasks, setTask] = useState([]); - const [value, setValue] = useState("ALL"); - const [selectedCategory, setSelectedCategory] = useState("ALL"); - const [anchorEl, setAnchorEl] = useState(null); - const user_id = localStorage.getItem("user_id"); - - - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleMenuItemClick = (category) => { - setValue(category); - setSelectedCategory(category); - handleClose(); - }; - - const filteredTasks = value === "ALL" ? tasks : tasks.filter((task) => task.category === value); - - const handleGetTasks = async () => { - try { - const response = await fetch(`http://localhost:8002/task/task?user_id=${user_id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": getCookie("csrftoken"), - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - setTask(data); - console.log("AviableTask:", data); - } else { - const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); - } - } catch (error) { - console.error("Errore nella richiesta:", error); - } - }; - - useEffect(() => { - handleGetTasks(); - }, []); - - const handleJoinTask = (task_id) => { - const user_id = localStorage.getItem("user_id"); - joinTasks(task_id, user_id); - }; - - function renderTabPanel(category) { - const filteredTasks = category === "ALL" ? tasks : tasks.filter((task) => task.category === category); - return ( - - - {filteredTasks.length > 0 ? ( - filteredTasks.map((task, index) => ( - - - {/**/} - - )) - ) : ( - - - - )} - - - ); - } - - const categories = ["ALL", "SP", "ED", "HE", "AR", "SS", "MD"]; - - return ( - - - - task available - - - {categories.map((category) => ( - handleMenuItemClick(category)} - > - {category} - - ))} - - - {categories.map((category) => renderTabPanel(category))} - - - ); -} \ No newline at end of file diff --git a/Front-End/old-frontend/src/components/TaskInfo/TaskInfo.jsx b/Front-End/old-frontend/src/components/TaskInfo/TaskInfo.jsx deleted file mode 100644 index 68d6963d..00000000 --- a/Front-End/old-frontend/src/components/TaskInfo/TaskInfo.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import { styled } from '@mui/material/styles'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogActions from '@mui/material/DialogActions'; -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; -import Typography from '@mui/material/Typography'; - -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ - '& .MuiDialogContent-root': { - padding: theme.spacing(2), - }, - '& .MuiDialogActions-root': { - padding: theme.spacing(1), - }, -})); - -export default function CustomizedDialogs({ task, open, onClose }) { - return ( - - - - {task.name.toUpperCase()} - - ({ - position: 'absolute', - right: 8, - top: 8, - color: theme.palette.grey[500], - })} - > - - - - - EXP: {task.exp} - - - Duration: {task.duration[0]} days - - - Description: - - - {task.description} - - - - - ); -} diff --git a/Front-End/old-frontend/src/components/WebSocket/WebSocket.jsx b/Front-End/old-frontend/src/components/WebSocket/WebSocket.jsx deleted file mode 100644 index ccd00b40..00000000 --- a/Front-End/old-frontend/src/components/WebSocket/WebSocket.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; -import useWebSocket from 'react-use-websocket'; - -const WebSocketComponent = forwardRef(({ isVisible }, ref) => { - const [message, setMessage] = useState(''); - const [chat, setChat] = useState([]); - const roomName = '2'; - const token = localStorage.getItem('token'); - const wsUrl = useRef(`ws://127.0.0.1:8001/ws/chat/${roomName}/`).current; - - const { sendMessage, lastMessage, readyState, getWebSocket } = useWebSocket(wsUrl, { - onOpen: () => { - console.log('WebSocket connection opened'); - }, - onClose: () => console.log('WebSocket connection closed'), - onError: (e) => console.error('WebSocket error:', e), - }, isVisible); - - useEffect(() => { - console.log('WebSocket URL:', wsUrl); - - if (lastMessage !== null) { - const data = JSON.parse(lastMessage.data); - setChat((prevChat) => [...prevChat, data.message]); - } - - return () => { - const socket = getWebSocket(); - //if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { - // console.log('Chiudendo la connessione WebSocket'); - // socket.close(); - //} - }; - }, [lastMessage, getWebSocket]); - - useImperativeHandle(ref, () => ({ - setMessage, - handleSendMessage, - })); - - const handleSendMessage = () => { - const socket = getWebSocket(); - console.log('Stato della connessione WebSocket:', socket.readyState); - if (socket && socket.readyState === WebSocket.OPEN) { - const messageData = { - message: message, - room_name: roomName, - sender: localStorage.getItem('user_id'), - }; - sendMessage(JSON.stringify(messageData)); - setMessage(''); - } else { - alert('Connessione WebSocket non attiva'); - } - }; - - return ( -
-
Ready state: {readyState}
-
- setMessage(e.target.value)} - placeholder="Type a message" - /> - -
-
-

Chat

-
- {chat.map((msg, index) => ( -
{msg}
- ))} -
-
-
- ); -}); - -export default WebSocketComponent; \ No newline at end of file diff --git a/Front-End/old-frontend/src/index.css b/Front-End/old-frontend/src/index.css deleted file mode 100644 index ec2585e8..00000000 --- a/Front-End/old-frontend/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/Front-End/old-frontend/src/index.js b/Front-End/old-frontend/src/index.js deleted file mode 100644 index d563c0fb..00000000 --- a/Front-End/old-frontend/src/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - - - -); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/Front-End/old-frontend/src/logo.svg b/Front-End/old-frontend/src/logo.svg deleted file mode 100644 index c0083bc2..00000000 --- a/Front-End/old-frontend/src/logo.svg +++ /dev/null @@ -1,2001 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Front-End/old-frontend/src/package.json b/Front-End/old-frontend/src/package.json deleted file mode 100644 index d290aebb..00000000 --- a/Front-End/old-frontend/src/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "src", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC" -} diff --git a/Front-End/old-frontend/src/reportWebVitals.js b/Front-End/old-frontend/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad..00000000 --- a/Front-End/old-frontend/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/Front-End/old-frontend/src/setupTests.js b/Front-End/old-frontend/src/setupTests.js deleted file mode 100644 index 8f2609b7..00000000 --- a/Front-End/old-frontend/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index b8e2ff35..2810a3f5 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -19,13 +19,14 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -35,13 +36,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -51,13 +53,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -67,13 +70,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -83,13 +87,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -99,13 +104,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -115,13 +121,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -131,13 +138,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -147,13 +155,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -163,13 +172,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -179,13 +189,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -195,13 +206,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -211,13 +223,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -227,13 +240,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -243,13 +257,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -259,13 +274,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -275,13 +291,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -291,13 +308,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -307,13 +325,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -323,13 +342,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -339,13 +359,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -355,13 +376,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -371,13 +393,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -387,13 +410,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -403,13 +427,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -422,6 +447,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -429,269 +455,305 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", - "integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", - "integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", - "integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz", - "integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", - "integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", - "integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", - "integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", - "integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", - "integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", - "integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", - "integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", - "integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", - "integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", - "integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", - "integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", - "integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", - "integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", - "integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", - "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" }, "node_modules/@zeit/schemas": { "version": "2.36.0", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/accepts": { "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==", "dev": true, + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -705,6 +767,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -721,6 +784,7 @@ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.1.0" } @@ -730,6 +794,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -738,13 +803,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ansi-align/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -759,6 +826,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -771,6 +839,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -783,6 +852,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -808,24 +878,27 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", @@ -836,6 +909,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -845,6 +919,7 @@ "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.0", @@ -867,6 +942,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -877,6 +953,7 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -886,6 +963,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -898,6 +976,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -910,6 +989,7 @@ "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2" }, @@ -925,6 +1005,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -940,6 +1021,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -956,6 +1038,7 @@ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -968,6 +1051,7 @@ "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", "dev": true, + "license": "MIT", "dependencies": { "arch": "^2.2.0", "execa": "^5.1.1", @@ -985,6 +1069,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -996,13 +1081,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -1015,6 +1102,7 @@ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -1032,13 +1120,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1048,6 +1138,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1062,6 +1153,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -1071,6 +1163,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -1079,20 +1172,23 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1100,31 +1196,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/execa": { @@ -1132,6 +1228,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -1154,7 +1251,23 @@ "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==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -1162,6 +1275,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1175,6 +1289,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1187,6 +1302,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1196,6 +1312,7 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } @@ -1204,13 +1321,15 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -1226,6 +1345,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1235,6 +1355,7 @@ "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1247,6 +1368,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -1259,6 +1381,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -1270,25 +1393,29 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1298,6 +1425,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1310,6 +1438,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1319,6 +1448,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1328,6 +1458,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1340,6 +1471,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1348,12 +1480,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1361,6 +1494,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1373,6 +1507,7 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1382,6 +1517,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -1394,6 +1530,7 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1403,6 +1540,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -1417,13 +1555,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true + "dev": true, + "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1432,18 +1572,33 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -1459,6 +1614,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -1473,6 +1629,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1482,6 +1639,7 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1491,6 +1649,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -1506,6 +1665,7 @@ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", "dev": true, + "license": "MIT", "dependencies": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" @@ -1516,6 +1676,7 @@ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", "dev": true, + "license": "MIT", "dependencies": { "rc": "^1.0.1" }, @@ -1528,17 +1689,19 @@ "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==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/rollup": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", - "integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -1548,25 +1711,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.1", - "@rollup/rollup-android-arm64": "4.32.1", - "@rollup/rollup-darwin-arm64": "4.32.1", - "@rollup/rollup-darwin-x64": "4.32.1", - "@rollup/rollup-freebsd-arm64": "4.32.1", - "@rollup/rollup-freebsd-x64": "4.32.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", - "@rollup/rollup-linux-arm-musleabihf": "4.32.1", - "@rollup/rollup-linux-arm64-gnu": "4.32.1", - "@rollup/rollup-linux-arm64-musl": "4.32.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", - "@rollup/rollup-linux-riscv64-gnu": "4.32.1", - "@rollup/rollup-linux-s390x-gnu": "4.32.1", - "@rollup/rollup-linux-x64-gnu": "4.32.1", - "@rollup/rollup-linux-x64-musl": "4.32.1", - "@rollup/rollup-win32-arm64-msvc": "4.32.1", - "@rollup/rollup-win32-ia32-msvc": "4.32.1", - "@rollup/rollup-win32-x64-msvc": "4.32.1", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, @@ -1574,13 +1738,15 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/serve": { "version": "14.2.4", "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", "dev": true, + "license": "MIT", "dependencies": { "@zeit/schemas": "2.36.0", "ajv": "8.12.0", @@ -1606,6 +1772,7 @@ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", @@ -1621,6 +1788,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1630,6 +1798,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "~1.33.0" }, @@ -1642,6 +1811,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -1654,6 +1824,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1662,13 +1833,15 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1676,7 +1849,8 @@ "node_modules/stats.js": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" }, "node_modules/statsjs": { "version": "1.0.7", @@ -1688,6 +1862,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1705,6 +1880,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1720,6 +1896,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1729,6 +1906,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1738,6 +1916,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1748,13 +1927,32 @@ "node_modules/three": { "version": "0.171.0", "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", - "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==" + "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -1767,6 +1965,7 @@ "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", "dev": true, + "license": "MIT", "dependencies": { "registry-auth-token": "3.3.2", "registry-url": "3.1.0" @@ -1777,6 +1976,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -1786,19 +1986,24 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vite": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", - "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -1866,6 +2071,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -1881,6 +2087,7 @@ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^5.0.1" }, @@ -1896,6 +2103,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", diff --git a/Front-End/package.json b/Front-End/package.json index 2d8caf61..d95e20b4 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -5,7 +5,10 @@ "description": "", "main": "index.js", "scripts": { - "start": "serve -s ." + "start": "serve -s .", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" }, "dependencies": { "bootstrap": "^5.3.3", diff --git a/Front-End/public/alert/alert.css b/Front-End/public/alert/alert.css new file mode 100644 index 00000000..a75045ab --- /dev/null +++ b/Front-End/public/alert/alert.css @@ -0,0 +1,46 @@ +.alert { + position: fixed; + padding: 15px; + border: 1px solid #000; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + display: flex; + justify-content: space-between; + align-items: center; + min-width: 250px; + max-width: 350px; + } + + /* Toast specifico (posizionato in basso a sinistra) */ + #toast-container .alert { + bottom: 20px; + left: 20px; + margin-bottom: 10px; + } + + .alert.warn { + background-color: #fff3cd; + color: #856404; + border-color: #ffeeba; + } + + .alert.error { + background-color: #f8d7da; + color: #721c24; + border-color: #f5c6cb; + } + + .alert.success { + background-color: #d4edda; + color: #155724; + border-color: #c3e6cb; + } + + .alert .btn-close { + margin-left: 10px; + cursor: pointer; + background: transparent; + border: none; + font-size: 1.5rem; + font-weight: bold; + } \ No newline at end of file diff --git a/Front-End/public/alert/alert.js b/Front-End/public/alert/alert.js new file mode 100644 index 00000000..7cee77b1 --- /dev/null +++ b/Front-End/public/alert/alert.js @@ -0,0 +1,59 @@ +const link = document.createElement('link'); +link.rel = 'stylesheet'; +link.href = '/alert/alert.css'; +document.head.appendChild(link); + +export function renderAlert(message, type) { + return ` +
+ ${message} + +
+ `; +} + +export function showAlertForXSeconds(message, type, seconds, options = {}) { + const { asToast = false } = options; + let container; + + if (asToast) { + container = document.getElementById('toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'toast-container'; + container.style.position = 'fixed'; + container.style.bottom = '20px'; + container.style.left = '20px'; + container.style.zIndex = '9999'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '10px'; + document.body.appendChild(container); + } + } else { + container = document.getElementById('alert-container'); + if (!container) { + console.error("Alert container not found"); + return; + } + } + + const alertHTML = renderAlert(message, type); + const wrapper = document.createElement('div'); + wrapper.innerHTML = alertHTML; + + if (asToast) { + const alertElement = wrapper.firstElementChild; + container.appendChild(alertElement); + setTimeout(() => { + if (alertElement.parentNode) { + alertElement.parentNode.removeChild(alertElement); + } + }, seconds * 1000); + } else { + container.innerHTML = alertHTML; + setTimeout(() => { + container.innerHTML = ''; + }, seconds * 1000); + } +} \ No newline at end of file diff --git a/Front-End/public/chat/1.jpg b/Front-End/public/chat/1.jpg new file mode 100644 index 00000000..b07e6243 Binary files /dev/null and b/Front-End/public/chat/1.jpg differ diff --git a/Front-End/public/chat/10.jpg b/Front-End/public/chat/10.jpg new file mode 100644 index 00000000..cd5a5407 Binary files /dev/null and b/Front-End/public/chat/10.jpg differ diff --git a/Front-End/public/chat/11.jpg b/Front-End/public/chat/11.jpg new file mode 100644 index 00000000..26a0a6de Binary files /dev/null and b/Front-End/public/chat/11.jpg differ diff --git a/Front-End/public/chat/12.jpg b/Front-End/public/chat/12.jpg new file mode 100644 index 00000000..04a19b7f Binary files /dev/null and b/Front-End/public/chat/12.jpg differ diff --git a/Front-End/public/chat/13.jpg b/Front-End/public/chat/13.jpg new file mode 100644 index 00000000..4f9ad68d Binary files /dev/null and b/Front-End/public/chat/13.jpg differ diff --git a/Front-End/public/chat/14.jpg b/Front-End/public/chat/14.jpg new file mode 100644 index 00000000..af741dc0 Binary files /dev/null and b/Front-End/public/chat/14.jpg differ diff --git a/Front-End/public/chat/15.jpg b/Front-End/public/chat/15.jpg new file mode 100644 index 00000000..e6220508 Binary files /dev/null and b/Front-End/public/chat/15.jpg differ diff --git a/Front-End/public/chat/2.jpg b/Front-End/public/chat/2.jpg new file mode 100644 index 00000000..04890199 Binary files /dev/null and b/Front-End/public/chat/2.jpg differ diff --git a/Front-End/public/chat/3.jpg b/Front-End/public/chat/3.jpg new file mode 100644 index 00000000..8b067614 Binary files /dev/null and b/Front-End/public/chat/3.jpg differ diff --git a/Front-End/public/chat/4.jpg b/Front-End/public/chat/4.jpg new file mode 100644 index 00000000..27065c11 Binary files /dev/null and b/Front-End/public/chat/4.jpg differ diff --git a/Front-End/public/chat/5.jpg b/Front-End/public/chat/5.jpg new file mode 100644 index 00000000..fabd7e46 Binary files /dev/null and b/Front-End/public/chat/5.jpg differ diff --git a/Front-End/public/chat/6.jpg b/Front-End/public/chat/6.jpg new file mode 100644 index 00000000..7f07aead Binary files /dev/null and b/Front-End/public/chat/6.jpg differ diff --git a/Front-End/public/chat/7.jpg b/Front-End/public/chat/7.jpg new file mode 100644 index 00000000..3ad834bc Binary files /dev/null and b/Front-End/public/chat/7.jpg differ diff --git a/Front-End/public/chat/8.jpg b/Front-End/public/chat/8.jpg new file mode 100644 index 00000000..446cfc93 Binary files /dev/null and b/Front-End/public/chat/8.jpg differ diff --git a/Front-End/public/chat/9.jpg b/Front-End/public/chat/9.jpg new file mode 100644 index 00000000..3cea373a Binary files /dev/null and b/Front-End/public/chat/9.jpg differ diff --git a/Front-End/public/chat/AddChat.js b/Front-End/public/chat/AddChat.js index 94498f9c..dba80dc0 100644 --- a/Front-End/public/chat/AddChat.js +++ b/Front-End/public/chat/AddChat.js @@ -1,20 +1,29 @@ -import { getCookie } from '../cookie.js'; -import { getVariables } from '../var.js'; -import { updateChatList } from './ExpandableSidebar.js'; +import { getCookie } from "../cookie.js"; +import { getVariables } from "../var.js"; +import { updateChatList } from "./ExpandableSidebar.js"; +import { initFriendAutocomplete } from "../notification/friendAutocomplete.js"; -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/chat/chat.css'; +const link = document.createElement("link"); +link.rel = "stylesheet"; +link.href = "/chat/chat.css"; document.head.appendChild(link); function renderAddChat() { - const addChatContainer = document.createElement('div'); - addChatContainer.className = 'add-chat'; + const addChatContainer = document.createElement("div"); + addChatContainer.className = "add-chat"; addChatContainer.innerHTML = ` +
+

Create Chat

+ +
- +
+ + +
+
+ + `).join(""); + badgeContainer.querySelectorAll(".btn-close").forEach(btn => { + btn.addEventListener("click", function() { + const idToRemove = this.getAttribute("data-userid"); + selectedUsers = selectedUsers.filter(u => String(u.id) !== idToRemove); + renderSelectedUserIds(); + }); + }); +} + +initFriendAutocomplete({ + inputId: "userIdsInput", + suggestionListId: "suggestionListChat", + handlerOnSelect: (input, userId, item) => { + userId = String(userId); + const username = item?.textContent?.split(" ")[0] || userId; + if (!selectedUsers.some(u => String(u.id) === userId)) { + selectedUsers.push({ id: userId, username }); + renderSelectedUserIds(); } + input.value = ""; + } +}); + + addChatContainer + .querySelector("#addChatForm") + .addEventListener("submit", async function (e) { + e.preventDefault(); + + const roomName = document.getElementById("roomName").value; + const roomDescription = document.getElementById("roomDescription").value; + const { userId, token, url_api } = getVariables(); - console.log('Utenti:', userIdsArray); - - try { - const response = await fetch('http://localhost:8001/chat/chat_rooms/create/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': getCookie('csrftoken'), - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ room_name: roomName, room_description: roomDescription, creator: userId, users: userIdsArray }), - }); - - if (response.ok) { - const data = await response.json(); - console.log('Chat room creata:', data); - - // Aggiorna la lista delle chat - updateChatList(); - } else { - const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData); + + let userIdsArray = selectedUsers.map(u => u.id); + if (!userIdsArray.includes(userId)) { + userIdsArray.push(userId); + } + + console.log("Utenti:", userIdsArray); + + try { + const response = await fetch( + `${url_api}/chat/chat/chat_rooms/create/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCookie("csrftoken"), + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + room_name: roomName, + room_description: roomDescription, + creator: userId, + users: userIdsArray, + }), + } + ); + + if (response.ok) { + const data = await response.json(); + console.log("Chat room creata:", data); + + // Aggiorna la lista delle chat + updateChatList(); + } else { + const errorData = await response.json(); + console.error( + "Errore nella risposta del server:", + errorData + ); + } + } catch (error) { + console.error("Errore nella richiesta:", error); } - } catch (error) { - console.error('Errore nella richiesta:', error); - } - }); + }); return addChatContainer; } -export { renderAddChat }; \ No newline at end of file +export { renderAddChat }; diff --git a/Front-End/public/chat/ChatBubble.js b/Front-End/public/chat/ChatBubble.js index b0ed6c06..fa3ce4ae 100644 --- a/Front-End/public/chat/ChatBubble.js +++ b/Front-End/public/chat/ChatBubble.js @@ -1,31 +1,189 @@ -import { getVariables } from '../var.js'; +import { getVariables, calculateInitials } from "../var.js"; +import { getCookie } from "../cookie.js"; -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/chat/chat.css'; +const link = document.createElement("link"); +link.rel = "stylesheet"; +link.href = "/chat/chat.css"; document.head.appendChild(link); -function renderChatBubble({ sender, date, message, isSingleChat }) { - const { userUsername } = getVariables(); - const isSenderMe = userUsername === sender; +function renderChatBubble({ senderName, date, message, isSingleChat, type, senderId }) { + const { userUsername, url_api } = getVariables(); + const isSenderMe = userUsername === senderName; + const initials = calculateInitials(senderName); + // if (!isSingleChat && !isSenderMe) { + // takeDatafromUserId(senderId).then(ImgData => { + // console.log("[renderChatBubble] ImgData:", ImgData); + // let ProfileImg = ImgData?.current_avatar_url || ""; + // if (ProfileImg == "/media/placeholder.jpeg") ProfileImg = ""; + // console.log("[renderChatBubble] ProfileImg:", ProfileImg); + // }); + // } - const chatBubble = document.createElement('div'); + const chatBubble = document.createElement("div"); chatBubble.className = `chat-bubble ${isSenderMe ? "true" : "false"}`; chatBubble.innerHTML = ` - ${!isSingleChat && !isSenderMe ? ` -
-
+ ${ + !isSingleChat && !isSenderMe + ? ` +
- ` : ''} + + ` + : "" + } +
- ${!isSingleChat && !isSenderMe ? `
${sender}
` : ''} -
${message}
+ ${!isSingleChat && !isSenderMe ? `
${senderName}
` : ""} +
+ ${message} + + ${type === "game_invitation" + ? `` + : "" + } + +
+
- ${isSingleChat || isSenderMe ? `
${date}
` : ''} + + ${ + isSingleChat || isSenderMe + ? `
${date}
` + : "" + } `; + if (!isSingleChat && !isSenderMe) { + takeDatafromUserId(senderId).then(ImgData => { + console.log("[renderChatBubble] ImgData:", ImgData); + let ProfileImg = ImgData?.current_avatar.image_url || ""; + if (ProfileImg == "/media/placeholder.jpeg") ProfileImg = ""; + else ProfileImg = url_api + '/user' + ProfileImg; + + const avatarDiv = chatBubble.querySelector('.friend-avatar'); + if (avatarDiv) { + if (ProfileImg) { + avatarDiv.innerHTML = `avatar`; + } else { + avatarDiv.textContent = initials || senderName?.charAt(0).toUpperCase() || '?'; + } + } + }); + } + + gameInvitationButton(chatBubble, senderName, senderId); + showOtherProfile(chatBubble, senderId); + return chatBubble; } -export { renderChatBubble }; \ No newline at end of file +async function showOtherProfile(chatBubble, senderId) { + const avatarButton = chatBubble.querySelector(".avatar"); + if (avatarButton && chatBubble.classList.contains("false")) { + avatarButton.addEventListener("click", async () => { + const messageDiv = chatBubble.querySelector('.message'); + if (messageDiv) { + // Rimuovi eventuale menu giร  presente + const existingMenu = messageDiv.querySelector('.profile-menu'); + const fakeSpan = messageDiv.querySelector('.fake-width'); + if (existingMenu) { + existingMenu.remove(); + if (fakeSpan) fakeSpan.remove(); + chatBubble.querySelector('.chat-content').classList.remove('show-profile-menu'); + messageDiv.classList.remove('show-profile-menu'); + return; + } + + chatBubble.querySelector('.chat-content').classList.add('show-profile-menu'); + messageDiv.classList.add('show-profile-menu'); + messageDiv.style.position = 'relative'; + + // Aggiungi uno span invisibile per forzare la larghezza + const span = document.createElement('span'); + span.className = 'fake-width'; + span.style.visibility = 'hidden'; + span.textContent = '[][][][][][][][][][][][][][][][][][profilo][][][][][][][][][][][][][][][][]'; + messageDiv.appendChild(span); + + // Prendi i dati dell'utente + const dataOtherUser = await takeDatafromUserId(senderId); + + // Crea il menu profilo + const menu = document.createElement('div'); + menu.className = 'profile-menu'; + menu.innerHTML = ` +

Name and Surname

+ ${dataOtherUser.first_name || '-'} ${dataOtherUser.last_name || '-'} +

Email

+ ${dataOtherUser.email || '-'} +

User ID

+ ${dataOtherUser.user_id || '-'} +

Birth Date

+ ${dataOtherUser.birth_date || '-'} +

Bio

+ ${dataOtherUser.bio || '-'} + + `; + + // menu.querySelector('.close-profile').addEventListener('click', () => { + // menu.remove(); + // span.remove(); + // chatBubble.querySelector('.chat-content').classList.remove('show-profile-menu'); + // messageDiv.classList.remove('show-profile-menu'); + // }); + + messageDiv.appendChild(menu); + } + }); + } +} + +async function takeDatafromUserId(userId) { + try { + const { token, url_api } = getVariables(); + console.log("[takeDatafromUserId] Fetching player info for user_id:", userId); + const response = await fetch( + `${url_api}/user/user/user?user_id=${userId}`, + // `${url_api}/pong/player/stats?user_id=${userId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + } + ); + const data = await response.json(); + console.log("[takeDatafromUserId] API response:", data[0]); + return data[0] || {}; + } catch (error) { + console.error("[takeDatafromUserId] API error:", error); + } +} + +function gameInvitationButton (chatBubble, senderName, senderId) { + const gameBtn = chatBubble.querySelector('.game-invitation-btn'); + if (gameBtn) { + gameBtn.addEventListener("click", () => { + console.log("Game invitation button clicked"); + + window.navigateTo(`#pongmulti?opponent=${senderId}&opponentName=${encodeURIComponent(senderName)}`); + + }); + } +} + +export { renderChatBubble, gameInvitationButton, showOtherProfile}; diff --git a/Front-End/public/chat/ExpandableSidebar.js b/Front-End/public/chat/ExpandableSidebar.js index 483814c2..3b90da57 100644 --- a/Front-End/public/chat/ExpandableSidebar.js +++ b/Front-End/public/chat/ExpandableSidebar.js @@ -2,14 +2,18 @@ import { getVariables } from '../var.js'; import { renderAddChat } from './AddChat.js'; import { renderChatBubble } from './ChatBubble.js'; import { getCookie } from '../cookie.js'; +import { getBlockedUsersList, showBlockedUsersModal, helperAutocomplete, loadBlockedUsers } from './blockUser.js'; +import { showAlertForXSeconds } from '../alert/alert.js'; const link = document.createElement('link'); link.rel = 'stylesheet'; -link.href = '/public/chat/chat.css'; +link.href = '/chat/chat.css'; document.head.appendChild(link); const displayedDates = new Set(); let addChatContainer = null; +let blockedUserListModal = null; +let blockedUsers = []; // Cache degli utenti bloccati function isFirstMessageOfDay(date) { const dateString = date.toLocaleDateString('it-IT'); @@ -20,20 +24,27 @@ function isFirstMessageOfDay(date) { return false; } +async function getBlockedUsers() { + blockedUsers = await getBlockedUsersList(); + console.log("Utenti bloccati caricati:", blockedUsers); + return blockedUsers; +} + async function getChatRooms() { - const { userId, token } = getVariables(); + const { userId, token, url_api } = getVariables(); console.log("/-----ExpandableSidebar.js-----\\"); console.log("user_id in sidechat: ", userId); console.log("Token getChatRooms:", token); - console.log(`http://localhost:8001/chat/chat_rooms/getchat/?users=${userId}`); + console.log(`${url_api}/chat/chat_rooms/getchat/`); console.log("\\_____ExpandableSidebar.js_____/"); try { const response = await fetch( - `http://localhost:8001/chat/chat_rooms/getchat/?user_id=${userId}`, + `${url_api}/chat/chat/chat_rooms/getchat/`, { method: "GET", headers: { 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), 'Authorization': `Bearer ${token}`, }, } @@ -41,20 +52,20 @@ async function getChatRooms() { if (response.ok) { const data = await response.json(); - console.log("Risposta dal server:", data); + console.log("Risposta di getChatRooms:", data); return data; } else { const errorData = await response.json(); - console.error("Errore nella risposta del server:", errorData); + console.error("Errore nella risposta di getChatRooms:", errorData); return null; } } catch (error) { - console.error("Errore nella richiesta:", error); + console.error("Errore nella richiesta di getChatRooms:", error); return null; } } -function renderExpandableSidebar() { +async function renderExpandableSidebar() { const sidebarContainer = document.querySelector('.expandable-sidebar-container'); sidebarContainer.innerHTML = ` `; @@ -199,8 +251,11 @@ function renderChatItem(chat) { if (!isOpen) { // Apri il WebSocket per la chat room - const { token } = getVariables(); - socket = new WebSocket(`ws://127.0.0.1:8001/ws/chat/${chat.id}/?token=${token}`); + const { token, wss_api } = getVariables(); + socket = new WebSocket(`${wss_api}/chat/chat?room_id=${chat.id}&token=${token}`); + console.log(socket); + if (!window.activeWebSockets) window.activeWebSockets = []; + window.activeWebSockets.push(socket); socket.onopen = () => { console.log(`WebSocket connection opened for chat room ${chat.id}`); @@ -210,6 +265,12 @@ function renderChatItem(chat) { const data = JSON.parse(event.data); console.log(`Messaggio ricevuto per chat room ${chat.id}:`, data); + // Verifica se il sender รจ bloccato + if (isUserBlocked(data.sender)) { + console.log(`Messaggio da utente bloccato ${data.sender} ignorato`); + return; // Non visualizzare il messaggio + } + const messageDate = new Date(data.timestamp); const chatContent = chatItem.querySelector('.scrollable-content'); @@ -222,10 +283,12 @@ function renderChatItem(chat) { // Aggiungi il messaggio alla chat room corrispondente const chatBubble = renderChatBubble({ - sender: data.sender, + senderName: data.sender, date: new Date(data.timestamp).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }), message: data.message, - isSingleChat: chat.type === 'single' + isSingleChat: chat.type === 'single', + type: data.type, + senderId: data.sender_id }); chatContent.appendChild(chatBubble); @@ -245,7 +308,7 @@ function renderChatItem(chat) { // Fetch old messages try { const response = await fetch( - `http://127.0.0.1:8001/chat/chat_rooms/${chat.id}/get_message/`, + `${url_api}/chat/chat/chat_rooms/${chat.id}/get_message/`, { method: "GET", headers: { @@ -259,6 +322,12 @@ function renderChatItem(chat) { const data = await response.json(); const chatContent = chatItem.querySelector('.scrollable-content'); data.forEach(msg => { + // Filtra i messaggi degli utenti bloccati anche dai messaggi storici + if (isUserBlocked(msg.sender)) { + console.log(`Messaggio storico da utente bloccato ${msg.sender} ignorato`); + return; // Non visualizzare il messaggio + } + const messageDate = new Date(msg.timestamp); if (isFirstMessageOfDay(messageDate)) { @@ -269,17 +338,20 @@ function renderChatItem(chat) { } const chatBubble = renderChatBubble({ - sender: msg.sender, + senderName: msg.sender, date: new Date(msg.timestamp).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }), message: msg.message, - isSingleChat: chat.type === 'single' + isSingleChat: chat.type === 'single', + type: msg.message_type, + senderId: msg.sender_id }); chatContent.appendChild(chatBubble); }); scrollToBottom(chatContent); } else { - console.error("Errore nella risposta del server:", response.statusText); + const text = await response.text(); + console.error("Server error in get_message:", response.status, text); } } catch (error) { console.error("Errore nella richiesta:", error); @@ -293,29 +365,101 @@ function renderChatItem(chat) { } }); - const chatsInput = chatItem.querySelector('.chats-input'); + const chatsInput = chatItem.querySelector('.chat-input'); const inputField = chatsInput.querySelector('input'); const sendButton = chatsInput.querySelector('button'); + // Modifica da apportare nel file ExpandableSidebar.js + // Nella funzione renderChatItem dove invii il messaggio + chatsInput.addEventListener('submit', function (e) { e.preventDefault(); const message = inputField.value; + console.info("Invio messaggio:", message); + const userVariables = getVariables(); if (message.trim() !== "" && socket && socket.readyState === WebSocket.OPEN) { + // Usa userUsername per identificare l'utente const messageData = { - type: "chat_message", + type: "text", room_id: chat.id, message: message, timestamp: new Date().toISOString(), - sender: getVariables().userUsername, + sender: userVariables.userUsername, }; + console.info("Invio messaggio tramite WebSocket:", messageData); socket.send(JSON.stringify(messageData)); inputField.value = ''; scrollToBottom(chatItem.querySelector('.scrollable-content')); + } else if (message.trim() == "" && socket && socket.readyState === WebSocket.OPEN) { + chatsInput.classList.add("shake"); + setTimeout(() => { + chatsInput.classList.remove("shake"); + }, 400); } else { - alert("Connessione WebSocket non attiva"); + showAlertForXSeconds( + "WebSocket connection not open", + "error", + 3, + { asToast: true } + ); } }); + + const pongInviteButton = chatsInput.querySelector('#pongInvite'); + + pongInviteButton.addEventListener('click', function (e) { + e.preventDefault(); + console.info("Pong invite button clicked"); + + const message = inputField.value; + console.info("Invio messaggio:", message); + const userVariables = getVariables(); + if (message.trim() !== "" && socket && socket.readyState === WebSocket.OPEN) { + // Usa userUsername per identificare l'utente + const messageData = { + type: "game_invitation", + room_id: chat.id, + message: message, + timestamp: new Date().toISOString(), + sender: userVariables.userUsername, + }; + console.info("Invio messaggio tramite WebSocket:", messageData); + socket.send(JSON.stringify(messageData)); + inputField.value = ''; + + scrollToBottom(chatItem.querySelector('.scrollable-content')); + } else if (message.trim() == "" && socket && socket.readyState === WebSocket.OPEN) { + chatsInput.classList.add("shake"); + setTimeout(() => { + chatsInput.classList.remove("shake"); + }, 400); + } else { + showAlertForXSeconds( + "WebSocket connection not open", + "error", + 3, + { asToast: true } + ); + } + + }); + + +} + +function isUserBlocked(username) { + return blockedUsers.some(user => user.username === username); +} + +// Funzione per aggiornare la lista degli utenti bloccati (da chiamare quando si blocca/sblocca un utente) +async function updateBlockedUsers() { + await getBlockedUsers(); + console.log("Lista utenti bloccati aggiornata"); + if (blockedUserListModal) { + await loadBlockedUsers(blockedUserListModal); + } + console.log("Lista utenti bloccati aggiornata"); } -export { renderExpandableSidebar, updateChatList }; \ No newline at end of file +export { renderExpandableSidebar, updateChatList, updateBlockedUsers, isUserBlocked }; \ No newline at end of file diff --git a/Front-End/public/chat/blockUser.js b/Front-End/public/chat/blockUser.js new file mode 100644 index 00000000..8b3100f0 --- /dev/null +++ b/Front-End/public/chat/blockUser.js @@ -0,0 +1,245 @@ +import { getVariables } from '../var.js'; +import { getCookie } from '../cookie.js'; +import { showAlertForXSeconds } from '../alert/alert.js'; +import { updateBlockedUsers, updateChatList } from './ExpandableSidebar.js'; +import { initFriendAutocomplete } from '../notification/friendAutocomplete.js'; + +const link = document.createElement('link'); +link.rel = 'stylesheet'; +link.href = '/chat/chat.css'; +document.head.appendChild(link); + +/** + * Blocca un utente + * @param {number} userId - ID dell'utente da bloccare + * @param {string} username - Username dell'utente da bloccare + */ +async function blockUser(userId, username) { + const { token, url_api } = getVariables(); + + try { + const response = await fetch( + `${url_api}/chat/chat/block_user/${userId}`, + { + method: "POST", + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (response.ok) { + const data = await response.json(); + showAlertForXSeconds( + `${username} has been blocked successfully`, + "success", + 3, + { asToast: true } + ); + + await updateBlockedUsers(); + await updateChatList(); + + return true; + } else { + const errorData = await response.json(); + showAlertForXSeconds( + errorData.error || "Failed to block user", + "error", + 3, + { asToast: true } + ); + return false; + } + } catch (error) { + console.error("Error blocking user:", error); + showAlertForXSeconds( + "Network error while blocking user", + "error", + 3, + { asToast: true } + ); + return false; + } +} + +/** + * Sblocca un utente + * @param {number} userId - ID dell'utente da sbloccare + * @param {string} username - Username dell'utente da sbloccare + */ +async function unblockUser(userId, username) { + const { token, url_api } = getVariables(); + + try { + const response = await fetch( + `${url_api}/chat/chat/block_user/${userId}`, + { + method: "DELETE", + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (response.ok) { + const data = await response.json(); + showAlertForXSeconds( + `${username} has been unblocked successfully`, + "success", + 3, + { asToast: true } + ); + + // Aggiorna la lista degli utenti bloccati + await updateBlockedUsers(); + await updateChatList(); + + return true; + } else { + const errorData = await response.json(); + showAlertForXSeconds( + errorData.error || "Failed to unblock user", + "error", + 3, + { asToast: true } + ); + return false; + } + } catch (error) { + console.error("Error unblocking user:", error); + showAlertForXSeconds( + "Network error while unblocking user", + "error", + 3, + { asToast: true } + ); + return false; + } +} + +/** + * Ottiene la lista degli utenti bloccati + */ +async function getBlockedUsersList() { + const { token, url_api } = getVariables(); + + try { + const response = await fetch( + `${url_api}/chat/chat/blocked_users/`, + { + method: "GET", + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (response.ok) { + const data = await response.json(); + console.log("Blocked users data:", data); + return data || []; + } else if (response.status === 404) { + // Nessun utente bloccato + return []; + } else { + console.error("Error fetching blocked users:", response.status); + return []; + } + } catch (error) { + console.error("Error fetching blocked users:", error); + return []; + } +} + +/** + * Crea un'interfaccia per gestire gli utenti bloccati + */ +function showBlockedUsersModal() { + // Crea il modal + const blockedUserListModal = document.createElement('div'); + blockedUserListModal.className = 'add-chat'; + blockedUserListModal.innerHTML = ` +

Blocked Users

+
+
+ `; + + loadBlockedUsers(blockedUserListModal); + + blockedUserListModal.innerHTML += ` +
+ + +
+ `; + + return blockedUserListModal; +} + + + +function helperAutocomplete(blockedUserListModal) { + setTimeout(() => { + initFriendAutocomplete({ + inputId: "blockUserInput", + suggestionListId: "blockUserSuggestionList" + }); +}, 0); +} + +/** + * Carica e mostra gli utenti bloccati nel modal + */ +async function loadBlockedUsers(blockedUserListModal) { + const blockedUsers = await getBlockedUsersList(); + const listContainer = blockedUserListModal.querySelector('#blockedUsersList'); + + if (blockedUsers.length === 0) { + listContainer.innerHTML = ` +
+ +

No blocked users

+
+ `; + return; + } + + listContainer.innerHTML = blockedUsers.map(user => ` +
+
+ ${user.username} +
+ +
+ `).join(''); +} + +/** + * Funzione globale per sbloccare utente dal modal + */ +window.unblockUserFromModal = async function(userId, username) { + const success = await unblockUser(userId, username); + if (success) { + // Ricarica la lista nel modal + const modal = document.querySelector('.login-box-modal'); + if (modal) { + loadBlockedUsers(modal); + } + } +}; + +// Rende le funzioni disponibili globalmente +window.blockUser = blockUser; +// window.unblockUser = unblockUser; +// window.showBlockedUsersModal = showBlockedUsersModal; + +export { blockUser, unblockUser, getBlockedUsersList, showBlockedUsersModal, helperAutocomplete, loadBlockedUsers }; diff --git a/Front-End/public/chat/chat.css b/Front-End/public/chat/chat.css index c8f77b41..20bad13a 100644 --- a/Front-End/public/chat/chat.css +++ b/Front-End/public/chat/chat.css @@ -1,15 +1,109 @@ +.shake { + animation: shake-anim 0.4s; +} +@keyframes shake-anim { + 0% { transform: translateX(0); } + 20% { transform: translateX(-4px); } + 40% { transform: translateX(4px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } + 100% { transform: translateX(0); } +} .chat-container { display: none; padding: 10px; overflow-y: auto; scrollbar-width: none; - width: 300px; + width: 400px; +} + +.profile-menu { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 9999; + background: rgb(255, 255, 255); + border: 0px; + height: 300px; + overflow-y: hidden; + border-top: 1px solid #dfdfdf; +} + +.profile-menu p { + padding: 0px; + margin: 0px; +} + +.profile-menu b { + padding: 0px; + display: block; + margin-bottom: 5px; + overflow-y: auto; + max-height: 3em; /* o l'altezza che preferisci */ + white-space: normal; +} + +.chat-content.show-profile-menu { + height: 350px; + width: 100%; + position: relative; +} + +.message.show-profile-menu { + max-width: 100%; + width: max-content; +} + +.chat-content { + transition: height 0.3s ease; +} + +.other-profile { + z-index: 10; + width: 100%; + background: #f8f8ff; + box-shadow: none; + padding: 16px; + margin-top: 10px; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: flex-start; + position: absolute; } +.shake { + animation: shake-anim 0.4s; +} +@keyframes shake-anim { + 0% { transform: translateX(0); } + 20% { transform: translateX(-4px); } + 40% { transform: translateX(4px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } + 100% { transform: translateX(0); } +} + + .chat-container.open { display: block; } +@media (max-width: 500px) { + .chat-container.open { + position: absolute; + left: 60px; + top: 0; + z-index: 1000; + height: 100%; + width: calc(100vw - 60px); + box-shadow: -3px 0 10px rgba(0, 0, 0, 0.2); + background-color: white; + border-radius: 12px; + } +} + .chat-item { background-color: #f3f4f6; color: #1f2937; @@ -21,12 +115,29 @@ } .scrollable-content { - height: 300px; + position: relative; + height: 500px; display: flex; flex-direction: column; flex: 1 1; padding: 10px; overflow-y: scroll; + + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.3) transparent; +} + +.scrollable-content::-webkit-scrollbar { + width: 6px; +} + +.scrollable-content::-webkit-scrollbar-track { + background: transparent; +} + +.scrollable-content::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.3); + border-radius: 3px; } .chat-item-header { @@ -48,6 +159,9 @@ width: 40px; margin-right: 10px; min-width: 40px; + border: none; + padding: 0; + background-color: transparent; } .avatar-placeholder { @@ -84,28 +198,31 @@ } .chat-bubble { + z-index: 20; display: flex; align-items: flex-start; margin-bottom: 10px; - max-width: 60%; + max-width: 75%; position: relative; } .chat-bubble.true { + z-index: 20; display: flex; align-items: flex-end; flex-direction: column; margin-left: auto; - + } .chat-bubble.false { + z-index: 20; align-self: flex-start; margin-right: auto; } .chat-content { - background-color: #f3f4f6; + background-color: #ffffff; padding: 10px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); @@ -116,6 +233,7 @@ display: flex; flex-direction: column; align-items: flex-end; + background-color: #ffffff; } .chat-bubble.true .chat-content { @@ -194,26 +312,30 @@ } .add-chat { - padding: 16px; - display: flex; - flex-direction: column; - gap: 8px; - - background-color: #f3f4f6; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - width: 100%; /* Assicura che la larghezza rimanga costante */ - margin-bottom: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + + background-color: #f3f4f6; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + width: 100%; /* Assicura che la larghezza rimanga costante */ + margin-bottom: 8px; } .add-chat input { - padding: 8px; - border: 1px solid var(--border); - border-radius: 4px; + padding: 8px; + border: 1px solid var(--border); + border-radius: 4px; } .right-button { display: flex; justify-content: flex-end; +} + +.chat-input{ + padding: 1rem; } \ No newline at end of file diff --git a/Front-End/public/chat/group.jpg b/Front-End/public/chat/group.jpg new file mode 100644 index 00000000..faf8de13 Binary files /dev/null and b/Front-End/public/chat/group.jpg differ diff --git a/Front-End/public/favicon/site.webmanifest b/Front-End/public/favicon/site.webmanifest index 45dc8a20..39153950 100644 --- a/Front-End/public/favicon/site.webmanifest +++ b/Front-End/public/favicon/site.webmanifest @@ -1 +1,19 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "Trascendence", + "short_name": "Trascendence", + "icons": [ + { + "src": "/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/Front-End/public/home/home.css b/Front-End/public/home/home.css index 16f20540..211a7588 100644 --- a/Front-End/public/home/home.css +++ b/Front-End/public/home/home.css @@ -6,30 +6,25 @@ align-items: flex-start; flex-direction: column; justify-content: flex-start; - border-top-left-radius: 12px; + border-radius: 12px; padding: 10px; } -.navbar { - width: 100%; - height: 50px; - display: flex; - justify-content: space-between; - align-items: center; -} - .undernavbar { flex: 1; width: 100vw; display: flex; - height: calc(100vh - 50px); + height: calc(100vh - 60px); } .expandable-sidebar-container { display: flex; height: 100%; transition: width 0.3s; + position: relative; + width: 80%; + max-width: max-content; } .sidebar { @@ -41,22 +36,41 @@ } .content-home { - background-color: rgba(1, 25, 78, 0.05); + background-color: rgb(221 223 237); display: flex; flex-wrap: wrap; justify-content: space-evenly; align-content: flex-start; gap: 10px; + padding: 10px; overflow-y: auto; height: 100%; - border-top-left-radius: 12px; + border-radius: 12px; + box-shadow: + inset 4px 4px 8px rgba(0, 0, 0, 0.2), /* Ombra scura in alto a sinistra (interno) */ + inset -2px -2px 4px rgba(240, 237, 237, 0.7); /* Luce in basso a destra (interno) */ +} +.content-home::-webkit-scrollbar { + width: 10px; + background: #00000000; + border-radius: 8px; +} +.content-home::-webkit-scrollbar-thumb { + background: #00000000; + border-radius: 8px; +} +.content-home::-webkit-scrollbar-thumb:hover { + background: #00000000; } -p { - all: unset; +/* Firefox */ +.content-home { + scrollbar-width: thin; + scrollbar-color: #9a9a9a #00000000; } + .btn-square { width: 40px; /* Imposta una larghezza fissa */ height: 40px; /* Imposta un'altezza fissa */ @@ -69,32 +83,30 @@ p { .task-container { flex: 1 1 45%; - min-width: 300px; max-width: 100%; + min-width: 168px; border-radius: 8px; + background-color: #ffffff; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; } -.profile { - flex: 1 1 100%; - min-width: 300px; - max-width: 100%; - border-radius: 8px; +.task-container.gap { + margin-bottom: 10px; } -.logo-image { - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding-left: 12px; +.task-double-container { + flex: 1 1 45%; + max-width: 100%; + min-width: 168px; + gap: 10px; } + -.propic-image { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; +.profile { + flex: 1 1 100%; + max-width: 100%; + border-radius: 8px; } .propic-button { diff --git a/Front-End/public/home/home.js b/Front-End/public/home/home.js index 99b5b9db..3629de22 100644 --- a/Front-End/public/home/home.js +++ b/Front-End/public/home/home.js @@ -1,59 +1,86 @@ -import { renderTaskAvaiable } from '../task/taskAvaiable.js'; -import { renderTaskActive } from '../task/taskActive.js'; -import { renderNotification, initializeWebSocket } from '../notification/notification.js'; -import { renderPongInfo } from '../pong/pongContainer.js'; -import { renderProfile } from '../profile/profile.js'; +import { renderTaskAvaiable } from "../task/taskAvaiable.js"; +import { renderTaskActive } from "../task/taskActive.js"; +import { + renderNotification, + initializeWebSocket, +} from "../notification/notification.js"; +import { renderPongInfo } from "../pong/pongContainer.js"; +import { initializeProfile, renderProfile } from "../profile/profile.js"; +import { renderTournament } from "../pong/tournament.js"; +import { renderMatchHistory } from "../pong/matchHistory.js"; const stylesheets = [ - '/public/home/home.css', - '/public/task/taskAvaiable.css', - '/public/task/taskActive.css', - '/public/notification/notification.css', - '/public/profile/profile.css', - '/public/chat/ExpandableSidebar.css', - '/public/pong/pongContainer.css' + "home/home.css", + "task/taskAvaiable.css", + "task/taskActive.css", + "notification/notification.css", + "profile/profile.css", + "chat/ExpandableSidebar.css", + "pong/pongContainer.css", ]; -stylesheets.forEach(href => { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = href; - document.head.appendChild(link); +stylesheets.forEach((href) => { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = href; + document.head.appendChild(link); }); +async function toggleProfile() { + const profileDiv = document.getElementById("profile"); + if (profileDiv) { + if (profileDiv.style.display === "block") { + profileDiv.style.display = "none"; + } else { + profileDiv.style.display = "block"; + await initializeProfile(); + } + } else { + console.error("Elemento #profile non trovato nel DOM"); + } +} + function renderHome() { const contentDiv = document.getElementById('content'); contentDiv.innerHTML = ` - - -
- -
-
-
-
-
- - +
+
+ + +
+
+
+
+
+ +
+
+
+
`; - renderTaskAvaiable(); - renderTaskActive(); - renderNotification(); - initializeWebSocket(); - renderPongInfo(); - - const toggleProfileButton = document.getElementById('toggleProfileButton'); - const profileDiv = document.getElementById('profile'); - - toggleProfileButton.addEventListener('click', () => { - if (profileDiv.style.display === 'none') { - profileDiv.style.display = 'block'; - renderProfile(); - } else { - profileDiv.style.display = 'none'; - } - }); + //renderTaskAvaiable(); + //renderTaskActive(); + renderNotification(); + initializeWebSocket(); + renderPongInfo(); + initializeProfile(); + renderTournament(); + renderMatchHistory(); + + // Se esiste un elemento .content, resetta il margine e rimuovi classi di animazione + const animatedContent = document.querySelector('.content'); + if (animatedContent) { + animatedContent.classList.remove('animate-margin', 'animate-margin-logout'); + animatedContent.style.margin = '0 10px 10px 0'; + } } -export { renderHome }; \ No newline at end of file +// 5notification.js:1473 WebSocket is not open, cannot send heartbeat +// sendHeartBeat @ notification.js:1473 +// (anonymous) @ notification.js:1467 +// setInterval +// startHeartbeat @ notification.js:1466 +// socket.onopen @ notification.js:1021Understand this warning + +export { renderHome, toggleProfile }; diff --git a/Front-End/public/index.css b/Front-End/public/index.css index 03246f28..9a79db38 100644 --- a/Front-End/public/index.css +++ b/Front-End/public/index.css @@ -1,6 +1,82 @@ -.content { +@keyframes animateMarginToBig { + from { + margin: 0 10px 10px 0; + } + to { + margin: 12% 41% 15% 38%; + } +} + +.animate-margin-logout { + animation: animateMarginToBig 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + overflow: hidden; +} + + +.App{ + display: flex; + flex-direction: column; + height: 100vh; + min-width: 200px; + min-height: 350px; +} +.logo-image { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; height: 100%; +} +.propic-image { width: 100%; - border-top-left-radius: 12px; + height: 100%; + border-radius: 50%; + object-fit: cover; + min-width: 30px; +} +.navbar { + width: 100%; + height: 60px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: nowrap; + padding: 8px !important; +} +.expandable-sidebar-container { + display: flex; + height: 100%; + min-width: 60px; +} +.content { + background-color: rgb(221 223 237); + width: 100%; + border-radius: 12px; + position: relative; + margin-bottom: 10px; + margin-right: 10px; + box-shadow: inset 4px 4px 8px rgba(0, 0, 0, 0.2), inset -2px -2px 4px rgba(240, 237, 237, 0.7); + min-height: 200px; + min-width: 200px; + margin: 12% 41% 15% 38%; + } + /* background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); */ + +@keyframes animateMarginToZero { + from { + margin: 12% 41% 15% 38%; + } + to { + margin: 0 10px 10px 0; + } +} +.animate-margin { + animation: animateMarginToZero 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards; } \ No newline at end of file diff --git a/Front-End/public/index.js b/Front-End/public/index.js index 8c8f0e7e..e992b5bd 100644 --- a/Front-End/public/index.js +++ b/Front-End/public/index.js @@ -1,69 +1,305 @@ -import { renderLogin } from './login/login.js'; -import { renderRegister } from './register/register.js'; -import { renderHome } from './home/home.js'; -import { renderPong } from './pong/pong.js'; -import { renderExpandableSidebar } from './chat/ExpandableSidebar.js'; +import { renderLogin } from "./login/login.js"; +import { renderRegister } from "./register/register.js"; +import { renderHome } from "./home/home.js"; +import { renderPong } from "./pong/locale/pong.js"; +import { renderExpandableSidebar } from "./chat/ExpandableSidebar.js"; + +function removeExpandableSidebar() { + // Rimuovi sidebar chat + const sidebarContainer = document.querySelector('.expandable-sidebar-container'); + if (sidebarContainer) { + sidebarContainer.innerHTML = ''; + } + // Rimuovi pulsante impostazioni + const toggleSettingsButton = document.getElementById('toggleSettingsButton'); + if (toggleSettingsButton) { + toggleSettingsButton.style.display = 'none'; + } +} //import { renderProfile } from './profile/profile.js'; +import { settingsPopup, Logout as OriginalLogout } from "./settings/settings.js"; +import { cleanupPong } from "./pong/locale/settings.js"; +import { getVariables, setVariables } from "./var.js"; + +function preloadPongCSS() { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "/pong/locale/pong.css"; + document.head.appendChild(link); + console.log("Pong CSS precaricato"); +} const routes = { - 404: { - template: "/templates/404.html", - title: "404", - description: "Page not found", - }, - "/": { - render: renderLogin, - title: "Login", - description: "This is the login page", - }, - login: { - render: renderLogin, - title: "Login", - description: "This is the login page", - }, - register: { - render: renderRegister, - title: "Register", - description: "This is the register page", - }, - home: { - render: renderHome, - title: "Home", - description: "This is the home page", - }, - pong: { - render: renderPong, - title: "Pong", - description: "This is the pong game", - } + 404: { + template: "/templates/404.html", + title: "404", + description: "Page not found", + protected: false, + }, + "/": { + render: renderLogin, + title: "Login", + description: "This is the login page", + protected: false, + }, + login: { + render: renderLogin, + title: "Login", + description: "This is the login page", + protected: false, + }, + register: { + render: renderRegister, + title: "Register", + description: "This is the register page", + protected: false, + }, + home: { + render: renderHome, + title: "Home", + description: "This is the home page", + protected: true, + }, + pong: { + render: renderPong, + // module: () => import("./pong/locale/pong.js").then(module => module.renderPong), + title: "Pong", + description: "This is the pong game", + protected: true, + }, + pongmulti: { + render: async () => { + console.log("Multiplayer pong route accessed"); + + // Extract parameters from the hash, not from window.location.search + const hashParts = window.location.hash.replace("#", "").split("?"); + const queryString = hashParts[1] || ""; + const urlParams = new URLSearchParams(queryString); + + const roomId = urlParams.get("room"); + const opponentId = urlParams.get("opponent"); + const opponentName = urlParams.get("opponentName"); + const tournamentId = urlParams.get("tournamentId"); + + console.log("Route params:", { roomId, opponentId, opponentName, tournamentId }); + + if (opponentId && opponentName) { + // Load multiplayer game with invitation data + try { + const { renderMultiplayerPong } = await import( + "./pong/multiplayer/multiplayerPong.js" + ); + renderMultiplayerPong( + opponentId, + decodeURIComponent(opponentName), + roomId || null, + tournamentId || null + ); + } catch (error) { + console.error("Failed to load multiplayer pong:", error); + window.navigateTo("#home"); + } + } + }, + title: "Multiplayer Pong", + description: "Multiplayer Pong Game", + protected: true, + }, }; + const navigateTo = (path) => { - window.location.hash = path; + window.previousRoute = currentRoute; + window.location.hash = path; }; +// Override Logout to also remove sidebar +function Logout() { + removeExpandableSidebar(); + OriginalLogout(); +} + +import { processOAuthRedirect } from "./var.js"; +let currentRoute = ""; +let previousRoute = ""; + const locationHandler = async () => { - var location = window.location.hash.replace("#", ""); - if (location.length == 0) { - location = "login"; - } - const route = routes[location] || routes["404"]; - if (route.template) { - const html = await fetch(route.template).then((response) => response.text()); - document.getElementById("content").innerHTML = html; - } else if (route.render) { - route.render(); - } - document.title = route.title; - document.querySelector('meta[name="description"]').setAttribute("content", route.description); + const wasOAuthRedirect = processOAuthRedirect(); + + // Extract the hash and separate route from query parameters + let fullLocation = window.location.hash.replace("#", ""); + let location = fullLocation; + let queryParams = ""; + + // Split route from query parameters + if (fullLocation.includes("?")) { + [location, queryParams] = fullLocation.split("?", 2); + } + + console.log("๐Ÿ”ง Routing Debug:"); + console.log(" - Full hash:", fullLocation); + console.log(" - Route:", location); + console.log(" - Query params:", queryParams); + + if (routes[location]?.protected && !checkAuth()) { + console.log("Utente non autenticato, reindirizzamento a login"); + Logout(); + return; + } + + if (location === "login" && checkAuth()) { + console.log("Utente autenticato, reindirizzamento a home"); + window.location.hash = "home"; + return; + } + + if (location.length == 0) { + window.location.hash = "login"; + return; + } + + // Check if the route exists (without query parameters) + if (!routes[location]) { + console.log(`Route "${location}" non trovata, reindirizzamento a login`); + window.location.hash = "login"; + return; + } + + if ( + (currentRoute === "pong" && location !== "pong") || + (currentRoute === "pongmulti" && location !== "pongmulti") + ) { + console.log("Navigando via da Pong, eseguo cleanup..."); + cleanupPong(); + } + +if (routes[location]?.protected && checkAuth()) { + renderExpandableSidebar(); + // Mostra il pulsante impostazioni se presente + const toggleSettingsButton = document.getElementById('toggleSettingsButton'); + if (toggleSettingsButton) toggleSettingsButton.style.display = ''; +} + + previousRoute = currentRoute; + currentRoute = location; + + if (wasOAuthRedirect && location === "home") { + console.log("OAuth login successful!"); + } + + // Use the route without query parameters + const route = routes[location] || routes["404"]; + if (route.module) { + console.log(`Caricamento dinamico del modulo per: ${location}`); + try { + const renderFunction = await route.module(); + renderFunction(); + } catch (error) { + console.error("Errore nel caricamento del modulo %s:", location, error); + } + } else if (route.template) { + const html = await fetch(route.template).then((response) => + response.text() + ); + document.getElementById("content").innerHTML = html; + } else if (route.render) { + route.render(); + } + document.title = route.title; + document + .querySelector('meta[name="description"]') + .setAttribute("content", route.description); }; -const initializeApp = () => { - renderExpandableSidebar(); - //renderProfile(); - locationHandler(); +const initializeApp = async () => { + console.log("Inizializzazione dell'applicazione..."); + console.log("URL corrente:", window.location.href); + console.log("Hash:", window.location.hash); + console.log("Search:", window.location.search); + + if (getVariables().url_api === null || getVariables().url_api === "") { + const apiUrl = window.location.origin + "/api"; + const wssUrl = + window.location.origin + .replace("https://", "wss://") + .replace("http://", "ws://") + "/api"; + setVariables({ + url_api: apiUrl, + wss_api: wssUrl, + }); + } + + if ( + window.location.pathname !== "/" && + window.location.pathname !== "/index.html" + ) { + const currentHash = window.location.hash; + window.location.href = "/" + currentHash; + return; + } + + preloadPongCSS(); + + + +if (checkAuth()) { + renderExpandableSidebar(); +} + + const toggleSettingsButton = document.getElementById("toggleSettingsButton"); + if (toggleSettingsButton) { + toggleSettingsButton.addEventListener("click", (event) => { + event.preventDefault(); + settingsPopup(event); + }); + } + + const currentHash = window.location.hash; + if (currentHash === "#pong") { + console.log("Rilevato refresh su pagina Pong, forzo il rendering..."); + await cleanupPong(); + } + + locationHandler(); }; +function checkAuth() { + const token = getVariables().token; + if (token && token.length > 0) { + try { + const payload = JSON.parse(atob(token.split(".")[1])); + const currentTime = Math.floor(Date.now() / 1000); + return payload.exp >= currentTime; + } catch { + return false; + } + } + return false; +} + + window.addEventListener("hashchange", locationHandler); window.addEventListener("load", initializeApp); -window.navigateTo = navigateTo; \ No newline at end of file +window.navigateTo = navigateTo; +window.removeExpandableSidebar = removeExpandableSidebar; +// Esporta anche per uso esterno +export { removeExpandableSidebar, Logout }; + +export function getBaseUrl() { + const hostname = window.location.hostname; + const port = window.location.port || "8443"; + + if ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "10.0.2.15" + ) { + return `http://localhost.xip.io:${port}`; + } else if (hostname.startsWith("10.11.")) { + return `http://${hostname}.xip.io:${port}`; + } else if (hostname.includes(".xip.io")) { + return `http://${hostname}:${port}`; + } else { + return `http://${hostname}.xip.io:${port}`; + } +} diff --git a/Front-End/public/js/friends-integration.js b/Front-End/public/js/friends-integration.js new file mode 100644 index 00000000..80e22b3b --- /dev/null +++ b/Front-End/public/js/friends-integration.js @@ -0,0 +1,263 @@ +/** + * Example integration for Friend List page + */ +class FriendListManager { + constructor() { + this.friends = []; + this.container = null; + } + + async init(containerId) { + this.container = document.getElementById(containerId); + await this.loadFriends(); + this.setupRealTimeUpdates(); + } + + async loadFriends() { + try { + const response = await fetch('/api/friends/?status=accepted', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` + } + }); + + if (response.ok) { + this.friends = await response.json(); + this.renderFriends(); + + // Initialize online status for all friends + const friendIds = this.getFriendIds(); + await window.onlineStatusManager.initializeUsersStatus(friendIds); + } + } catch (error) { + console.error('Error loading friends:', error); + } + } + + getFriendIds() { + return this.friends.map(friendship => { + // Determine which user is the friend + const currentUserId = getCurrentUserId(); // Implement this function + return friendship.user_1.user_id === currentUserId + ? friendship.user_2.user_id + : friendship.user_1.user_id; + }); + } + + renderFriends() { + if (!this.container) return; + + this.container.innerHTML = ''; + + this.friends.forEach(friendship => { + const currentUserId = getCurrentUserId(); + const friend = friendship.user_1.user_id === currentUserId + ? friendship.user_2 + : friendship.user_1; + + const friendElement = this.createFriendElement(friend); + this.container.appendChild(friendElement); + }); + } + + createFriendElement(friend) { + const div = document.createElement('div'); + div.className = 'friend-item'; + div.setAttribute('data-user-id', friend.user_id); + + const isOnline = window.onlineStatusManager.isUserOnline(friend.user_id); + const statusIndicator = createOnlineStatusIndicator(friend.user_id, isOnline); + + div.innerHTML = ` +
+ ${friend.username} +
+

${friend.username}

+ + ${isOnline ? 'Online' : 'Offline'} + +
+
+
+ + +
+ `; + + // Add status indicator + div.querySelector('.friend-details').appendChild(statusIndicator); + + return div; + } + + setupRealTimeUpdates() { + // Subscribe to status changes for all friends + this.getFriendIds().forEach(friendId => { + window.onlineStatusManager.onStatusChange(friendId, (userId, isOnline) => { + this.updateFriendStatus(userId, isOnline); + }); + }); + } + + updateFriendStatus(userId, isOnline) { + const friendElement = this.container.querySelector(`[data-user-id="${userId}"]`); + if (friendElement) { + const statusText = friendElement.querySelector('.status-text'); + if (statusText) { + statusText.textContent = isOnline ? 'Online' : 'Offline'; + statusText.className = `status-text ${isOnline ? 'online' : 'offline'}`; + } + } + } +} + +/** + * Example integration for User Search + */ +class UserSearchManager { + constructor() { + this.searchResults = []; + this.container = null; + this.searchInput = null; + } + + init(searchInputId, resultsContainerId) { + this.searchInput = document.getElementById(searchInputId); + this.container = document.getElementById(resultsContainerId); + + this.setupSearchHandler(); + } + + setupSearchHandler() { + let searchTimeout; + + this.searchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + this.performSearch(e.target.value); + }, 500); // Debounce search + }); + } + + async performSearch(query) { + if (query.length < 2) { + this.container.innerHTML = ''; + return; + } + + try { + const response = await fetch(`/api/users/search/?search=${encodeURIComponent(query)}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.searchResults = data.results || []; + this.renderSearchResults(); + + // Initialize online status for search results + const userIds = this.searchResults.map(user => user.user_id); + if (userIds.length > 0) { + await window.onlineStatusManager.initializeUsersStatus(userIds); + } + } + } catch (error) { + console.error('Error searching users:', error); + } + } + + renderSearchResults() { + if (!this.container) return; + + this.container.innerHTML = ''; + + if (this.searchResults.length === 0) { + this.container.innerHTML = '

No users found

'; + return; + } + + this.searchResults.forEach(user => { + const userElement = this.createUserElement(user); + this.container.appendChild(userElement); + }); + } + + createUserElement(user) { + const div = document.createElement('div'); + div.className = 'user-search-item'; + div.setAttribute('data-user-id', user.user_id); + + const isOnline = user.is_online || window.onlineStatusManager.isUserOnline(user.user_id); + const statusIndicator = createOnlineStatusIndicator(user.user_id, isOnline); + + div.innerHTML = ` + + + `; + + // Add status indicator + div.querySelector('.user-details').appendChild(statusIndicator); + + // Subscribe to real-time status updates + window.onlineStatusManager.onStatusChange(user.user_id, (userId, isOnline) => { + const statusText = div.querySelector('.status-text'); + if (statusText) { + statusText.textContent = isOnline ? 'Online' : 'Offline'; + statusText.className = `status-text ${isOnline ? 'online' : 'offline'}`; + } + }); + + return div; + } +} + +// Global functions for easy access +function getCurrentUserId() { + // Implement based on your authentication system + const token = localStorage.getItem('access_token'); + if (token) { + try { + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.user_id; + } catch (e) { + return null; + } + } + return null; +} + +function startChat(userId) { + // Implement chat functionality + window.location.href = `/chat/?user=${userId}`; +} + +function inviteToGame(userId) { + // Implement game invitation + console.log('Inviting user to game:', userId); +} + +function sendFriendRequest(userId) { + // Implement friend request + console.log('Sending friend request to:', userId); +} + +function viewProfile(userId) { + // Implement profile view + window.location.href = `/profile/${userId}`; +} diff --git a/Front-End/public/js/online-status.js b/Front-End/public/js/online-status.js new file mode 100644 index 00000000..1b81c6dc --- /dev/null +++ b/Front-End/public/js/online-status.js @@ -0,0 +1,362 @@ +/** + * OnlineStatusManager - Handles real-time online status for users + * Integrates with NotificationWebSocket for heartbeat and status updates + */ +class OnlineStatusManager { + constructor() { + this.onlineUsers = new Map(); // user_id -> boolean + this.statusCallbacks = new Map(); // user_id -> [callback functions] + this.notificationWS = null; + this.heartbeatInterval = null; + this.lastHeartbeat = Date.now(); + } + + /** + * Initialize with notification WebSocket + */ + init(notificationWebSocket) { + this.notificationWS = notificationWebSocket; + this.setupMessageHandlers(); + this.startHeartbeat(); + } + + /** + * Setup message handlers for WebSocket + */ + setupMessageHandlers() { + if (!this.notificationWS) return; + + // Add our message handler to the notification WebSocket + const originalHandleMessage = this.notificationWS.handleMessage.bind(this.notificationWS); + this.notificationWS.handleMessage = (data) => { + // Handle online status messages + this.handleOnlineStatusMessage(data); + // Call original handler for other messages + originalHandleMessage(data); + }; + } + + /** + * Handle online status related WebSocket messages + */ + handleOnlineStatusMessage(data) { + switch(data.type) { + case 'heartbeat_request': + this.sendHeartbeat(); + break; + + case 'heartbeat_ack': + this.lastHeartbeat = Date.now(); + break; + + case 'friend_status_update': + // Real-time friend status change + this.updateUserStatus(data.user_id, data.is_online); + this.showStatusChangeNotification(data.user_id, data.is_online); + break; + + case 'friends_online_status': + // Batch update of friends' status + this.updateMultipleUserStatus(data.friends); + break; + } + } + + /** + * Start heartbeat mechanism + */ + startHeartbeat() { + // Send heartbeat every 25 seconds + this.heartbeatInterval = setInterval(() => { + this.sendHeartbeat(); + }, 25000); + } + + /** + * Send heartbeat to server + */ + sendHeartbeat() { + if (this.notificationWS && this.notificationWS.ws && + this.notificationWS.ws.readyState === WebSocket.OPEN) { + this.notificationWS.ws.send(JSON.stringify({type: 'heartbeat'})); + } + } + + /** + * Update single user online status + */ + updateUserStatus(userId, isOnline) { + const userIdStr = userId.toString(); + const wasOnline = this.onlineUsers.get(userIdStr); + + if (wasOnline !== isOnline) { + this.onlineUsers.set(userIdStr, isOnline); + this.notifyStatusChange(userIdStr, isOnline); + this.updateUIForUser(userIdStr, isOnline); + } + } + + /** + * Update multiple users' online status + */ + updateMultipleUserStatus(statusMap) { + for (const [userId, isOnline] of Object.entries(statusMap)) { + this.updateUserStatus(userId, isOnline); + } + } + + /** + * Get user's online status + */ + isUserOnline(userId) { + return this.onlineUsers.get(userId.toString()) || false; + } + + /** + * Subscribe to status changes for a user + */ + onStatusChange(userId, callback) { + const userIdStr = userId.toString(); + if (!this.statusCallbacks.has(userIdStr)) { + this.statusCallbacks.set(userIdStr, []); + } + this.statusCallbacks.get(userIdStr).push(callback); + + // Return unsubscribe function + return () => { + const callbacks = this.statusCallbacks.get(userIdStr); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index > -1) callbacks.splice(index, 1); + } + }; + } + + /** + * Notify all callbacks about status change + */ + notifyStatusChange(userId, isOnline) { + const callbacks = this.statusCallbacks.get(userId); + if (callbacks) { + callbacks.forEach(callback => { + try { + callback(userId, isOnline); + } catch (error) { + console.error('Error in status change callback:', error); + } + }); + } + } + + /** + * Update UI elements for user status + */ + updateUIForUser(userId, isOnline) { + // Update all elements with data-user-id attribute + const elements = document.querySelectorAll(`[data-user-id="${userId}"]`); + elements.forEach(element => { + const statusIndicator = element.querySelector('.online-status, .status-indicator'); + if (statusIndicator) { + statusIndicator.className = `online-status ${isOnline ? 'online' : 'offline'}`; + statusIndicator.textContent = isOnline ? 'โ—' : 'โ—‹'; + statusIndicator.title = isOnline ? 'Online' : 'Offline'; + } + + // Update text elements + const statusText = element.querySelector('.status-text'); + if (statusText) { + statusText.textContent = isOnline ? 'Online' : 'Offline'; + statusText.className = `status-text ${isOnline ? 'online' : 'offline'}`; + } + }); + } + + /** + * Fetch online status for multiple users from API + */ + async fetchOnlineStatus(userIds) { + if (!userIds || userIds.length === 0) return {}; + + try { + const response = await fetch('/api/notifications/online-status/', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}`, + 'Content-Type': 'application/json' + }, + // Use query parameters for GET request + url: `/api/notifications/online-status/?user_ids=${userIds.join(',')}` + }); + + if (response.ok) { + const data = await response.json(); + this.updateMultipleUserStatus(data.online_statuses); + return data.online_statuses; + } + } catch (error) { + console.error('Error fetching online status:', error); + } + return {}; + } + + /** + * Fetch friends' online status + */ + async fetchFriendsOnlineStatus() { + try { + const response = await fetch('/api/notifications/online-status/?friends=true', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` + } + }); + + if (response.ok) { + const data = await response.json(); + const statusMap = {}; + data.friends.forEach(friend => { + statusMap[friend.user_id] = friend.is_online; + }); + this.updateMultipleUserStatus(statusMap); + return data; + } + } catch (error) { + console.error('Error fetching friends online status:', error); + } + return null; + } + + /** + * Show notification when friend comes online/offline + */ + showStatusChangeNotification(userId, isOnline) { + // Get username from cache or element + const userElement = document.querySelector(`[data-user-id="${userId}"]`); + const username = userElement?.querySelector('.username, .friend-name, h4')?.textContent || `User ${userId}`; + + // Show subtle notification + if (this.shouldShowStatusNotifications()) { + const message = `${username} is now ${isOnline ? 'online' : 'offline'}`; + this.showToast(message, isOnline ? 'success' : 'info'); + } + } + + /** + * Check if status notifications should be shown + */ + shouldShowStatusNotifications() { + return localStorage.getItem('show_status_notifications') !== 'false'; + } + + /** + * Show toast notification + */ + showToast(message, type = 'info') { + // Simple toast implementation + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + + // Style the toast + Object.assign(toast.style, { + position: 'fixed', + top: '20px', + right: '20px', + padding: '12px 20px', + borderRadius: '6px', + color: 'white', + fontSize: '14px', + zIndex: '10000', + transition: 'all 0.3s ease', + backgroundColor: type === 'success' ? '#10b981' : '#3b82f6' + }); + + document.body.appendChild(toast); + + // Remove after 3 seconds + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => { + document.body.removeChild(toast); + }, 300); + }, 3000); + } + + /** + * Initialize online status for a list of users + */ + async initializeUsersStatus(userIds) { + return await this.fetchOnlineStatus(userIds); + } + + /** + * Clean up resources + */ + destroy() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + this.onlineUsers.clear(); + this.statusCallbacks.clear(); + } +} + +// Create global instance +window.onlineStatusManager = new OnlineStatusManager(); + +/** + * Utility function to create online status indicator + */ +function createOnlineStatusIndicator(userId, isOnline = false) { + const indicator = document.createElement('span'); + indicator.className = `online-status ${isOnline ? 'online' : 'offline'}`; + indicator.textContent = isOnline ? 'โ—' : 'โ—‹'; + indicator.title = isOnline ? 'Online' : 'Offline'; + + // Subscribe to status changes + window.onlineStatusManager.onStatusChange(userId, (id, online) => { + indicator.className = `online-status ${online ? 'online' : 'offline'}`; + indicator.textContent = online ? 'โ—' : 'โ—‹'; + indicator.title = online ? 'Online' : 'Offline'; + }); + + return indicator; +} + +/** + * CSS for online status indicators + */ +const statusCSS = ` + .online-status { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + font-size: 12px; + line-height: 1; + margin-left: 5px; + } + + .online-status.online { + color: #10b981; + background: #10b981; + } + + .online-status.offline { + color: #6b7280; + background: #6b7280; + } + + .status-text.online { + color: #10b981; + font-weight: 500; + } + + .status-text.offline { + color: #6b7280; + } +`; + +// Inject CSS +const style = document.createElement('style'); +style.textContent = statusCSS; +document.head.appendChild(style); diff --git a/Front-End/public/js/realtime-status-strategy.js b/Front-End/public/js/realtime-status-strategy.js new file mode 100644 index 00000000..82e99707 --- /dev/null +++ b/Front-End/public/js/realtime-status-strategy.js @@ -0,0 +1,302 @@ +/** + * Complete Frontend Integration Strategy for Real-Time Online Status + * + * This demonstrates the BEST PRACTICE approach: + * 1. โœ… WebSocket push notifications for real-time updates + * 2. โœ… Initial API call to get current status + * 3. โœ… Efficient caching and UI updates + * 4. โŒ NO polling every 5 seconds (inefficient) + */ + +class RealTimeStatusManager { + constructor() { + this.notificationWS = null; + this.onlineStatusManager = null; + this.pageLoadTime = Date.now(); + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + } + + /** + * Initialize the complete real-time status system + */ + async init() { + // 1. Initialize online status manager + this.onlineStatusManager = window.onlineStatusManager; + + // 2. Connect to notification WebSocket + await this.connectWebSocket(); + + // 3. Load initial friends status + await this.loadInitialFriendsStatus(); + + // 4. Setup page visibility handling + this.setupVisibilityHandling(); + + // 5. Setup connection monitoring + this.setupConnectionMonitoring(); + } + + /** + * Connect to notification WebSocket + */ + async connectWebSocket() { + try { + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${wsProtocol}//${window.location.host}/ws/notifications/`; + + this.notificationWS = new WebSocket(wsUrl); + + this.notificationWS.onopen = () => { + console.log('โœ… Real-time status connection established'); + this.reconnectAttempts = 0; + this.onlineStatusManager.init(this); + }; + + this.notificationWS.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleMessage(data); + }; + + this.notificationWS.onclose = () => { + console.log('โŒ Real-time status connection lost'); + this.handleDisconnection(); + }; + + this.notificationWS.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + } catch (error) { + console.error('Failed to connect WebSocket:', error); + this.scheduleReconnect(); + } + } + + /** + * Handle WebSocket messages + */ + handleMessage(data) { + // Delegate to online status manager + this.onlineStatusManager.handleOnlineStatusMessage(data); + + // Handle other notification types + switch(data.type) { + case 'friend_request': + case 'friend_request_accepted': + case 'game_invitation': + this.showNotification(data); + break; + + case 'queued_notifications': + this.handleQueuedNotifications(data.notifications); + break; + } + } + + /** + * Load initial friends status on page load + */ + async loadInitialFriendsStatus() { + try { + console.log('๐Ÿ“ก Loading initial friends status...'); + const friendsData = await this.onlineStatusManager.fetchFriendsOnlineStatus(); + + if (friendsData) { + console.log(`โœ… Loaded status for ${friendsData.total_friends} friends, ${friendsData.online_friends} online`); + } + } catch (error) { + console.error('Failed to load initial friends status:', error); + } + } + + /** + * Handle page visibility changes (user switches tabs) + */ + setupVisibilityHandling() { + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + // Page became visible - refresh friends status + setTimeout(() => { + this.refreshFriendsStatus(); + }, 1000); + } + }); + } + + /** + * Refresh friends status when needed + */ + async refreshFriendsStatus() { + const timeSinceLoad = Date.now() - this.pageLoadTime; + + // Only refresh if page has been loaded for more than 30 seconds + if (timeSinceLoad > 30000) { + console.log('๐Ÿ”„ Refreshing friends status after tab switch...'); + await this.loadInitialFriendsStatus(); + } + } + + /** + * Monitor connection health + */ + setupConnectionMonitoring() { + setInterval(() => { + if (this.notificationWS && this.notificationWS.readyState === WebSocket.OPEN) { + // Connection is healthy + const lastHeartbeat = this.onlineStatusManager.lastHeartbeat; + if (Date.now() - lastHeartbeat > 120000) { // 2 minutes without heartbeat + console.warn('โš ๏ธ No heartbeat for 2 minutes, reconnecting...'); + this.reconnect(); + } + } else { + // Connection is broken + this.handleDisconnection(); + } + }, 60000); // Check every minute + } + + /** + * Handle WebSocket disconnection + */ + handleDisconnection() { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.scheduleReconnect(); + } else { + console.error('โŒ Max reconnection attempts reached'); + this.showConnectionError(); + } + } + + /** + * Schedule reconnection attempt + */ + scheduleReconnect() { + this.reconnectAttempts++; + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); // Exponential backoff, max 30s + + console.log(`๐Ÿ”„ Reconnecting in ${delay/1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); + + setTimeout(() => { + this.connectWebSocket(); + }, delay); + } + + /** + * Force reconnection + */ + reconnect() { + if (this.notificationWS) { + this.notificationWS.close(); + } + this.connectWebSocket(); + } + + /** + * Show connection error to user + */ + showConnectionError() { + const errorDiv = document.createElement('div'); + errorDiv.id = 'connection-error'; + errorDiv.innerHTML = ` +
+ โš ๏ธ Connection lost. Some features may not work properly. + +
+ `; + document.body.appendChild(errorDiv); + } + + /** + * Send heartbeat + */ + sendHeartbeat() { + if (this.notificationWS && this.notificationWS.readyState === WebSocket.OPEN) { + this.notificationWS.send(JSON.stringify({type: 'heartbeat'})); + } + } + + /** + * Handle queued notifications + */ + handleQueuedNotifications(notifications) { + notifications.forEach(notification => { + this.showNotification(notification); + }); + } + + /** + * Show notification to user + */ + showNotification(notification) { + // Implement your notification display logic + console.log('๐Ÿ“ฌ New notification:', notification); + } +} + +/** + * Usage Strategy Examples + */ + +// โœ… BEST PRACTICE: Initialize on page load +document.addEventListener('DOMContentLoaded', async () => { + window.realTimeStatusManager = new RealTimeStatusManager(); + await window.realTimeStatusManager.init(); +}); + +// โœ… BEST PRACTICE: Use for friend list +class OptimizedFriendList { + constructor() { + this.friends = []; + } + + async init() { + await this.loadFriends(); + // No need for manual status polling - WebSocket handles it! + this.setupRealTimeUpdates(); + } + + async loadFriends() { + // Load friends from API (includes current online status) + const response = await fetch('/api/friends/?status=accepted'); + this.friends = await response.json(); + this.renderFriends(); + } + + setupRealTimeUpdates() { + // Subscribe to real-time updates + this.friends.forEach(friendship => { + const friendId = this.getFriendId(friendship); + + window.onlineStatusManager.onStatusChange(friendId, (userId, isOnline) => { + this.updateFriendUI(userId, isOnline); + console.log(`๐Ÿ‘ค ${userId} is now ${isOnline ? 'online' : 'offline'}`); + }); + }); + } + + updateFriendUI(userId, isOnline) { + const friendElement = document.querySelector(`[data-user-id="${userId}"]`); + if (friendElement) { + const statusElement = friendElement.querySelector('.online-status'); + statusElement.className = `online-status ${isOnline ? 'online' : 'offline'}`; + statusElement.textContent = isOnline ? '๐ŸŸข' : 'โšซ'; + } + } +} + +// โŒ BAD PRACTICE: Don't do this! +/* +setInterval(async () => { + // This creates unnecessary load on your servers + const response = await fetch('/api/friends/online-status/'); + // Update UI... +}, 5000); // Every 5 seconds = 720 requests per hour per user! +*/ + +// โœ… INSTEAD: Use the real-time system above which only makes requests when: +// 1. Page loads initially +// 2. User switches back to tab after being away +// 3. Connection is lost and needs refresh diff --git a/Front-End/public/login/login.css b/Front-End/public/login/login.css index 83afbb80..e6617a6b 100644 --- a/Front-End/public/login/login.css +++ b/Front-End/public/login/login.css @@ -1,37 +1,42 @@ -.login -{ - height: 100vh; - width: 100vw; - background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); +.login { + height: 100%; + width: 100%; display: flex; justify-content: center; align-items: center; - border-top-left-radius: 12px; - -} - + border-radius: 12px; + position: absolute; + padding: 10px; + } + /* background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); */ + .register { - height: 100vh; - width: 100vw; - background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); + height: 100%; + width: 100%; display: flex; justify-content: center; align-items: center; - border-top-left-radius: 12px; - -} + border-radius: 12px; + padding: 10px; + } + /* background: radial-gradient(circle at -200px 230px, #03173f 0%, #142869 10%, #26357c 18%, #335fab 51%, #1152a1 100%); */ -.login_box -{ - height: 600px; - width: 400px; - background-color: #62ff0027; - border-radius: 20px; +.login_box { display: flex; - flex-direction: column; + background-color: #fefefe; + /* padding: 20px; */ + /* border: 1px solid #888; */ + width: 100%; + height: 100%; + border-radius: 8px; + position: relative; + overflow-y: auto; + flex-direction: row; + flex-wrap: wrap; justify-content: center; - align-items: center; + padding: 10px; + box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.25); } .login_form @@ -39,10 +44,11 @@ display:flex; flex-direction: column; justify-content: center; - align-items: center; - gap: 10px; - height: 300px; -} + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 5px; + } .empty { diff --git a/Front-End/public/login/login.js b/Front-End/public/login/login.js index 83d834e1..210d5862 100644 --- a/Front-End/public/login/login.js +++ b/Front-End/public/login/login.js @@ -1,142 +1,488 @@ -import { setVariables, getVariables } from '../var.js'; -import { getCookie } from '../cookie.js'; +import { setVariables, getVariables, calculateInitials } from "../var.js"; +import { removeExpandableSidebar } from "../index.js"; +import { getCookie } from "../cookie.js"; +import { showAlertForXSeconds } from "../alert/alert.js"; -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/login/login.css'; +const link = document.createElement("link"); +link.rel = "stylesheet"; +link.href = "/login/login.css"; document.head.appendChild(link); function renderLogin() { - const contentDiv = document.getElementById('content'); - contentDiv.innerHTML = ` - - `; - - document.getElementById('loginForm').addEventListener('submit', async function (e) { - e.preventDefault(); - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - await onHandleSubmit(e, email, password); - }); - - document.getElementById('registerButton').addEventListener('click', function () { - window.navigateTo('#register'); - }); + // Animazione logout solo se arrivo da una route protetta + if ( + window.previousRoute === "home" || + window.previousRoute === "pong" || + window.previousRoute === "pongmulti" + ) { + const contentDiv = document.getElementById("content"); + if (contentDiv) { + contentDiv.classList.remove("animate-margin"); + contentDiv.classList.add("animate-margin-logout"); + contentDiv.addEventListener("animationend", function handler() { + contentDiv.removeEventListener("animationend", handler); + contentDiv.classList.remove("animate-margin-logout"); + contentDiv.style.margin = "12% 41% 15% 38%"; + }); + } + window.previousRoute = ""; + } + // Rimuovi sidebar se presente + if (typeof removeExpandableSidebar === "function") { + removeExpandableSidebar(); + } + // Nascondi pulsante impostazioni se presente + const toggleSettingsButton = document.getElementById("toggleSettingsButton"); + if (toggleSettingsButton) toggleSettingsButton.style.display = "none"; + + const contentDiv = document.getElementById("content"); + contentDiv.innerHTML = ` + + `; + + document + .getElementById("loginForm") + .addEventListener("submit", async function (e) { + e.preventDefault(); + const email = document.getElementById("email").value; + const password = document.getElementById("password").value; + if (password.length < 8) { + showAlertForXSeconds( + "Password must be at least 8 characters long.", + "error", + 3, + { asToast: true } + ); + return; + } + await onHandleSubmit(e, email, password); + }); + + document + .getElementById("registerButton") + .addEventListener("click", function () { + window.navigateTo("#register"); + }); + + // document + // .getElementById("loginGoogle") + // .addEventListener("click", () => handleOAuthLogin("google")); + + // document + // .getElementById("login42") + // .addEventListener("click", () => handleOAuthLogin("42")); +} + +/** + * Gestisce il login OAuth per diversi provider - VERSIONE LOCALSTORAGE + * @param {string} provider - Il provider OAuth ("google" o "42") + */ +async function handleOAuthLogin(provider) { + const csrftoken = getCookie("csrftoken"); + const { url_api } = getVariables(); + + let popup = null; + let storageCheckInterval; + + try { + const response = await fetch( + `${url_api}/login/login/oauth/${provider}/`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": csrftoken, + }, + } + ); + + if (response.ok) { + const data = await response.json(); + + // PULISCI localStorage prima di iniziare + localStorage.removeItem("oauth_result"); + + // Apri il popup OAuth + popup = window.open( + data.redirect_url, + "oauth-popup", + "width=500,height=600,scrollbars=yes,resizable=yes,left=" + + (window.screen.width / 2 - 250) + + ",top=" + + (window.screen.height / 2 - 300) + ); + + if (!popup) { + showAlertForXSeconds( + "Popup blocked by browser. Please allow popups for this site.", + "error", + 5, + { asToast: true } + ); + return; + } + + // controllo localStorage ogni secondo + storageCheckInterval = setInterval(() => { + try { + const resultString = localStorage.getItem("oauth_result"); + + if (resultString) { + const result = JSON.parse(resultString); + + // Pulisci localStorage + localStorage.removeItem("oauth_result"); + + // Pulisci interval + clearInterval(storageCheckInterval); + storageCheckInterval = null; + + // Chiudi popup se ancora aperto + if (popup && !popup.closed) { + popup.close(); + } + + // Processa il risultato + if (result.type === "OAUTH_SUCCESS") { + // Verifica che i dati ci siano + if (!result.access_token || !result.refresh_token) { + console.error( + "Token mancanti nel risultato:", + result + ); + showAlertForXSeconds( + "OAuth authentication failed: missing tokens", + "error", + 5, + { asToast: true } + ); + return; + } + + const initials = calculateInitials(result.username); + + // Salva i token + setVariables({ + token: result.access_token, + refreshToken: result.refresh_token, + userId: result.user_id, + userUsername: result.username, + userEmail: result.email, + initials: initials, + }); + + // Reindirizza alla home + window.navigateTo("#home"); + + // Mostra messaggio di successo + showAlertForXSeconds( + "OAuth login successful!", + "success", + 3, + { asToast: true } + ); + } else if (result.type === "OAUTH_ERROR") { + showAlertForXSeconds( + `OAuth login failed: ${result.error}`, + "error", + 5, + { asToast: true } + ); + } + } + } catch (e) { + console.error("Errore controllo localStorage:", e); + } + }, 1000); + + // Timeout di sicurezza (5 minuti) + setTimeout(() => { + if (storageCheckInterval) { + clearInterval(storageCheckInterval); + storageCheckInterval = null; + } + try { + if (popup) { + popup.close(); + } + } catch (e) { + console.log("Popup giร  chiuso o non accessibile"); + } + localStorage.removeItem("oauth_result"); + }, 300000); // 5 minuti + } else { + const errorData = await response.json(); + showAlertForXSeconds( + "Authentication error. Please try again.", + "error", + 3, + { asToast: true } + ); + } + } catch (error) { + showAlertForXSeconds( + "Connection error during authentication.", + "error", + 3, + { asToast: true } + ); + } } async function onHandleSubmit(e, email, password) { - e.preventDefault(); - if (email && password) { - console.log('Email:', email); - console.log('Password:', password); - const csrftoken = getCookie('csrftoken'); - const loginSuccess = await loginUser(email, password, csrftoken, true); - if (loginSuccess) { - await handleGetUser(csrftoken); - window.navigateTo('#home'); - } - } else { - console.log('Per favore, inserisci sia email che password.'); - } + e.preventDefault(); + if (email && password) { + const csrftoken = getCookie("csrftoken"); + const loginSuccess = await loginUser(email, password, csrftoken, true); + if (loginSuccess) { + const contentDiv = document.getElementById("content"); + contentDiv.classList.add("animate-margin"); + contentDiv.addEventListener("animationend", function handler() { + contentDiv.removeEventListener("animationend", handler); + window.navigateTo("#home"); + }); + } + } else { + showAlertForXSeconds( + "Please enter both email and password.", + "error", + 3, + { asToast: true } + ); + } } /** * Effettua il login dell'utente. - * + * * @param {boolean} isBaseLogin - true per il login base, false per il login multiplayer per pong. * @returns {Promise} - Ritorna true se il login ha successo, altrimenti false. */ async function loginUser(email, password, csrftoken, isBaseLogin) { if (email && password) { - console.log('Email:', email); - console.log('Password:', password); + const { url_api } = getVariables(); try { - const response = await fetch('http://localhost:8000/login/login', { - method: 'POST', + const response = await fetch(`${url_api}/login/login/login`, { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrftoken, + "Content-Type": "application/json", + "X-CSRFToken": csrftoken, }, body: JSON.stringify({ email, password }), }); if (response.ok) { const data = await response.json(); - console.log('Risposta dal server:', data); + + // Check if 2FA verification is needed + if ( + data.temp_token && + data.message && + data.message.includes("2FA") + ) { + // Show OTP verification form + showOTPVerificationForm(data.temp_token, email); + return false; + } if (isBaseLogin) { setVariables({ token: data.access_token }); - } - else { + setVariables({ refreshToken: data.refresh_token }); + + const { user_id, email, username } = data; + const initials = calculateInitials(username); + + setVariables({ + userEmail: email, + userUsername: username, + userId: user_id, + initials: initials, + }); + } else { setVariables({ multiplayer_username: data.username }); setVariables({ multiplayer_id: data.user_id }); } return true; } else { const errorData = await response.json(); - console.error('Errore login:', errorData); + console.error("Login error:", errorData); + + // Handle different error formats + let errorMessage = "Invalid credentials"; + + if (errorData.error) { + if (typeof errorData.error === "string") { + // Handle Django REST Framework validation errors in string format + if (errorData.error.includes("ErrorDetail")) { + errorMessage = "Please enter a valid email address"; + } else { + errorMessage = errorData.error; + } + } + } + + showAlertForXSeconds( + `Login failed: ${errorMessage}`, + "error", + 3, + { asToast: true } + ); return false; } } catch (error) { - console.error('Exception login:', error); + showAlertForXSeconds( + "Connection error during login. Please try again later.", + "error", + 3, + { asToast: true } + ); return false; } } else { - console.log('Per favore, inserisci sia email che password.'); return false; } } -async function handleGetUser(csrftoken) { +function showOTPVerificationForm(tempToken, email) { + const contentDiv = document.getElementById("content"); + // Save the original content to restore if needed + const originalContent = contentDiv.innerHTML; + + // Show OTP verification form + contentDiv.innerHTML = ` + + `; + + // Handle OTP verification + document + .getElementById("otpForm") + .addEventListener("submit", async function (e) { + e.preventDefault(); + const otpCode = document.getElementById("otpCode").value; + if (otpCode) { + const verified = await verifyOTP(tempToken, otpCode, email); + if (verified) { + window.navigateTo("#home"); + } else { + showAlertForXSeconds( + "Invalid OTP code. Please try again.", + "error", + 3, + { asToast: true } + ); + } + } + }); + + // Handle cancel button + document + .getElementById("cancelButton") + .addEventListener("click", function () { + // Restore original login form + contentDiv.innerHTML = originalContent; + + // Re-attach event listeners to the restored content + document + .getElementById("loginForm") + .addEventListener("submit", async function (e) { + e.preventDefault(); + const email = document.getElementById("email").value; + const password = document.getElementById("password").value; + await onHandleSubmit(e, email, password); + }); + + document + .getElementById("registerButton") + .addEventListener("click", function () { + window.navigateTo("#register"); + }); + + document + .getElementById("loginGoogle") + .addEventListener("click", () => handleOAuthLogin("google")); + + document + .getElementById("login42") + .addEventListener("click", () => handleOAuthLogin("42")); + }); +} + +async function verifyOTP(tempToken, otpCode, email) { + const { url_api } = getVariables(); + const csrftoken = getCookie("csrftoken"); + try { - const { token } = getVariables(); - const response = await fetch('trascendence.42firenze.it/api/login/user', { - method: 'GET', + const response = await fetch(`${url_api}/login/login/2fa/verify/`, { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrftoken, - 'Authorization': `Bearer ${token}`, + "Content-Type": "application/json", + "X-CSRFToken": csrftoken, + Authorization: `Bearer ${tempToken}`, }, + body: JSON.stringify({ otp_code: otpCode }), }); if (response.ok) { const data = await response.json(); - const { user, user_id } = data; - const { email, username } = user; + + // Set tokens and user info after successful verification + setVariables({ token: data.access_token }); + setVariables({ refreshToken: data.refresh_token }); + + const { user_id, username, email } = data; + const initials = calculateInitials(username); setVariables({ userEmail: email, userUsername: username, - userId: user_id + userId: user_id, + initials: initials, }); - console.log('User email:', email); - console.log('User username:', username); - console.log('User ID:', user_id); + return true; } else { const errorData = await response.json(); - console.error('Errore nella risposta del server:', errorData); + console.error("OTP verification failed:", errorData); + return false; } } catch (error) { - console.error('Errore nella richiesta:', error); + console.error("Exception during OTP verification:", error); + return false; } } -export { renderLogin, loginUser }; \ No newline at end of file +export { renderLogin, loginUser }; diff --git a/Front-End/public/notification/friendAutocomplete.js b/Front-End/public/notification/friendAutocomplete.js new file mode 100644 index 00000000..b8587f5b --- /dev/null +++ b/Front-End/public/notification/friendAutocomplete.js @@ -0,0 +1,123 @@ +import { getVariables } from "../var.js"; +import { handleFriendRequest } from "./notification.js"; +import { blockUser } from "../chat/blockUser.js"; + +let debounceTimeout = null; + +export function initFriendAutocomplete({ inputId = "friendID", suggestionListId = "suggestionList", handlerOnSelect = null } = {}) { + const friendInput = document.getElementById(inputId); + const suggestionList = document.getElementById(suggestionListId); + const inputGroup = friendInput?.closest('.input-group'); + const { url_api, token } = getVariables(); + if (friendInput && suggestionList && inputGroup) { + suggestionList.style.width = inputGroup.offsetWidth + "px"; + + function positionSuggestionList() { + const inputRect = friendInput.getBoundingClientRect(); + const listHeight = suggestionList.offsetHeight || 200; + const spaceBelow = window.innerHeight - inputRect.bottom; + const spaceAbove = inputRect.top; + // Default: sotto + suggestionList.style.left = inputRect.left + "px"; + suggestionList.style.width = inputRect.width + "px"; + suggestionList.style.position = "fixed"; + suggestionList.style.zIndex = "9999"; + if (spaceBelow < listHeight && spaceAbove > listHeight) { + // Mostra sopra + suggestionList.style.top = (inputRect.top - listHeight) + "px"; + suggestionList.style.bottom = "auto"; + } else { + // Mostra sotto + suggestionList.style.top = inputRect.bottom + "px"; + suggestionList.style.bottom = "auto"; + } + } + + friendInput.addEventListener("focus", () => { + suggestionList.style.width = inputGroup.offsetWidth + "px"; + suggestionList.style.display = "block"; + positionSuggestionList(); + }); + + friendInput.addEventListener("input", () => { + if (debounceTimeout) clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => { + if (friendInput.value.trim() === "") { + suggestionList.style.display = "none"; + suggestionList.innerHTML = ""; + return; + } + suggestionList.style.display = "block"; + searchUsers(url_api, token, friendInput.value) + .then((results) => { + updateSuggestionList(suggestionList, results, friendInput, handlerOnSelect); + positionSuggestionList(); + }) + .catch((error) => { + console.error("Error searching users:", error); + }); + }, 100); + }); + + window.addEventListener("resize", positionSuggestionList); + window.addEventListener("scroll", positionSuggestionList, true); + + friendInput.addEventListener("blur", () => { + setTimeout(() => suggestionList.style.display = "none", 150); + }); + } +} + +export function updateSuggestionList(suggestionList, results, friendInput, handlerOnSelect) { + if (!results || results.length === 0) { + suggestionList.innerHTML = `Nessun risultato`; + return; + } + const isBlockUser = friendInput.id === "blockUserInput"; + suggestionList.innerHTML = results.map(user => + `${user.username} + + ` + ).join(''); + + suggestionList.querySelectorAll("a[data-userid]").forEach(item => { + item.addEventListener("mousedown", function(e) { + const userId = Number(this.getAttribute("data-userid")); + const username = this.textContent.trim(); + if (typeof handlerOnSelect === "function") { + handlerOnSelect(friendInput, userId, this); + } else if (friendInput.id === "blockUserInput") { + // Chiamata diretta per il blocco utenti + blockUser(userId, username); + suggestionList.style.display = "none"; + if (friendInput) { + friendInput.value = ""; + friendInput.dataset.userid = ""; + } + } else { + handleFriendRequest('POST', userId, username); + suggestionList.style.display = "none"; + if (friendInput) friendInput.value = ""; + } + suggestionList.style.display = "none"; + }); + }); +} + +export async function searchUsers(url_api, token, query) { + const response = await fetch(`${url_api}/user/user/search?search=${query}&page=1&page_size=4`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + }); + + if (response.ok) { + const data = await response.json(); + return data.results || []; + } else { + console.error("searchUsers error:", response); + return []; + } +} \ No newline at end of file diff --git a/Front-End/public/notification/notification.css b/Front-End/public/notification/notification.css index 2b29a2d0..3e3255f4 100644 --- a/Front-End/public/notification/notification.css +++ b/Front-End/public/notification/notification.css @@ -1,16 +1,596 @@ +/* Existing notification styles */ .notification-box { width: 100%; height: 100%; - background-color: #f5f5f5f0; border-radius: 8px; flex-grow: 1; padding: 10px; } .friend-item { - display: flex; - align-items: center; - padding: 10px; - border-bottom: 1px solid #e0e0e0; - justify-content: space-between; + display: flex; + align-items: center; + padding: 10px; +} + +/* ====== UNIFIED NOTIFICATION SYSTEM ====== */ + +/* Base notification toast styles */ +.unified-notification, +.notification-toast, +.toast-notification { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 350px; + min-width: 280px; + margin-bottom: 10px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease-in-out; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: white; + border-left: 4px solid #3b82f6; +} + +.unified-notification.show, +.notification-toast.show, +.toast-notification.show { + opacity: 1; + transform: translateX(0); +} + +.unified-notification.hide, +.notification-toast.hide, +.toast-notification.hide { + opacity: 0; + transform: translateX(100%); +} + +/* Notification header */ +.unified-notification-header, +.notification-header { + display: flex; + align-items: center; + padding: 12px 16px; + border-radius: 8px 8px 0 0; + color: white; + font-weight: 600; + font-size: 14px; +} + +/* Notification body */ +.unified-notification-body, +.notification-body { + padding: 12px 16px; + background: white; + border-radius: 0 0 8px 8px; + color: #333; + font-size: 14px; + line-height: 1.4; +} + +/* Simple notification style (for backward compatibility) */ +.simple-notification { + padding: 12px 16px; + background: white; + border-radius: 8px; + color: #333; + font-size: 14px; + line-height: 1.4; + display: flex; + align-items: center; + gap: 8px; +} + +/* Close button */ +.unified-notification-close, +.notification-close { + background: none; + border: none; + color: currentColor; + font-size: 18px; + font-weight: bold; + cursor: pointer; + margin-left: auto; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.8; + transition: opacity 0.2s; +} + +.unified-notification-close:hover, +.notification-close:hover { + opacity: 1; +} + +/* Type-specific colors */ +.unified-notification.success, +.notification-toast.success, +.toast-notification.success { + border-left-color: #10b981; +} + +.unified-notification.success .unified-notification-header, +.notification-toast.success .notification-header { + background: linear-gradient(135deg, #10b981, #059669); +} + +.unified-notification.error, +.notification-toast.error, +.toast-notification.error { + border-left-color: #ef4444; +} + +.unified-notification.error .unified-notification-header, +.notification-toast.error .notification-header { + background: linear-gradient(135deg, #ef4444, #dc2626); +} + +.unified-notification.warning, +.notification-toast.warning, +.toast-notification.warning { + border-left-color: #f59e0b; +} + +.unified-notification.warning .unified-notification-header, +.notification-toast.warning .notification-header { + background: linear-gradient(135deg, #f59e0b, #d97706); +} + +.unified-notification.info, +.notification-toast.info, +.toast-notification.info { + border-left-color: #3b82f6; +} + +.unified-notification.info .unified-notification-header, +.notification-toast.info .notification-header { + background: linear-gradient(135deg, #3b82f6, #2563eb); +} + +/* Simple notification type colors (no header) */ +.simple-notification.success { + background: #f0fdf4; + border-left-color: #10b981; + color: #065f46; +} + +.simple-notification.error { + background: #fef2f2; + border-left-color: #ef4444; + color: #991b1b; +} + +.simple-notification.warning { + background: #fffbeb; + border-left-color: #f59e0b; + color: #92400e; +} + +.simple-notification.info { + background: #eff6ff; + border-left-color: #3b82f6; + color: #1e40af; +} + +/* Icons for different types */ +.unified-notification-icon, +.notification-icon { + margin-right: 8px; + font-size: 16px; +} + +/* Progress bar for auto-dismiss */ +.unified-notification-progress, +.notification-progress { + position: absolute; + bottom: 0; + left: 0; + height: 3px; + background: rgba(255, 255, 255, 0.3); + border-radius: 0 0 8px 8px; + transition: width linear; +} + +/* Action buttons in notification body */ +.unified-notification-actions, +.notification-actions { + margin-top: 12px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.unified-notification-btn, +.notification-btn { + padding: 6px 12px; + border-radius: 4px; + border: none; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.unified-notification-btn.primary, +.notification-btn.primary { + background: #3b82f6; + color: white; +} + +.unified-notification-btn.primary:hover, +.notification-btn.primary:hover { + background: #2563eb; +} + +.unified-notification-btn.secondary, +.notification-btn.secondary { + background: #e5e7eb; + color: #374151; +} + +.unified-notification-btn.secondary:hover, +.notification-btn.secondary:hover { + background: #d1d5db; +} + +/* Modal styles for game invitations */ +.unified-modal, +.game-invitation-modal, +#gameInvitationModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.unified-modal.show, +.game-invitation-modal.show, +#gameInvitationModal.show { + opacity: 1; +} + +.unified-modal-content, +.game-invitation-modal .modal-content { + background: white; + border-radius: 12px; + max-width: 400px; + width: 90%; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + transform: scale(0.9); + transition: transform 0.3s ease; +} + +.unified-modal.show .unified-modal-content, +.game-invitation-modal.show .modal-content { + transform: scale(1); +} + +.unified-modal-header, +.game-invitation-modal .modal-header { + padding: 20px 24px; + border-bottom: 1px solid #e5e7eb; + border-radius: 12px 12px 0 0; } + +.unified-modal-title, +.game-invitation-modal .modal-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #111827; + display: flex; + align-items: center; +} + +.unified-modal-body, +.game-invitation-modal .modal-body { + padding: 24px; + text-align: center; +} + +.unified-modal-footer, +.game-invitation-modal .modal-footer { + padding: 16px 24px; + border-top: 1px solid #e5e7eb; + display: flex; + gap: 12px; + justify-content: center; + border-radius: 0 0 12px 12px; +} + +/* Pong game specific notifications */ +.pong-notification { + border-left: 4px solid #8b5cf6; + background: linear-gradient(135deg, #f3f4f6, #e5e7eb); +} + +.pong-notification .notification-icon { + color: #8b5cf6; +} + +/* Game status indicators */ +.game-status-indicator { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.game-status-indicator.connecting { + background: #fef3c7; + color: #92400e; +} + +.game-status-indicator.connected { + background: #d1fae5; + color: #065f46; +} + +.game-status-indicator.error { + background: #fee2e2; + color: #991b1b; +} + +.game-status-indicator.waiting { + background: #dbeafe; + color: #1e40af; +} + +/* Responsive design */ +@media (max-width: 480px) { + .unified-notification, + .notification-toast, + .toast-notification { + top: 10px; + right: 10px; + left: 10px; + max-width: none; + transform: translateY(-100%); + } + + .unified-notification.show, + .notification-toast.show, + .toast-notification.show { + transform: translateY(0); + } + + .unified-notification.hide, + .notification-toast.hide, + .toast-notification.hide { + transform: translateY(-100%); + } + + .unified-modal-content, + .game-invitation-modal .modal-content { + margin: 20px; + width: calc(100% - 40px); + } +} + +/* Animation keyframes */ +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOutRight { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(100%); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +/* Utility classes */ +.notification-stack { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; +} + +.notification-container { + pointer-events: none; +} + +.notification-container > * { + pointer-events: auto; +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .unified-notification, + .notification-toast, + .toast-notification { + border: 2px solid currentColor; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .unified-notification, + .notification-toast, + .toast-notification, + .unified-modal, + .game-invitation-modal { + transition: none; + } + + .unified-notification-progress, + .notification-progress { + transition: none; + } +} + +/* Add these styles at the end of your existing CSS */ + +/* ====== MULTIPLAYER PONG READY SCREEN FIXES ====== */ + +/* Ready screen container */ +#ready-screen { + display: flex !important; + flex-direction: column !important; + min-width: 400px !important; + max-width: 500px !important; + width: auto !important; +} + +/* Player status cards */ +#player-ready-status, +#opponent-ready-status { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 0.75rem !important; + margin-bottom: 0.5rem !important; + background: rgba(0, 0, 0, 0.5) !important; + border-radius: 0.375rem !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; +} + +/* Button container */ +#ready-screen .d-grid { + display: grid !important; + gap: 0.5rem !important; +} + +/* Controls section */ +#ready-screen .mt-auto { + margin-top: auto !important; + padding-top: 1rem !important; + border-top: 1px solid rgba(255, 255, 255, 0.2) !important; +} + +/* Keyboard key styling */ +#ready-screen kbd { + background-color: #6c757d !important; + color: white !important; + padding: 0.25rem 0.5rem !important; + border-radius: 0.25rem !important; + font-size: 0.875rem !important; + font-weight: bold !important; +} + +/* Controls layout */ +#ready-screen .d-flex.gap-3 { + display: flex !important; + gap: 1rem !important; + justify-content: center !important; +} + +/* Responsive adjustments */ +@media (max-width: 576px) { + #ready-screen { + min-width: 300px !important; + max-width: 350px !important; + padding: 1rem !important; + } + + #ready-screen .d-flex.gap-3 { + flex-direction: column !important; + gap: 0.5rem !important; + text-align: center !important; + } +} + +/* Force block display for status elements */ +.ready-screen-status { + display: block !important; + width: 100% !important; + margin-bottom: 0.5rem !important; +} + +/* Ensure proper spacing */ +#ready-screen h2, +#ready-screen h5 { + display: block !important; + margin-bottom: 1rem !important; +} + +/* Fix any Bootstrap conflicts */ +#ready-screen .row { + margin: 0 !important; +} + +#ready-screen .col-12 { + padding: 0 !important; + margin-bottom: 0.5rem !important; +} + +/* Force hide ready screen when game is active */ +.game-active #ready-screen { + display: none !important; +} + +/* Ensure game canvas is visible */ +.game-active .gamecontainer { + position: relative !important; + width: 100% !important; + height: 100vh !important; + background: #000 !important; +} + +.game-active #threejs-container { + position: absolute !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + z-index: 1 !important; +} + +/* Hide all overlays when game is active */ +.game-active .position-absolute.top-0.end-0 { + display: none !important; +} \ No newline at end of file diff --git a/Front-End/public/notification/notification.js b/Front-End/public/notification/notification.js index e5103b31..afd6d3e6 100644 --- a/Front-End/public/notification/notification.js +++ b/Front-End/public/notification/notification.js @@ -1,36 +1,50 @@ -import { setVariables, getVariables } from '../var.js'; -import { updateChatList } from '../chat/ExpandableSidebar.js'; +import { setVariables, getVariables, calculateInitials } from "../var.js"; +import { updateChatList } from "../chat/ExpandableSidebar.js"; +import { getCookie } from "../cookie.js"; +import { showAlertForXSeconds } from "../alert/alert.js"; +import { initFriendAutocomplete } from "./friendAutocomplete.js"; +import { renderNewTournament } from "../pong/tournament.js"; -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/notification/notification.css'; +const link = document.createElement("link"); +link.rel = "stylesheet"; +link.href = "/notification/notification.css"; document.head.appendChild(link); let messageHistory = []; let socket; -async function handleFriendRequest(str_method, user_id, friend_id, index) { - +async function handleFriendRequest( + str_method, + receiver_id, + receiver_username, + index +) { // Pulisce l'input dopo aver inviato la richiesta - const friendInput = document.getElementById('friendID'); + receiver_id = Number(receiver_id); + const friendInput = document.getElementById("friendID"); if (friendInput) { - friendInput.value = ''; + friendInput.value = ""; } - + const { url_api } = getVariables(); console.log(`/----handleFriendRequest ${str_method}----\\`); - console.log("User ID:", user_id); - console.log("Friend ID:", friend_id); - const card = document.getElementById(`notification-card-${index}`); + console.log("Friend ID:", receiver_id); + console.log("Index:", index); + + let card = document.getElementById(`notification-card-${index}`); + if (!card) { + card = document.getElementById(`notification-card-${receiver_id}`); + } const deleteCard = document.getElementById(`friend-item-${index}`); + try { - const response = await fetch("http://localhost:8002/user/addfriend", { + const response = await fetch(`${url_api}/user/user/addfriend`, { method: str_method, headers: { + Authorization: `Bearer ${getVariables().token}`, "Content-Type": "application/json", }, body: JSON.stringify({ - user_1: user_id, - user_2: friend_id, + receiver: receiver_id, }), }); @@ -38,25 +52,41 @@ async function handleFriendRequest(str_method, user_id, friend_id, index) { const data = await response.json(); console.log("Friend Request Sent:", data); + if (index !== undefined && index !== null && index >= 0) { + messageHistory.splice(index, 1); + } + if (str_method === "DELETE") { + messageHistory = messageHistory.filter( + (msg) => msg.user_id !== receiver_id + ); + } + const info = data.info; - console.log("Info handleFriendRequest:", info); //------------------------ + console.log("Info handleFriendRequest:", info); //------------------------ if (info === "friend request accepted") { console.log("Friend request accepted handleFriendRequest"); if (card) { card.remove(); } getFriends(); - } - else if (info === "friendship deleted") { + } else if (info === "friendship deleted") { console.log("friendship deleted handleFriendRequest"); if (deleteCard) { deleteCard.remove(); } - } - else { + if (card) { + card.remove(); + } + getFriends(); + } else if (info === "friend request sent") { + console.log("Friend request sent handleFriendRequest"); + console.log("receiver_id:", receiver_id); + console.log("receiver_username:", receiver_username); + renderSentFriendRequest(receiver_id, receiver_username, true); + } else { console.log("else handleFriendRequest"); - //renderNotification(); - } + getFriends(); + } } else { const errorData = await response.json(); console.error("Error in server response:", errorData); @@ -66,27 +96,67 @@ async function handleFriendRequest(str_method, user_id, friend_id, index) { } } +function renderSentFriendRequest( + receiver_id, + receiver_username, + addToDOM = false +) { + console.log("/***********renderSentFriendRequest************/"); + console.log("Rendering sent friend request for ID:", receiver_id); + console.log("Rendering sent friend request for Username:", receiver_username); + + const notificationContent = document.getElementById("notificationContent"); + + // Create the HTML + const htmlToAdd = ` +
+
+

Friend request sent to ${receiver_username}

+ +
+
+ `; + + if (addToDOM) { + const notificationContent = document.getElementById("notificationContent"); + notificationContent.innerHTML += htmlToAdd; + } + + return htmlToAdd; +} + async function getFriends() { - const { token, userId } = getVariables(); + const { token, userId, url_api } = getVariables(); console.log("/----getFirends notification.js----\\"); console.log("User ID:", userId); console.log("Token:", token); - console.log("indirizzo:", `http://127.0.0.1:8002/user/friend?user_id=${userId}&accepted=true`) + console.log("indirizzo:", `${url_api}/user/friend`); try { - const response = await fetch( - `http://127.0.0.1:8002/user/friend?user_id=${userId}&accepted=true`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, - }, - } - ); + const response = await fetch(`${url_api}/user/user/friend`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + }); if (response.ok) { const data = await response.json(); console.log("getFriends data:", data); - renderFriendsList(data); + + const acceptedFriends = data.filter( + (friendship) => friendship.accepted === true + ); + console.log("Accepted Friends:", acceptedFriends); + const pendingFriends = data.filter( + (friendship) => friendship.accepted === false + ); + console.log("Pending Friends:", pendingFriends); + renderFriendsList(acceptedFriends); + renderFriendRequest2(pendingFriends); } else { console.error("Errore nella risposta del server:", response.statusText); } @@ -97,137 +167,1429 @@ async function getFriends() { } function renderFriendsList(friends) { - console.log("/***********renderFriendsList************/"); - const { userId } = getVariables(); - const friendsList = document.getElementById('friendsList'); - const numericUserId = Number(userId); - - friendsList.innerHTML = friends.map((friend, index) => { - const friendId = (friend.user_1 === numericUserId) ? friend.user_2 : friend.user_1; - return ` -
-

Friend ID: ${friendId}

- -
- `; - }).join(''); + console.log("/***********renderFriendsList************/"); + const { userId } = getVariables(); + const friendsList = document.getElementById("friendsList"); + const numericUserId = Number(userId); + + if (friends.length === 0) { + friendsList.innerHTML = ` +
+ +

No friends yet. Add some friends to get started!

+
+ `; + return; + } + + friendsList.innerHTML = friends + .map((friend, index) => { + const friendInfo = friend.friend_info; + const friendId = friendInfo.user_id || friend.friend_id; + const username = friendInfo.username || "Unknown"; + const email = friendInfo.email || ""; + const isOnline = friendInfo.is_online || false; // Use the is_online data from backend + + const friend_initials = calculateInitials(username); + + return ` +
+ +
+
+ ${friend_initials} +
+ +
+
+
+ + +
+
${username}
+
+ + +
+ +
+
+ `; + }) + .join(""); +} + +/** + * Update a single friend's online status indicator + */ +function updateFriendOnlineStatus(friendId, isOnline) { + const statusElement = document.getElementById(`status-${friendId}`); + if (statusElement) { + statusElement.className = `online-status-indicator rounded-circle position-absolute ${ + isOnline ? 'bg-success' : 'bg-secondary' + }`; + statusElement.style.cssText = "width: 12px; height: 12px; bottom: 2px; right: 2px; border: 2px solid white;"; + statusElement.title = isOnline ? "Online" : "Offline"; + console.log(`Updated friend ${friendId} status to ${isOnline ? 'online' : 'offline'}`); + } +} + +// Add these functions after the renderFriendsList function + +/** + * Invite a friend to play a game + */ +async function inviteToGame(friendId, friendName) { + try { + console.log(`Inviting ${friendName} (ID: ${friendId}) to play`); + + const { token, url_api } = getVariables(); + + // TODO: Replace with your actual game invitation API endpoint + const response = await fetch(`${url_api}/pong/api/invite/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + body: JSON.stringify({ + invited_user_id: friendId, + game_type: "pong", + message: `${friendName}, let's play Pong!`, + }), + }); + + if (response.ok) { + showNotificationToast( + `๐ŸŽฎ Game invitation sent to ${friendName}!`, + "success" + ); + } else { + throw new Error("Failed to send invitation"); + } + } catch (error) { + console.error("Error sending game invitation:", error); + showNotificationToast("โŒ Failed to send game invitation", "error"); + } +} + +/** + * Open chat with a friend + */ +function openChatWithFriend(friendId, friendName) { + console.log(`Opening chat with ${friendName} (ID: ${friendId})`); + + // TODO: Implement chat opening logic + // This could navigate to chat page or open a chat modal + // Example: + // window.navigateTo(`#chat/${friendId}`); + // or + // openChatModal(friendId, friendName); + + showNotificationToast(`๐Ÿ’ฌ Opening chat with ${friendName}`, "info"); } function renderFriendRequest() { - console.log("/***********renderFriendRequest************/"); - const { userId } = getVariables(); - const notificationContent = document.getElementById('notificationContent'); - notificationContent.innerHTML = messageHistory.map((message, index) => { - return ` -
-
-

User ID: ${message.user_id}

- - -
-
- `; - }).join(''); + console.log("/***********renderFriendRequest************/"); + const { userId } = getVariables(); + const notificationContent = document.getElementById("notificationContent"); + + notificationContent.innerHTML = messageHistory + .map((friendRequests, index) => { + const senderData = friendRequests.userData; + const senderId = friendRequests.user_id; + const username = senderData.username || "Unknown"; + console.log("senderdata:", senderData); + return ` +
+
+

Friend request from: ${username}

+ + +
+
+ `; + }) + .join(""); +} + +function renderFriendRequest2(friends) { + console.log("/***********renderFriendRequest2************/"); + const { userId } = getVariables(); + const notificationContent = document.getElementById("notificationContent"); + console.log("Friends:", friends); + + const existingFriendIds = friends.map((f) => f.friend_info.user_id); + messageHistory = messageHistory.filter((msg) => { + // Mantieni solo i messaggi che esistono ancora come pending friends + return existingFriendIds.includes(msg.user_id); + }); + + notificationContent.innerHTML = friends + .map((friendship, index) => { + const Data = friendship.friend_info; + const otherId = friendship.friend_info.user_id; + const actualSenderId = friendship.friend_info.sent_by.user_id; + const username = friendship.friend_info.username || "Unknown"; + console.log("otherId:", otherId); + console.log("actualSenderId:", actualSenderId); + console.log("userId:", userId); + if (actualSenderId == userId) { + return renderSentFriendRequest(otherId, username); + //return ""; // Skip if the user sent the request + } else { + return ` +
+
+

Friend request from: ${username}

+ + +
+
+ `; + } + }) + .join(""); } function renderNotification() { console.log("/***********renderNotification************/"); const { userId } = getVariables(); - const notificationContainer = document.getElementById('notificationContainer'); + const notificationContainer = document.getElementById( + "notificationContainer" + ); notificationContainer.innerHTML = `
-
Notifications
+
Friends
-
- - - +
+ + + + +
+
`; renderFriendRequest(); getFriends(); + initFriendAutocomplete({ + inputId: "friendID", + suggestionListId: "suggestionList", + }); +} + +// Message validation utilities +function validateFriendRequestData(userData) { + const requiredFields = ["user_id", "username"]; + const missingFields = requiredFields.filter((field) => !userData[field]); + + if (missingFields.length > 0) { + console.warn("Friend request missing required fields:", missingFields); + return false; + } + + return true; +} + +function normalizeUserData(userData) { + return { + ...userData, + user_id: userData.user_id || 0, + username: userData.username || "Unknown User", + first_name: userData.first_name || "", + last_name: userData.last_name || "", + level: userData.level || 0, + current_avatar_url: + userData.current_avatar_url || "/static/default_avatar.png", + }; +} + +// WebSocket connection management +function closeWebSocketConnection() { + if (socket && socket.readyState === WebSocket.OPEN) { + console.log("Closing WebSocket connection"); + socket.close(1000, "User initiated close"); + } +} + +/** + * EXTENSIBLE WebSocket MESSAGE HANDLER SYSTEM + * + * This system allows easy addition of new message types by simply adding: + * 1. A handler function following the naming convention: handle[Type]Message + * 2. A type detection case in getMessageType + * + * Features: + * - Auto-registration of message handlers + * - Type-safe message routing + * - Comprehensive error handling + * - Easy extensibility for new message types + */ + +// ==================== MESSAGE HANDLER REGISTRY ==================== + +/** + * Central registry for all WebSocket message handlers + * Add new message types here following the pattern: + * 'message_type': handler_function + */ +const MESSAGE_HANDLERS = { + // Core message types + friend_request: handleFriendRequestMessage, + friend_request_accepted: handleFriendAcceptedMessage, + friendship_deleted: handleFriendDeletedMessage, + friend_blocked: handleFriendBlockedMessage, + friend_unblocked: handleFriendUnblockedMessage, + friend_status_change: handleFriendStatusUpdateMessage, + + // Chat-related messages + chat_room_created: handleChatInviteMessage, + chat_room_joined: handleChatInviteMessage, + chat_room_left: handleChatRoomLeftMessage, + chat_message: handleChatMessage, + + // Game-related messages + game_invitation: handleGameInvitationMessage, + game_started: handleGameStartedMessage, + game_ended: handleGameEndedMessage, + game_created: handleGameCreatedMessage, + tournament_created: handleTournamentCreatedMessage, + tournament_started: handleTournamentStartedMessage, + tournament_ended: handleTournamentEndedMessage, + + // System messages + system_notification: handleSystemNotificationMessage, + maintenance_mode: handleMaintenanceModeMessage, + user_status_changed: handleUserStatusChangedMessage, + + // String-based messages (legacy support) + string_message: handleStringMessage, + heartbeat_request: sendHeartBeat, + heartbeat_ack: handleHeartbeatAckMessage, + pong_invitation: handlePongInvitationMessage, + + // Fallback handler + default: handleDefaultMessage, +}; + +// ping +function handleHeartbeatAckMessage() { + return; +} + +// tournaments +function handleTournamentCreatedMessage(message) { + console.log("Processing tournament created message:", message); + showAlertForXSeconds( + `๐Ÿ† Tournament created: ${message.message.name}`, + "success", + 3, + { asToast: true } + ); + + renderNewTournament(message); +} + + + + +function handlePongInvitationMessage(message) { + console.log("Processing pong invitation message:", message); + + const gameData = message.message?.data || message.data || {}; + const inviterName = gameData.inviter_name || gameData.username || "Someone"; + const inviterId = gameData.inviter_id || gameData.user_id; + const roomId = gameData.room_id || gameData.game_id; + const gameUrl = gameData.game_url; + + // Show invitation modal + showPongInvitationModal(inviterName, inviterId, roomId, gameUrl); +} + +function showPongInvitationModal(inviterName, inviterId, roomId, gameUrl) { + // Remove any existing invitation modals + const existingModal = document.getElementById("pongInvitationModal"); + if (existingModal) { + existingModal.remove(); + } + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML("beforeend", modalHTML); + + // Show the modal + const modal = document.getElementById("pongInvitationModal"); + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + + // Auto-decline after 30 seconds + setTimeout(() => { + if (document.getElementById("pongInvitationModal")) { + bsModal.hide(); + modal.remove(); + } + }, 30000); + } else { + // Fallback if Bootstrap is not available + modal.style.display = "block"; + modal.classList.add("show"); + } + + // Show toast notification as well + showNotificationToast(`๐ŸŽฎ ${inviterName} invited you to play Pong!`, "info"); +} + +// Add these global functions +window.acceptPongInvitation = function (gameUrl) { + console.log(`Accepting pong invitation: ${gameUrl}`); + + // Close the modal + const modal = document.getElementById("pongInvitationModal"); + if (modal) { + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + } + modal.remove(); + } + + // Navigate to multiplayer game + window.location.hash = gameUrl; + + showNotificationToast("Joining Pong game...", "success"); +}; + +window.declinePongInvitation = function () { + console.log("Declining pong invitation"); + + // Close the modal + const modal = document.getElementById("pongInvitationModal"); + if (modal) { + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + } + modal.remove(); + } + + showNotificationToast("Game invitation declined", "info"); +}; + +//chat +function handleChatInviteMessage(message) { + console.log("Processing chat invite message:", message); + + console.log("test: ", message.message.data); + + const chatData = message.message?.data || {}; + const roomName = chatData.room_name || "a chat room"; + const creatorName = chatData.creator_name || "Someone"; + const roomId = chatData.room_id; + + // Mostra notifica toast + if (creatorName != getVariables().userUsername) { + showAlertForXSeconds( + `You have been invited to join "${roomName}" by ${creatorName}`, + "success", + 5, + { asToast: true } + ); + } + + // Aggiorna la lista delle chat + if (typeof updateChatList === "function") { + updateChatList(); + } +} + +//pong +function handleGameCreatedMessage(message) { + console.log("๐Ÿš€ DEBUG: handleGameCreatedMessage called!"); + console.log("๐Ÿš€ DEBUG: Full message:", JSON.stringify(message, null, 2)); + + const gameData = message?.message || {}; + const gameId = gameData.game_id; + const player1Data = gameData.player_1 || {}; + const player2Data = gameData.player_2 || {}; + + console.log("๐Ÿš€ DEBUG: gameData:", gameData); + console.log("๐Ÿš€ DEBUG: gameId:", gameId); + console.log("๐Ÿš€ DEBUG: player1Data:", player1Data); + console.log("๐Ÿš€ DEBUG: player2Data:", player2Data); + + const { userId } = getVariables(); + const currentUserId = parseInt(userId); + + console.log("๐Ÿš€ DEBUG: currentUserId:", currentUserId); + console.log("๐Ÿš€ DEBUG: player1Data.user_id:", player1Data.user_id); + console.log("๐Ÿš€ DEBUG: player2Data.user_id:", player2Data.user_id); + + // Determine if this user is the inviter or the invited player + const isPlayer1 = player1Data.user_id === currentUserId; + const isPlayer2 = player2Data.user_id === currentUserId; + + console.log("๐Ÿš€ DEBUG: isPlayer1:", isPlayer1); + console.log("๐Ÿš€ DEBUG: isPlayer2:", isPlayer2); + + if (isPlayer2) { + // This user is the invited player (player 2) - show invitation modal + const inviterName = player1Data.username || "Someone"; + const inviterId = player1Data.user_id; + + console.log("๐Ÿš€ DEBUG: Player 2 detected! Showing invitation modal"); + console.log("๐Ÿš€ DEBUG: inviterName:", inviterName); + console.log("๐Ÿš€ DEBUG: inviterId:", inviterId); + console.log("๐Ÿš€ DEBUG: gameId:", gameId); + + // Show the invitation modal + showGameInvitationModal(inviterName, inviterId, gameId); + } else if (isPlayer1) { + console.log("๐Ÿš€ DEBUG: Player 1 detected! (Game creator)"); + } else { + console.log("๐Ÿš€ DEBUG: Neither player 1 nor player 2!"); + } +} + +function showGameInvitationModal(inviterName, inviterId, gameId) { + console.log("๐ŸŽฎ DEBUG: showGameInvitationModal called!"); + console.log("๐ŸŽฎ DEBUG: inviterName:", inviterName); + console.log("๐ŸŽฎ DEBUG: inviterId:", inviterId); + console.log("๐ŸŽฎ DEBUG: gameId:", gameId); + + // Remove any existing invitation modals + const existingModal = document.getElementById("gameInvitationModal"); + if (existingModal) { + console.log("๐ŸŽฎ DEBUG: Removing existing modal"); + existingModal.remove(); + } + + console.log("๐ŸŽฎ DEBUG: Creating modal HTML..."); + const modalHTML = ` + + `; + + console.log("๐ŸŽฎ DEBUG: Adding modal to document..."); + document.body.insertAdjacentHTML("beforeend", modalHTML); + + // Show the modal + const modal = document.getElementById("gameInvitationModal"); + console.log("๐ŸŽฎ DEBUG: Modal element found:", !!modal); + + if (modal) { + console.log( + "๐ŸŽฎ DEBUG: Modal HTML:", + modal.outerHTML.substring(0, 200) + "..." + ); + } + + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + console.log("๐ŸŽฎ DEBUG: Bootstrap Modal available, showing modal..."); + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + + // Auto-decline after 30 seconds + setTimeout(() => { + if (document.getElementById("gameInvitationModal")) { + console.log("๐ŸŽฎ DEBUG: Auto-declining modal after 30 seconds"); + bsModal.hide(); + modal.remove(); + } + }, 30000); + } else { + console.log("๐ŸŽฎ DEBUG: Bootstrap Modal not available, using fallback..."); + // Fallback if Bootstrap is not available + modal.style.display = "block"; + modal.classList.add("show"); + modal.style.position = "fixed"; + modal.style.top = "0"; + modal.style.left = "0"; + modal.style.width = "100%"; + modal.style.height = "100%"; + modal.style.backgroundColor = "rgba(0,0,0,0.5)"; + modal.style.zIndex = "9999"; + } + + // Show toast notification as well + console.log("๐ŸŽฎ DEBUG: Showing toast notification..."); + showNotificationToast(`๐ŸŽฎ ${inviterName} invited you to play Pong!`, "info"); +} + +// Add these global functions +window.acceptGameInvitation = function (gameId, opponentId, opponentName) { + console.log( + `Accepting game invitation: game=${gameId}, opponent=${opponentId}` + ); + + // Close the modal + const modal = document.getElementById("gameInvitationModal"); + if (modal) { + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + } + modal.remove(); + } + + // Navigate to multiplayer game with the correct parameters + // Use gameId as roomId since that's what your backend uses + const gameUrl = `#pongmulti?room=${gameId}&opponent=${opponentId}&opponentName=${encodeURIComponent( + opponentName + )}`; + window.navigateTo(gameUrl); + showNotificationToast(`Joining game with ${opponentName}...`, "success"); +}; + +window.declineGameInvitation = function () { + console.log("Declining game invitation"); + + // Close the modal + const modal = document.getElementById("gameInvitationModal"); + if (modal) { + if (typeof bootstrap !== "undefined" && bootstrap.Modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) bsModal.hide(); + } + modal.remove(); + } + + showNotificationToast("Game invitation declined", "info"); +}; + +/** + * Enhanced message type detection with support for multiple message formats + */ +function getMessageType(message) { + // Validate message structure + if (!message || typeof message !== "object") { + console.warn("Invalid message structure:", message); + return "default"; + } + + // Check for structured message with explicit type + if (message.message && typeof message.message === "object") { + const messageType = message.message.type; + + if (messageType) { + console.log(`Detected structured message type: ${messageType}`); + return messageType; + } + } + + // Check for direct type field + if (message.type && typeof message.type === "string") { + console.log(`Detected direct message type: ${message.type}`); + return message.type; + } + + // Handle string-based messages (legacy support) + if (typeof message.message === "string") { + const stringMessage = message.message.toLowerCase(); + + // Map string patterns to specific message types + const stringTypeMapping = { + "accepted your friend request": "friend_accepted", + "deleted friendship with": "friend_deleted", + "blocked you": "friend_blocked", + "unblocked you": "friend_unblocked", + "chat room": "chat_room_created", + "joined chat room": "chat_room_joined", + "left chat room": "chat_room_left", + "game invitation": "game_invitation", + "game started": "game_started", + "game ended": "game_ended", + tournament: "tournament_started", + maintenance: "maintenance_mode", + }; + + // Find matching string pattern + for (const [pattern, type] of Object.entries(stringTypeMapping)) { + if (stringMessage.includes(pattern)) { + console.log( + `Detected string message type: ${type} (pattern: ${pattern})` + ); + return type; + } + } + + console.log("Detected generic string message"); + return "string_message"; + } + + // Log unhandled message types for debugging + console.log("Unhandled message type:", message); + return "default"; +} + +// Centralized error handling for message processing +function handleMessageError(error, message) { + console.error("Error processing WebSocket message:", { + error: error.message, + stack: error.stack, + message: message, + }); + + // Fallback to default handler + handleDefaultMessage(); +} + +// Message Handler Functions +function handleFriendRequestMessage(message) { + const userData = message.message.data; + + // Validate friend request data + if (!validateFriendRequestData(userData)) { + console.error("Invalid friend request data received"); + return; + } + + console.log("Processing friend request message:"); + console.log(`User avatar: ${userData.current_avatar_url}`); + console.log(`User status: ${userData.first_name} ${userData.last_name}`); + console.log(`User level: ${userData.level}`); + console.log(`User ID: ${userData.user_id}`); + console.log(`Friend request from: ${userData.username}`); + + // Store message in history with normalized data + messageHistory.push({ + user_id: userData.user_id, + type: "friend_request", + userData: normalizeUserData(userData), + }); + + renderFriendRequest(); +} + +function handleStringMessage(message) { + const info = message.message; + + console.log("Processing string message:", info); + + // Route string messages based on content + const stringMessageActions = { + "accepted your friend request": () => getFriends(), + "deleted friendship with": () => getFriends(), + "Chat Room": () => updateChatList(), + }; + + // Find matching action or default to renderFriendRequest + const matchedAction = Object.keys(stringMessageActions).find((key) => + info.includes(key) + ); + + if (matchedAction) { + stringMessageActions[matchedAction](); + } else { + renderFriendRequest(); + } +} + +function handleDefaultMessage(message = null) { + console.log("Processing default/unknown message:", message); + renderFriendRequest(); } function initializeWebSocket() { - const { token } = getVariables(); - const wsUrl = `ws://127.0.0.1:8003/ws/user_notifications/?token=${token}`; + const { token, wss_api } = getVariables(); + + // Use secure header-based authentication instead of query parameters + const wsUrl = `${wss_api}/notifications/ws/user_notifications/?token=${token}`; + // Create WebSocket with token in query string socket = new WebSocket(wsUrl); + if (!window.activeWebSockets) window.activeWebSockets = []; + window.activeWebSockets.push(socket); socket.onmessage = function (event) { - const message = JSON.parse(event.data); - console.log("/----websocket notification.js----\\"); - console.log("Nuovo messaggio:", message); - - const messageId = message.id; - const sender = message.Sender; - const sender_id = Number(message.message); - const isSent = message.is_sent; - const userId = message.user_id; - const info = message.message; - console.log("Message ID:", messageId); - console.log("Sender:", sender); - console.log("sender_id:", sender_id); - console.log("Is Sent:", isSent); - console.log("User ID:", userId); - console.log("Info:", info); //------------------------ - - messageHistory.push(message); - if (info === "accepted your friend request") { - getFriends(); - } - else if (info.includes("deleted friendship with")) { - getFriends() - // const parts = info.split(' '); - // const friendId = parts[parts.length - 1]; // ultimo token, es. "4" - // console.log("deleted friendship with ID:", friendId); - - // const friendItems = Array.from(document.querySelectorAll('.friend-item')); - // const itemToRemove = friendItems.find(item => item.innerText.includes(`Friend ID: ${friendId}`)); - // if (itemToRemove) { - // itemToRemove.remove(); - // } - } - else if (info.includes("Chat Room")) { - updateChatList(); - } - else { - renderFriendRequest(); + + try { + const message = JSON.parse(event.data); + console.log("Received message:", message); + + // Determine message type and route to appropriate handler + const messageType = getMessageType(message); + const handler = + MESSAGE_HANDLERS[messageType] || MESSAGE_HANDLERS["default"]; + + console.log(`Routing to handler: ${messageType}`); + handler(message); + } catch (error) { + console.error("Failed to parse WebSocket message:", error); + handleMessageError(error, event.data); } console.log("\\____websocket notification.js____/"); }; socket.onopen = function () { - console.log("WebSocket connection is active"); + console.log("WebSocket connection established successfully"); + // Reset any reconnection attempts if successful + if (window.wsReconnectAttempts) { + window.wsReconnectAttempts = 0; + } + startHeartbeat(); + console.log("WebSocket heartbeat started"); }; - socket.onclose = function () { - console.log("token:", token); - console.log("WebSocket connection is not active"); + socket.onclose = function (event) { + console.log("WebSocket connection closed:", { + code: event.code, + reason: event.reason, + wasClean: event.wasClean, + }); + + // Handle different close codes + if (event.code === 1006) { + console.warn("WebSocket connection lost unexpectedly"); + } else if (event.code === 1000) { + console.log("WebSocket closed normally"); + } else { + console.warn("WebSocket closed with code:", event.code); + } + }; + + socket.onerror = function (error) { + console.error("WebSocket error occurred:", error); }; } -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener("DOMContentLoaded", function () { window.renderNotification = renderNotification; window.handleFriendRequest = handleFriendRequest; window.getFriends = getFriends; window.initializeWebSocket = initializeWebSocket; + window.closeWebSocketConnection = closeWebSocketConnection; + window.registerMessageHandler = registerMessageHandler; + window.unregisterMessageHandler = unregisterMessageHandler; + window.getRegisteredMessageTypes = getRegisteredMessageTypes; + window.testMessageHandler = testMessageHandler; }); -export { renderNotification, handleFriendRequest, getFriends, initializeWebSocket }; \ No newline at end of file +export { + renderNotification, + handleFriendRequest, + getFriends, + initializeWebSocket, + closeWebSocketConnection, + registerMessageHandler, + unregisterMessageHandler, + getRegisteredMessageTypes, + testMessageHandler, + MESSAGE_HANDLERS, +}; + +// ==================== DYNAMIC HANDLER REGISTRATION ==================== + +/** + * Register a new message handler dynamically + * @param {string} messageType - The message type to handle + * @param {function} handlerFunction - The function to handle this message type + */ +function registerMessageHandler(messageType, handlerFunction) { + if (typeof handlerFunction !== "function") { + console.error(`Handler for ${messageType} must be a function`); + return false; + } + + MESSAGE_HANDLERS[messageType] = handlerFunction; + console.log(`Registered new message handler: ${messageType}`); + return true; +} + +/** + * Remove a message handler + * @param {string} messageType - The message type to remove + */ +function unregisterMessageHandler(messageType) { + if (messageType === "default") { + console.error("Cannot remove default message handler"); + return false; + } + + delete MESSAGE_HANDLERS[messageType]; + console.log(`Unregistered message handler: ${messageType}`); + return true; +} + +/** + * Get all registered message types + * @returns {string[]} Array of registered message types + */ +function getRegisteredMessageTypes() { + return Object.keys(MESSAGE_HANDLERS); +} + +/** + * Test a message handler with sample data + * @param {string} messageType - The message type to test + * @param {object} sampleMessage - Sample message data for testing + */ +function testMessageHandler(messageType, sampleMessage) { + const handler = MESSAGE_HANDLERS[messageType]; + if (!handler) { + console.error(`No handler found for message type: ${messageType}`); + return false; + } + + console.log(`Testing handler for: ${messageType}`); + try { + handler(sampleMessage); + console.log(`โœ… Handler test successful for: ${messageType}`); + return true; + } catch (error) { + console.error(`โŒ Handler test failed for: ${messageType}`, error); + return false; + } +} + +// ==================== MESSAGE PROCESSING UTILITIES ==================== + +// Friend-related message handlers +function handleFriendAcceptedMessage(message) { + console.log("Processing friend accepted message:", message); + + // Update friends list to reflect new friendship + getFriends(); + + // Show notification if user data is available + if (message.message && message.message.data) { + const userData = message.message.data; + showNotificationToast( + `${userData.username || "Someone"} accepted your friend request!`, + "success" + ); + } else { + showNotificationToast("Your friend request was accepted!", "success"); + } +} + +function handleFriendDeletedMessage(message) { + console.log("Processing friend deleted message:", message); + + // Update friends list to reflect removed friendship + getFriends(); + + // Show notification + if (message.message && message.message.data) { + const userData = message.message.data; + console.log("User data:", userData); + showNotificationToast( + `${userData.username || "Someone"} removed you from their friends list`, + "warning" + ); + } else { + showNotificationToast("A friendship was removed", "warning"); + } +} + +function handleFriendBlockedMessage(message) { + console.log("Processing friend blocked message:", message); + + // Update friends list and remove any ongoing interactions + getFriends(); + + // Show notification + if (message.message && message.message.data) { + const userData = message.message.data; + showNotificationToast( + `${userData.username || "Someone"} blocked you`, + "error" + ); + } else { + showNotificationToast("You have been blocked by a user", "error"); + } +} + +function handleFriendUnblockedMessage(message) { + console.log("Processing friend unblocked message:", message); + + // Show notification + if (message.message && message.message.data) { + const userData = message.message.data; + showNotificationToast( + `${userData.username || "Someone"} unblocked you`, + "info" + ); + } else { + showNotificationToast("You have been unblocked by a user", "info"); + } +} + +function handleFriendStatusUpdateMessage(message) { + console.log("Processing friend status update message:", message); + + // Extract friend data from message + let friendUserId, isOnline; + + if (message.friend_user_id !== undefined) { + // Direct format from Redis pub/sub (expected by OnlineStatusManager) + friendUserId = message.friend_user_id; + isOnline = message.is_online; + } else if (message.message && message.message.friend_user_id !== undefined) { + // Nested format + friendUserId = message.message.friend_user_id; + isOnline = message.message.is_online; + } else { + console.warn("Friend status update message missing required data:", message); + return; + } + + // Use OnlineStatusManager if available, otherwise fallback to direct update + if (window.onlineStatusManager) { + window.onlineStatusManager.updateUserStatus(friendUserId, isOnline); + } else { + // Fallback to direct UI update + updateFriendOnlineStatus(friendUserId, isOnline); + } + + console.log(`Updated friend ${friendUserId} status to ${isOnline ? 'online' : 'offline'}`); +} + +// Chat-related message handlers +function handleChatRoomCreatedMessage(message) { + console.log("Processing chat room created message:", message); + + // Update chat list to show new room + updateChatList(); + + // Show notification + const roomData = message.message?.data || {}; + const roomName = roomData.room_name || "A new chat room"; + showNotificationToast(`${roomName} was created`, "info"); +} + +function handleChatRoomJoinedMessage(message) { + console.log("Processing chat room joined message:", message); + + // Update chat list + updateChatList(); + + // Show notification + const roomData = message.message?.data || {}; + const roomName = roomData.room_name || "a chat room"; + const userName = roomData.user_name || "Someone"; + showNotificationToast(`${userName} joined ${roomName}`, "info"); +} + +function handleChatRoomLeftMessage(message) { + console.log("Processing chat room left message:", message); + + // Update chat list + updateChatList(); + + // Show notification + const roomData = message.message?.data || {}; + const roomName = roomData.room_name || "a chat room"; + const userName = roomData.user_name || "Someone"; + showNotificationToast(`${userName} left ${roomName}`, "info"); +} + +function handleChatMessage(message) { + console.log("Processing chat message:", message); + + // Handle real-time chat message (if chat is open) + const messageData = message.message?.data || {}; + const senderName = messageData.sender_name || "Someone"; + const roomName = messageData.room_name || "a chat room"; + + // Only show notification if user is not currently in that chat room + if (!isUserInChatRoom(messageData.room_id)) { + showNotificationToast( + `New message from ${senderName} in ${roomName}`, + "info" + ); + } +} + +// Game-related message handlers +function handleGameInvitationMessage(message) { + console.log("Processing game invitation message:", message); + + const gameData = message.message?.data || {}; + const inviterName = gameData.inviter_name || "Someone"; + const gameType = gameData.game_type || "a game"; + + // Store invitation in message history for user action + messageHistory.push({ + user_id: gameData.inviter_id || 0, + type: "game_invitation", + userData: gameData, + invitation_id: gameData.invitation_id, + }); + + renderFriendRequest(); // Reuse existing notification rendering + showNotificationToast( + `${inviterName} invited you to play ${gameType}`, + "info" + ); +} + +function handleGameStartedMessage(message) { + console.log("Processing game started message:", message); + + const gameData = message.message?.data || {}; + const gameType = gameData.game_type || "game"; + + showNotificationToast(`Your ${gameType} has started!`, "success"); + + // Redirect to game if needed + if (gameData.game_url) { + setTimeout(() => { + window.location.href = gameData.game_url; + }, 2000); + } +} + +function handleGameEndedMessage(message) { + console.log("Processing game ended message:", message); + + const gameData = message.message?.data || {}; + const result = gameData.result || "completed"; + const gameType = gameData.game_type || "game"; + + showNotificationToast(`Your ${gameType} has ${result}`, "info"); +} + +function handleTournamentStartedMessage(message) { + console.log("Processing tournament started message:", message); + + const tournamentData = message.message?.data || {}; + const tournamentName = tournamentData.tournament_name || "tournament"; + + showNotificationToast( + `Tournament "${tournamentName}" has started!`, + "success" + ); +} + +function handleTournamentEndedMessage(message) { + console.log("Processing tournament ended message:", message); + + const tournamentData = message.message?.data || {}; + const tournamentName = tournamentData.tournament_name || "tournament"; + const placement = tournamentData.user_placement; + + let notificationText = `Tournament "${tournamentName}" has ended`; + if (placement) { + notificationText += `. You finished in ${placement} place!`; + } + + showNotificationToast(notificationText, "info"); +} + +// System message handlers +function handleSystemNotificationMessage(message) { + console.log("Processing system notification:", message); + + const notificationData = message.message?.data || {}; + const notificationText = + notificationData.text || message.message || "System notification"; + const priority = notificationData.priority || "info"; + + showNotificationToast(notificationText, priority); +} + +function handleMaintenanceModeMessage(message) { + console.log("Processing maintenance mode message:", message); + + const maintenanceData = message.message?.data || {}; + const startTime = maintenanceData.start_time; + const duration = maintenanceData.duration || "unknown duration"; + + let notificationText = "Maintenance mode scheduled"; + if (startTime) { + notificationText += ` starting at ${startTime}`; + } + notificationText += ` (${duration})`; + + showNotificationToast(notificationText, "warning"); +} + +function handleUserStatusChangedMessage(message) { + console.log("Processing user status changed message:", message); + + const statusData = message.message?.data || {}; + const userName = statusData.username || "A friend"; + const newStatus = statusData.status || "changed status"; + + // Only show for friends who come online/offline + if (statusData.is_friend && ["online", "offline"].includes(newStatus)) { + showNotificationToast(`${userName} is now ${newStatus}`, "info"); + } +} + +// Utility Functions +function showNotificationToast(message, type = "info") { + console.log(`${type.toUpperCase()}: ${message}`); + + // Create a simple toast notification + const toast = document.createElement("div"); + toast.className = `alert alert-${ + type === "error" ? "danger" : type + } toast-notification`; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 300px; + opacity: 0; + transition: opacity 0.3s ease; + `; + toast.textContent = message; + + document.body.appendChild(toast); + + // Fade in + setTimeout(() => (toast.style.opacity = "1"), 100); + + // Fade out and remove + setTimeout(() => { + toast.style.opacity = "0"; + setTimeout(() => document.body.removeChild(toast), 300); + }, 5000); +} + +function isUserInChatRoom(roomId) { + // Check if user is currently viewing this chat room + // This would need to be implemented based on your chat system + const currentRoomId = getCurrentChatRoomId(); // You'll need to implement this + return currentRoomId === roomId; +} + +function getCurrentChatRoomId() { + // Implement this based on your chat system + // For example, check URL parameters or global state + return null; // Placeholder +} + +function startHeartbeat() { + if (!socket || socket.readyState !== WebSocket.OPEN) { + console.warn("WebSocket is not open, cannot start heartbeat"); + return; + } + + window.notificationHeartbeatInterval = setInterval(() => { + console.log("Sending WebSocket heartbeat..."); + sendHeartBeat(); + }, 25000); + // console.log("Starting WebSocket heartbeat..."); + // sendHeartBeat(); + // // Send a heartbeat request every 25 seconds + // setInterval(() => { + // sendHeartBeat(); + // }, 25000); +} + +function sendHeartBeat() { + if (!socket || socket.readyState !== WebSocket.OPEN) { + console.warn("WebSocket is not open, cannot send heartbeat"); + return; + } + try { + socket.send(JSON.stringify({ type: "heartbeat" })); + } catch (error) { + console.error("Failed to send heartbeat request:", error); + } +} + +// Enhanced Message Handler Functions (existing ones updated) + +// ==================== USAGE EXAMPLES AND DOCUMENTATION ==================== + +/** + * EXAMPLES: How to add new message handlers + * + * Example 1: Add a simple custom message handler + * ----------------------------------------------- + * registerMessageHandler('custom_notification', function(message) { + * console.log('Custom notification received:', message); + * showNotificationToast(message.message?.text || 'Custom notification', 'info'); + * }); + * + * Example 2: Add a complex handler with validation + * ------------------------------------------------ + * registerMessageHandler('payment_notification', function(message) { + * const paymentData = message.message?.data; + * if (!paymentData || !paymentData.amount) { + * console.error('Invalid payment notification data'); + * return; + * } + * + * const amount = paymentData.amount; + * const currency = paymentData.currency || 'USD'; + * const status = paymentData.status || 'completed'; + * + * showNotificationToast( + * `Payment ${status}: ${amount} ${currency}`, + * status === 'completed' ? 'success' : 'warning' + * ); + * }); + * + * Example 3: Backend message format support + * ----------------------------------------- + * Backend should send messages in one of these formats: + * + * Format 1 - Structured message: + * { + * "message": { + * "type": "friend_request", + * "data": { "user_id": 123, "username": "john_doe", ... } + * } + * } + * + * Format 2 - Direct type: + * { + * "type": "game_invitation", + * "data": { "game_type": "pong", "inviter_name": "Alice", ... } + * } + * + * Format 3 - String message (legacy): + * { + * "message": "accepted your friend request" + * } + * + * Example 4: Testing handlers + * --------------------------- + * testMessageHandler('friend_request', { + * message: { + * type: 'friend_request', + * data: { user_id: 123, username: 'test_user' } + * } + * }); + * + * Example 5: List all registered handlers + * --------------------------------------- + * console.log('Registered handlers:', getRegisteredMessageTypes()); + * + * Example 6: Remove a handler + * --------------------------- + * unregisterMessageHandler('custom_notification'); + */ + +/** + * SUPPORTED MESSAGE TYPES (auto-registered): + * + * Friend System: + * - friend_request: New friend request received + * - friend_accepted: Friend request was accepted + * - friend_deleted: Friendship was removed + * - friend_blocked: User was blocked + * - friend_unblocked: User was unblocked + * + * Chat System: + * - chat_room_created: New chat room created + * - chat_room_joined: User joined a chat room + * - chat_room_left: User left a chat room + * - chat_message: New chat message (if not in room) + * + * Game System: + * - game_invitation: Game invitation received + * - game_started: Game has started + * - game_ended: Game has ended + * - tournament_started: Tournament has started + * - tournament_ended: Tournament has ended + * + * System: + * - system_notification: General system notification + * - maintenance_mode: Maintenance mode notification + * - user_status_changed: User status changed (online/offline) + * - string_message: Legacy string-based messages + * - default: Fallback for unknown message types + */ + +console.log("๐Ÿš€ Enhanced WebSocket Notification System Loaded"); +console.log("๐Ÿ“‹ Registered message types:", getRegisteredMessageTypes().length); +console.log("๐Ÿ’ก Use registerMessageHandler() to add custom handlers"); +console.log("๐Ÿ”ง Use testMessageHandler() to test your handlers"); diff --git a/Front-End/public/pong/ai.js b/Front-End/public/pong/ai.js deleted file mode 100644 index 251d01bb..00000000 --- a/Front-End/public/pong/ai.js +++ /dev/null @@ -1,53 +0,0 @@ -import { state } from "./state.js"; - -export function moveIA() { - const iaSpeed = state.player_speed; - const delay = 1; - const timeStep = 0.01; - let simulatedBallY = state.ball.position.y; - let simulatedBallX = state.ball.position.x; - let simulatedAngle = state.angle; - let timeElapsed = 0; - - while (timeElapsed < delay) { - simulatedBallY += - state.ball_speed * - Math.sin((simulatedAngle * Math.PI) / 180) * - timeStep; - simulatedBallX += - state.ball_speed * - Math.cos((simulatedAngle * Math.PI) / 180) * - timeStep; - timeElapsed += timeStep; - - if ( - simulatedBallY + state.ball_radius + state.ball_speed > - state.ring.length / 2 || - simulatedBallY - state.ball_radius + state.ball_speed < - -state.ring.length / 2 - ) { - simulatedAngle *= -1; - } - } - - const futureBallY = simulatedBallY; - - if (futureBallY > state.p2.position.y + state.player.length / 4) { - state.p2_move_y = iaSpeed; - } else if (futureBallY < state.p2.position.y - state.player.length / 4) { - state.p2_move_y = -iaSpeed; - } else { - state.p2_move_y = 0; - } - - if ( - (state.p2_move_y > 0 && - state.p2.position.y + state.player.length / 2 <= - state.ring.length / 2 - state.ring.thickness / 2) || - (state.p2_move_y < 0 && - state.p2.position.y - state.player.length / 2 >= - -state.ring.length / 2 + state.ring.thickness / 2) - ) { - state.p2.position.y += state.p2_move_y; - } -} diff --git a/Front-End/public/pong/friendlist.css b/Front-End/public/pong/friendlist.css new file mode 100644 index 00000000..ba565edd --- /dev/null +++ b/Front-End/public/pong/friendlist.css @@ -0,0 +1,204 @@ +/* Friend List Modal Styles */ +.friend-list-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1050; + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.friend-list-container { + background: white; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.friend-list-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1.5rem; + text-align: center; + border-radius: 12px 12px 0 0; +} + +.friend-list-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.friend-list-body { + max-height: 400px; + overflow-y: auto; + padding: 0; +} + +.friend-item { + padding: 1rem 1.5rem; + border-bottom: 1px solid #e9ecef; + transition: all 0.2s ease; +} + +.friend-item:hover { + background-color: #f8f9fa; +} + +.friend-item:last-child { + border-bottom: none; +} + +.friend-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + font-size: 1.1rem; + position: relative; + flex-shrink: 0; +} + +.friend-name { + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; +} + +.friend-status { + font-size: 0.875rem; + color: #6c757d; + margin: 0; +} + +.btn-game-invite { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + border: none; + color: white; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(40, 167, 69, 0.2); +} + +.btn-game-invite:hover:not(:disabled) { + background: linear-gradient(135deg, #218838 0%, #17a2b8 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); + color: white; +} + +.btn-game-invite:disabled { + background: #6c757d; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.btn-close-custom { + background: #6c757d; + border: none; + color: white; + font-weight: 500; + transition: all 0.2s ease; +} + +.btn-close-custom:hover { + background: #5a6268; + transform: translateY(-1px); + color: white; +} + +.empty-state { + text-align: center; + padding: 3rem 2rem; + color: #6c757d; +} + +.empty-state-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.loading-state { + text-align: center; + padding: 3rem 2rem; + color: #6c757d; +} + +.loading-spinner { + width: 2.5rem; + height: 2.5rem; + margin-bottom: 1rem; +} + +/* Custom scrollbar */ +.friend-list-body::-webkit-scrollbar { + width: 6px; +} + +.friend-list-body::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.friend-list-body::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.friend-list-body::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Responsive design */ +@media (max-width: 576px) { + .friend-list-container { + width: 95%; + margin: 1rem; + } + + .friend-item { + padding: 0.75rem 1rem; + } + + .friend-avatar { + width: 40px; + height: 40px; + font-size: 1rem; + } + + .btn-game-invite { + font-size: 0.8rem; + padding: 0.375rem 0.75rem; + } +} \ No newline at end of file diff --git a/Front-End/public/pong/friendlist.html b/Front-End/public/pong/friendlist.html new file mode 100644 index 00000000..d47cb9fd --- /dev/null +++ b/Front-End/public/pong/friendlist.html @@ -0,0 +1,45 @@ +
+
+

+ + Select a Friend to Play +

+
+ +
+ +
+
+ Loading... +
+

Loading your friends...

+
+ + + + + + + + + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/Front-End/public/pong/gameLogic.js b/Front-End/public/pong/gameLogic.js deleted file mode 100644 index f7c2c8cb..00000000 --- a/Front-End/public/pong/gameLogic.js +++ /dev/null @@ -1,112 +0,0 @@ -import * as THREE from "three"; -import { FontLoader } from "three/addons/loaders/FontLoader.js"; -import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; -import { Group, remove } from "three/addons/libs/tween.module.js"; -import { state } from "./state.js"; -import * as IA from "./ai.js"; -import * as UTILS from "./utils.js"; -import * as SETUP from "./setup.js"; -import * as SETTINGS from "./settings.js"; -import * as UP from "./powerups.js"; -import { initializeWebSocket } from "./serverSide.js"; - -let previousTimestamp = 0; -const timeStep = 1000 / 60; - -export const animate = (timestamp) => { - console.log("animate"); - state.stats.begin(); - requestAnimationFrame(animate); - const deltaTime = timestamp - previousTimestamp; - if (deltaTime >= timeStep) { - previousTimestamp = timestamp; - if (!state.isPaused) { - if (UTILS.p1IsHit()) { - state.hit_position = - state.ball.position.y - state.p1.position.y; - state.wallHitPosition = 0; - state.angle = (state.hit_position / state.player.length) * -90; - if (state.ball_speed < 5 * state.player.width) - state.ball_speed += 0.1; - } else if (UTILS.p2IsHit()) { - state.hit_position = - state.ball.position.y - state.p2.position.y; - state.wallHitPosition = 0; - state.angle = - 180 + (state.hit_position / state.player.length) * 90; - if (state.ball_speed < 5 * state.player.width) - state.ball_speed += 0.1; - } else if ( - (state.wallHitPosition <= 0 && - state.ball.position.y + - state.ball_radius + - state.ball_speed >= - state.ring.length / 2) || - (state.wallHitPosition >= 0 && - state.ball.position.y - - state.ball_radius - - state.ball_speed <= - -state.ring.length / 2) - ) { - state.wallHitPosition = state.ball.position.y; - state.angle *= -1; - } else if ( - state.ball.position.x - state.ball_radius < - state.r_left.position.x + state.ring.thickness - ) { - console.log("p2 ha segnato"); - state.p2_score += 1; - UTILS.score(); - } else if ( - state.ball.position.x + state.ball_radius > - state.r_right.position.x - state.ring.thickness - ) { - console.log("p1 ha segnato"); - state.p1_score += 1; - UTILS.score(); - } - state.ball.position.y += - state.ball_speed * -Math.sin((state.angle * Math.PI) / 180); - state.ball.position.x += - state.ball_speed * Math.cos((state.angle * Math.PI) / 180); - if ( - (state.p1_move_y > 0 && - state.p1.position.y + state.player.length / 2 <= - state.ring.length / 2 - state.ring.thickness / 2) || - (state.p1_move_y < 0 && - state.p1.position.y - state.player.length / 2 >= - -state.ring.length / 2 + state.ring.thickness / 2) - ) - state.p1.position.y += state.p1_move_y; - if (state.IAisActive) IA.moveIA(); - if ( - (state.p2_move_y > 0 && - state.p2.position.y + state.player.length / 2 <= - state.ring.length / 2 - state.ring.thickness / 2) || - (state.p2_move_y < 0 && - state.p2.position.y - state.player.length / 2 >= - -state.ring.length / 2 + state.ring.thickness / 2) - ) - state.p2.position.y += state.p2_move_y; - if ( - state.spawnPowerUpFlag && - timestamp - state.lastPowerUpSpawnTime > - state.powerUpInterval && - state.powerUps.length < 10 - ) { - UP.spawnPowerUp(); - state.lastPowerUpSpawnTime = timestamp; - } - - // Handle power-up collisions - UP.handlePowerUpCollision(); - } - - state.renderer.render(state.scene, state.camera); - state.stats.end(); - } -}; - -export const serverAnimate = () => { - state.renderer.render(state.scene, state.camera); -}; diff --git a/Front-End/public/pong/gungeon.png b/Front-End/public/pong/gungeon.png deleted file mode 100644 index b84bd345..00000000 Binary files a/Front-End/public/pong/gungeon.png and /dev/null differ diff --git a/Front-End/public/pong/locale/ai.js b/Front-End/public/pong/locale/ai.js new file mode 100644 index 00000000..80f0d90c --- /dev/null +++ b/Front-End/public/pong/locale/ai.js @@ -0,0 +1,21 @@ +import { state } from "./state.js"; + +let frameCounter = 0; + +export function moveIA() { + const ai = state.players[1]; + const ball = state.ball; + + frameCounter++; + if (!ai || !ai.mesh || !ball || !ball.mesh) return; + const aiZ = ai.mesh.position.z; + const ballZ = ball.mesh.position.z; + const moveDistance = ballZ - aiZ; + let moveAmount = 0; + if (Math.abs(moveDistance) > state.player_speed) { + moveAmount = Math.sign(moveDistance) * state.player_speed; + } else { + moveAmount = moveDistance; + } + ai.move(moveAmount); +} diff --git a/Front-End/public/pong/locale/gameLogic.js b/Front-End/public/pong/locale/gameLogic.js new file mode 100644 index 00000000..ae1a80b6 --- /dev/null +++ b/Front-End/public/pong/locale/gameLogic.js @@ -0,0 +1,45 @@ +import * as THREE from "three"; +import { FontLoader } from "three/addons/loaders/FontLoader.js"; +import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; +import { Group, remove } from "three/addons/libs/tween.module.js"; +import { moveIA } from "./ai.js"; +import { state } from "./state.js"; + +const clock = new THREE.Clock(); + +export function animate() { + const originalDelta = clock.getDelta(); + const deltaTime = originalDelta; // Convert to milliseconds + + if (state.isPaused === false) { + if (state.stats) { + state.stats.update(); + } + + // Skip ball physics in multiplayer - server is authoritative + if (state.ball && !state.isMultiplayer) { + state.ball.update(deltaTime); + } + + // Skip player movement calculations in multiplayer - server controls positions + if (!state.isMultiplayer) { + if (state.players[0]) { + state.players[0].move(state.p1_move_y); + } + + if (state.players[1] && state.IAisActive) { + moveIA(); + } else if (state.players[1]) { + state.players[1].move(state.p2_move_y); + } + } + + if (state.controls) { + state.controls.update(); + } + } + if (state.renderer && state.scene && state.camera) { + state.renderer.render(state.scene, state.camera); + } + state.animationFrameId = requestAnimationFrame(animate); +} diff --git a/Front-End/public/pong/locale/pong.css b/Front-End/public/pong/locale/pong.css new file mode 100644 index 00000000..b494c510 --- /dev/null +++ b/Front-End/public/pong/locale/pong.css @@ -0,0 +1,235 @@ +@import url("https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"); + +/* Game container styles */ +.pong-app { + position: absolute; + top: 0px; /* Top navbar height */ + left: 0px; /* Left navbar width */ + right: 0; + bottom: 0; + overflow: hidden; + font-family: "Press Start 2P", cursive, sans-serif; + /* background-color: #000; */ + margin: 0; + border-radius: 12px; +} + +/* Game container */ +.pong-app .gamecontainer { + width: 100%; + height: 100%; + overflow: hidden; + position: relative; + /* background: linear-gradient(135deg, #000 0%, #111 100%); */ +} + +.pong-app #threejs-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-height: 400px; +} + +/* Common menu styles */ +.pong-app #menu, +.pong-app #nbrOfPlayerMenu, +.pong-app #settingsMenu, +.pong-app #pauseMenu { + transition: all 0.3s ease; + min-width: 320px; + max-width: 450px; + border: 3px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 0 30px rgba(0, 128, 255, 0.5), + inset 0 0 15px rgba(0, 128, 255, 0.3); + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + padding: 25px; +} + +/* Menu headings */ +.pong-app #menu h1, +.pong-app #nbrOfPlayerMenu h2, +.pong-app #settingsMenu h2, +.pong-app #pauseMenu h2 { + color: #fff; + text-shadow: 0 0 10px #4deeea, 0 0 20px #0bff01; + margin-bottom: 1.5rem; + letter-spacing: 2px; +} + +/* Button enhancements */ +.pong-app .btn { + font-family: "Press Start 2P", cursive, sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + transition: all 0.2s; +} + +.pong-app .btn:hover { + transform: translateY(-2px); + box-shadow: 0 7px 10px rgba(0, 0, 0, 0.3); +} + +.pong-app .btn:active { + transform: translateY(1px); +} + +/* Custom button styles */ +.pong-app .btn-primary { + background: linear-gradient(to bottom, #4deeea, #2e8b8a); + border-color: #4deeea; +} + +.pong-app .btn-secondary { + background: linear-gradient(to bottom, #8c5fb3, #6a3b8d); + border-color: #8c5fb3; +} + +.pong-app .btn-success { + background: linear-gradient(to bottom, #0bff01, #089c00); + border-color: #0bff01; +} + +.pong-app .btn-info { + background: linear-gradient(to bottom, #74e8ff, #2bbeff); + border-color: #74e8ff; +} + +.pong-app .btn-warning { + background: linear-gradient(to bottom, #ffe700, #ffc107); + border-color: #ffe700; +} + +.pong-app .btn-danger { + background: linear-gradient(to bottom, #ff2b2b, #c41e3a); + border-color: #ff2b2b; +} + +/* Settings menu enhancements */ +.pong-app #settingsMenu { + max-width: 400px; +} + +.pong-app #settingsMenu .mb-3 { + margin-bottom: 1.2rem !important; +} + +.pong-app .form-label { + font-size: 0.8rem; + margin-bottom: 0.5rem; + color: #ddd; + text-shadow: 0 0 5px rgba(255, 255, 255, 0.3); +} + +/* Custom styling for color pickers */ +.pong-app .form-control-color { + width: 100%; + height: 38px; + border: 2px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +.pong-app .form-control-color:hover { + border-color: rgba(255, 255, 255, 0.4); +} + +/* Form check styling */ +.pong-app .form-check-input { + background-color: rgba(0, 0, 0, 0.5); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.pong-app .form-check-input:checked { + background-color: #4deeea; + border-color: #4deeea; +} + +.pong-app .form-check-label { + margin-left: 0.3rem; +} + +/* Game over image styling */ +.pong-app #gameOverImage { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 60%; + max-height: 60%; + display: none; + filter: drop-shadow(0 0 15px rgba(255, 0, 0, 0.7)); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + opacity: 0.7; + transform: translate(-50%, -50%) scale(0.95); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0.7; + transform: translate(-50%, -50%) scale(0.95); + } +} + +/* Score display */ +.pong-app .score-display { + position: absolute; + top: 20px; + left: 0; + right: 0; + display: flex; + justify-content: center; + gap: 100px; + font-size: 2.5rem; + color: #fff; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + z-index: 1000; + font-family: "Press Start 2P", cursive, sans-serif; +} + +/* Media queries for responsive design */ +@media (max-width: 768px) { + .pong-app #menu, + .pong-app #nbrOfPlayerMenu, + .pong-app #modeMenu, + .pong-app #settingsMenu, + .pong-app #pauseMenu { + min-width: 280px; + padding: 15px; + } + + .pong-app .btn-lg { + font-size: 0.9rem; + padding: 0.6rem 1.2rem; + } + + .pong-app h1 { + font-size: 1.8rem; + } + + .pong-app h2 { + font-size: 1.4rem; + } + + .pong-app .score-display { + gap: 50px; + font-size: 1.8rem; + } +} + +/* Stats panel positioning */ +.pong-app #stats { + position: absolute; + top: 0; + left: 0; + z-index: 100; +} diff --git a/Front-End/public/pong/locale/pong.js b/Front-End/public/pong/locale/pong.js new file mode 100644 index 00000000..7c970f28 --- /dev/null +++ b/Front-End/public/pong/locale/pong.js @@ -0,0 +1,352 @@ +import * as THREE from "three"; +import { FontLoader } from "three/addons/loaders/FontLoader.js"; +import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; +import { Group, remove } from "three/addons/libs/tween.module.js"; +import { state } from "./state.js"; +import * as UTILS from "./utils.js"; +import * as SETUP from "./setup.js"; +import * as SETTINGS from "./settings.js"; +import * as GAME from "./gameLogic.js"; +import Stats from "three/addons/libs/stats.module.js"; + +export function renderPong() { + //add bootstrap css + const bootstrapCSS = document.createElement("link"); + bootstrapCSS.rel = "stylesheet"; + bootstrapCSS.href = + "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"; + document.head.appendChild(bootstrapCSS); + + //add bootstrap js + const bootstrapJS = document.createElement("script"); + bootstrapJS.src = + "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"; + document.head.appendChild(bootstrapJS); + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "/pong/locale/pong.css"; + document.head.appendChild(link); + + // Inside renderPong function + + const contentDiv = document.getElementById("content"); + contentDiv.innerHTML = ` +
+
+
+ + + + + + + + + + + + + +
+
+ `; + + document.addEventListener("keydown", pongKeyDownHandler); + document.addEventListener("keyup", pongKeyUpHandler); + + window.pongKeyDownHandler = pongKeyDownHandler; + window.pongKeyUpHandler = pongKeyUpHandler; + + document + .getElementById("newGameButton") + .addEventListener("click", SETTINGS.shownbrOfPlayerMenu); + document + .getElementById("settingsButton") + .addEventListener("click", SETTINGS.showSettingsMenu); + document + .getElementById("exitButton") + .addEventListener("click", SETTINGS.exitGame); + document + .getElementById("onePlayerButton") + .addEventListener("click", SETTINGS.startOnePlayerGame); + document + .getElementById("twoPlayerButton") + .addEventListener("click", SETTINGS.startTwoPlayerGame); + document + .getElementById("backButton") + .addEventListener("click", SETTINGS.showMainMenu); + + document + .getElementById("saveSettingsButton") + .addEventListener("click", SETTINGS.saveSettings); + document + .getElementById("resetSettingsButton") + .addEventListener("click", SETTINGS.resetSettings); + document + .getElementById("backFromSettingsButton") + .addEventListener("click", SETTINGS.showMainMenu); + document + .getElementById("resumeButton") + .addEventListener("click", SETTINGS.resumeGame); + document + .getElementById("exitButtonPause") + .addEventListener("click", SETTINGS.exitGame); + document + .getElementById("player1Color") + .addEventListener("input", (event) => { + state.mat.p1.color.set(event.target.value); + }); + document + .getElementById("player2Color") + .addEventListener("input", (event) => { + state.mat.p2.color.set(event.target.value); + }); + document.getElementById("ballColor").addEventListener("input", (event) => { + state.mat.ball.color.set(event.target.value); + }); + + document.getElementById("ringColor").addEventListener("input", (event) => { + state.mat.ring.color.set(event.target.value); + }); + + document.getElementById("planeColor").addEventListener("input", (event) => { + state.mat.plane.color.set(event.target.value); + }); + + document.getElementById("showStats").addEventListener("change", (event) => { + UTILS.toggleStats(event.target.checked); + }); + + document + .getElementById("restartGameButton") + .addEventListener("click", SETTINGS.restartGame); + + document + .getElementById("mainMenuButton") + .addEventListener("click", SETTINGS.restartMenu); + + document.getElementById("menu").style.display = "block"; + + setTimeout(() => { + SETUP.setupGame(); + GAME.animate(); + }, 100); +} + +//Resize handler + +window.addEventListener("resize", () => { + const container = document.getElementById("threejs-container"); + if (container && state.camera && state.renderer) { + const rect = container.getBoundingClientRect(); + const width = rect.width; + const height = rect.height; + + state.camera.aspect = width / height; + state.camera.updateProjectionMatrix(); + state.renderer.setSize(width, height); + } +}); + + +function pongKeyDownHandler(event) { + if (!event || !event.key) return; + if (event.key.toLowerCase() == "s") { + state.p1_move_y = state.player_speed; + state.keys.s = true; + } + if (event.key.toLowerCase() == "w") { + state.p1_move_y = -state.player_speed; + state.keys.w = true; + } + if (event.key == "ArrowDown" && !state.IAisActive) { + state.p2_move_y = state.player_speed; + state.keys.ArrowDown = true; + } + if (event.key == "ArrowUp" && !state.IAisActive) { + state.p2_move_y = -state.player_speed; + state.keys.ArrowUp = true; + } + if (event.key == "Escape" && state.isStarted) { + if (state.isPaused) { + SETTINGS.resumeGame(); + } else { + state.isPaused = true; + SETTINGS.showPauseMenu(); + } + } +} + +function pongKeyUpHandler(event) { + if (!event || !event.key) return; + if (event.key.toLowerCase() == "s") { + state.keys.s = false; + if (state.keys.w) { + state.p1_move_y = -state.player_speed; + } else { + state.p1_move_y = 0; + } + } + if (event.key.toLowerCase() == "w") { + state.keys.w = false; + if (state.keys.s) { + state.p1_move_y = state.player_speed; + } else { + state.p1_move_y = 0; + } + } + if (event.key == "ArrowDown" && !state.IAisActive) { + state.keys.ArrowDown = false; + if (state.keys.ArrowUp) { + state.p2_move_y = -state.player_speed; + } else { + state.p2_move_y = 0; + } + } + if (event.key == "ArrowUp" && !state.IAisActive) { + state.keys.ArrowUp = false; + if (state.keys.ArrowDown) { + state.p2_move_y = state.player_speed; + } else { + state.p2_move_y = 0; + } + } +} + +// Keyboard setup +// document.addEventListener("keydown", function (event) { +// if (event.key.toLowerCase() == "s") { +// state.p1_move_y = state.player_speed; +// state.keys.s = true; +// } +// if (event.key.toLowerCase() == "w") { +// state.p1_move_y = -state.player_speed; +// state.keys.w = true; +// } + // if (event.key == "ArrowDown" && !state.IAisActive) { + // state.p2_move_y = state.player_speed; + // state.keys.ArrowDown = true; + // } + // if (event.key == "ArrowUp" && !state.IAisActive) { + // state.p2_move_y = -state.player_speed; + // state.keys.ArrowUp = true; + // } +// if (event.key == "Escape" && state.isStarted) { +// if (state.isPaused) { +// SETTINGS.resumeGame(); +// } else { +// state.isPaused = true; +// SETTINGS.showPauseMenu(); +// } +// } +// }); + +// document.addEventListener("keyup", function (event) { +// if (event.key.toLowerCase() == "s") { +// state.keys.s = false; +// if (state.keys.w) { +// state.p1_move_y = -state.player_speed; +// } else { +// state.p1_move_y = 0; +// } +// } +// if (event.key.toLowerCase() == "w") { +// state.keys.w = false; +// if (state.keys.s) { +// state.p1_move_y = state.player_speed; +// } else { +// state.p1_move_y = 0; +// } +// } +// if (event.key == "ArrowDown" && !state.IAisActive) { +// state.keys.ArrowDown = false; +// if (state.keys.ArrowUp) { +// state.p2_move_y = -state.player_speed; +// } else { +// state.p2_move_y = 0; +// } +// } +// if (event.key == "ArrowUp" && !state.IAisActive) { +// state.keys.ArrowUp = false; +// if (state.keys.ArrowDown) { +// state.p2_move_y = state.player_speed; +// } else { +// state.p2_move_y = 0; +// } +// } +// }); diff --git a/Front-End/public/pong/locale/settings.js b/Front-End/public/pong/locale/settings.js new file mode 100644 index 00000000..39506ee7 --- /dev/null +++ b/Front-End/public/pong/locale/settings.js @@ -0,0 +1,303 @@ +import * as THREE from "three"; +import { state } from "./state.js"; +import { updateScore } from "./src/Score.js"; + +export { + showMainMenu, + shownbrOfPlayerMenu, + showSettingsMenu, + showPauseMenu, + hidePauseMenu, + startOnePlayerGame, + startTwoPlayerGame, + resumeGame, + exitGame, + saveSettings, + resetSettings, + showGameOverMenu, + restartGame, + restartMenu, +}; + +function saveSettings() { + const player1Color = document.getElementById("player1Color").value; + const player2Color = document.getElementById("player2Color").value; + const ballColor = document.getElementById("ballColor").value; + const ringColor = document.getElementById("ringColor").value; + const planeColor = document.getElementById("planeColor").value; + + state.mat.p1.color.set(player1Color); + state.mat.p2.color.set(player2Color); + state.mat.ball.color.set(ballColor); + state.mat.ring.color.set(ringColor); + state.mat.plane.color.set(planeColor); + + showMainMenu(); +} + +function resetSettings() { + document.getElementById("player1Color").value = "#4deeea"; + document.getElementById("player2Color").value = "#4deeea"; + document.getElementById("ballColor").value = "#8c5fb3"; + document.getElementById("ringColor").value = "#ffe700"; + document.getElementById("planeColor").value = "#089c00"; + + state.mat.p1.color.set("#4deeea"); + state.mat.p2.color.set("#4deeea"); + state.mat.ball.color.set("#8c5fb3"); + state.mat.ring.color.set("#ffe700"); + state.mat.plane.color.set("#089c00"); +} + +function shownbrOfPlayerMenu() { + document.getElementById("menu").style.display = "none"; + document.getElementById("nbrOfPlayerMenu").style.display = "block"; +} + +function showMainMenu() { + document.getElementById("nbrOfPlayerMenu").style.display = "none"; + document.getElementById("settingsMenu").style.display = "none"; + document.getElementById("pauseMenu").style.display = "none"; + document.getElementById("menu").style.display = "block"; +} + +function showSettingsMenu() { + document.getElementById("menu").style.display = "none"; + document.getElementById("settingsMenu").style.display = "block"; +} + +function showPauseMenu() { + document.getElementById("pauseMenu").style.display = "block"; +} + +function hidePauseMenu() { + document.getElementById("pauseMenu").style.display = "none"; +} + +function startOnePlayerGame() { + document.getElementById("nbrOfPlayerMenu").style.display = "none"; + state.IAisActive = true; + state.isStarted = true; + state.isPaused = false; + + // Initialize mobile controls if on touch device + if (typeof window.initializeMobileControls === 'function') { + window.initializeMobileControls(); + } +} + +function startTwoPlayerGame() { + document.getElementById("nbrOfPlayerMenu").style.display = "none"; + state.IAisActive = false; + state.isStarted = true; + state.isPaused = false; + + // Initialize mobile controls if on touch device + if (typeof window.initializeMobileControls === 'function') { + window.initializeMobileControls(); + } +} + +function resumeGame() { + hidePauseMenu(); + state.isPaused = false; + // GAME.animate(); +} + +export function cleanupPong() { + console.log("Starting complete cleanup..."); + + // Remove mobile controls first + if (typeof window.removeMobileControls === 'function') { + window.removeMobileControls(); + } + + resetPongMenuState(); + + // 1. Dispose all meshes in the game group + if (state.game) { + console.log( + `Cleaning game group with ${state.game.children.length} objects` + ); + while (state.game.children.length > 0) { + const obj = state.game.children[0]; + state.game.remove(obj); + + if (obj.geometry) obj.geometry.dispose(); + + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach((mat) => mat.dispose()); + } else { + obj.material.dispose(); + } + } + } + } + + // 2. Dispose all scene objects + if (state.scene) { + console.log( + `Cleaning scene with ${state.scene.children.length} objects` + ); + while (state.scene.children.length > 0) { + const obj = state.scene.children[0]; + state.scene.remove(obj); + + // Skip disposing game since we already handled it + if (obj === state.game) continue; + + // Dispose if it's a mesh + if (obj.isMesh) { + if (obj.geometry) obj.geometry.dispose(); + + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach((mat) => mat.dispose()); + } else { + obj.material.dispose(); + } + } + } + } + } + + // 3. Remove renderer from DOM + if (state.renderer) { + const container = document.getElementById("threejs-container"); + if ( + container && + state.renderer.domElement && + container.contains(state.renderer.domElement) + ) { + container.removeChild(state.renderer.domElement); + } + state.renderer.dispose(); + state.renderer = null; + } + + // 4. Remove event listeners + window.removeEventListener("resize", state.onWindowResize); + + if (window.pongKeydownHandler) { + document.removeEventListener("keydown", window.pongKeydownHandler); + window.pongKeydownHandler = null; + } + if (window.pongKeyupHandler) { + document.removeEventListener("keyup", window.pongKeyupHandler); + window.pongKeyupHandler = null; + } + + // 5. Cancel animation frame if active + if (state.animationFrameId) { + cancelAnimationFrame(state.animationFrameId); + state.animationFrameId = null; + } + + // 6. Reset state variables + state.ball = null; + state.players = []; + state.p1_score = 0; + state.p2_score = 0; + state.isStarted = false; + state.game = new THREE.Group(); // Create fresh game group + + console.log("Cleanup complete"); +} + +function exitGame() { + state.isStarted = false; + state.isPaused = true; + state.IAisActive = false; + + // Hide mobile controls + if (typeof window.hideMobileControls === 'function') { + window.hideMobileControls(); + } + + cleanupPong(); + + // Uncomment this to navigate to home + window.navigateTo("#home"); + + // Or use this to show main menu + // showMainMenu(); +} + +function showGameOverMenu(winner) { + // Hide other menus + if (!state.IsMultiplayer){ + document.getElementById("menu").style.display = "none"; + document.getElementById("pauseMenu").style.display = "none"; + document.getElementById("settingsMenu").style.display = "none"; + document.getElementById("nbrOfPlayerMenu").style.display = "none"; + } + + // Update winner text + document.getElementById( + "winnerAnnouncement" + ).textContent = `${winner} Wins!`; + + // Show game over menu + document.getElementById("gameOverMenu").style.display = "block"; +} + +// Add this function to restart the game with current settings +function resetGame() { + // Hide game over menu + document.getElementById("gameOverMenu").style.display = "none"; + + // Reset scores and positions but keep other settings + state.p1_score = 0; + state.p2_score = 0; + + updateScore("p1"); + updateScore("p2"); + + if (state.players[0] && state.players[0].mesh) { + state.players[0].mesh.position.set( + -((state.ring.length * 2) / 5), + 0, + 0 + ); + } + + if (state.players[1] && state.players[1].mesh) { + state.players[1].mesh.position.set((state.ring.length * 2) / 5, 0, 0); + } + + if (state.ball && state.ball.mesh) { + state.ball.mesh.position.set(0, 0, 0); + state.ball.resetSpeed(); + } + + // Start the game with current settings +} + +function resetPongMenuState() { + // Reset solo le variabili di stato, non il DOM + state.p1_score = 0; + state.p2_score = 0; + state.isStarted = false; + state.isPaused = true; + state.IAisActive = false; + + // DOM reset solo se siamo sicuri che esista + if (document.getElementById("menu")) { + showMainMenu(); + } +} + +function restartMenu() { + // Hide game over menu + document.getElementById("gameOverMenu").style.display = "none"; + // Show main menu + showMainMenu(); + resetGame(); +} + +function restartGame() { + resetGame(); + state.isStarted = true; + state.isPaused = false; +} diff --git a/Front-End/public/pong/locale/setup.js b/Front-End/public/pong/locale/setup.js new file mode 100644 index 00000000..1adcd606 --- /dev/null +++ b/Front-End/public/pong/locale/setup.js @@ -0,0 +1,144 @@ +import * as THREE from "three"; +import { state } from "./state.js"; +// import { createScore, updateScore } from "./utils.js"; +import Stats from "three/addons/libs/stats.module.js"; +import { OrbitControls } from "three/addons/controls/OrbitControls.js"; +import Ball from "./src/Ball.js"; +import Player from "./src/Player.js"; +import { setupRing } from "./src/Ring.js"; +import { createScore, updateScore } from "./src/Score.js"; +import { initLights } from "./src/Light.js"; +import { game_over } from "./utils.js"; + +//Scene setup + +export function setupGame() { + state.scene = new THREE.Scene(); + state.scene.background = new THREE.Color(0x222233); + + state.camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + state.camera.position.set(state.cam.x, state.cam.y, state.cam.z); + + // Clear existing players + state.players.forEach((player) => { + if (player && player.mesh) { + state.scene.remove(player.mesh); + } + }); + state.players = []; + + // Create renderer + state.renderer = new THREE.WebGLRenderer({ + antialias: window.devicePixelRatio < 2, + logarithmicDepthBuffer: true, + powerPreference: "high-performance", + //quality: "high", + failIfMajorPerformanceCaveat: false, + }); + + // Get the container + const container = document.getElementById("threejs-container"); + if (container) { + // Use container dimensions + const rect = container.getBoundingClientRect(); + const width = rect.width; + const height = rect.height; + + // Set renderer size based on container + state.renderer.setSize(width, height); + + // Update camera aspect ratio + state.camera.aspect = width / height; + state.camera.updateProjectionMatrix(); + + // Attach renderer to container, not body + container.appendChild(state.renderer.domElement); + } else { + console.error("Three.js container not found"); + // Only as fallback + state.renderer.setSize(window.innerWidth, window.innerHeight); + } + + state.controls = new OrbitControls(state.camera, state.renderer.domElement); + + // state.stats = new Stats(); + // document.body.appendChild(state.stats.dom); + + setupRing(); + + new Player(new THREE.Vector3(-((state.ring.length * 2) / 5), 0, 0), 0); + + new Player(new THREE.Vector3((state.ring.length * 2) / 5, 0, 0), 1); + // //Ball setup + + console.log("Ball radius:", state.ball_radius); + + new Ball(state.scene, state.ball_radius, state.boundaries, [ + state.players[0], + state.players[1], + ]); + + // Score + + createScore(); + + if (state.isMultiplayer) { + // For multiplayer, prevent local game state changes + state.ball.addEventListener("score", (event) => { + console.log("Score event in multiplayer mode:", event); + if (event.message === "p1") { + state.p1_score++; + } else if (event.message === "p2") { + state.p2_score++; + } + updateScore(event.message); + // Don't call game_over() or change game state + }); + } else { + // Original single-player scoring logic + state.ball.addEventListener("score", (event) => { + console.log("Score event:", event); + if (event.message === "p1") { + state.p1_score++; + } else if (event.message === "p2") { + state.p2_score++; + } + updateScore(event.message); + if ( + state.p1_score >= state.maxScore || + state.p2_score >= state.maxScore + ) { + state.isStarted = false; + state.isPaused = true; + if (!state.isMultiplayer) + game_over(); + } + }); + } + + // //Game setup + + state.game.rotateOnAxis(new THREE.Vector3(1, 0, 0), Math.PI / 3); + state.scene.add(state.game); + + //Movement setup + + state.P1cursor = new THREE.Vector2(); + state.P2cursor = new THREE.Vector2(); + + initLights(); + + state.renderer.shadowMap.enabled = true; + state.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + state.renderer.render(state.scene, state.camera); + + console.log("Game setup complete"); + console.log("Scene contains:", state.scene.children.length, "objects"); + console.log("Camera position:", state.camera.position); +} diff --git a/Front-End/public/pong/locale/src/Ball.js b/Front-End/public/pong/locale/src/Ball.js new file mode 100644 index 00000000..f1edf718 --- /dev/null +++ b/Front-End/public/pong/locale/src/Ball.js @@ -0,0 +1,174 @@ +import * as THREE from "three"; +import { state } from "../state.js"; +export default class Ball extends THREE.EventDispatcher { + speed = 25; + velocity = new THREE.Vector3(1, 0, 0); + + constructor(scene, radius, boundaries, players) { + super(); + this.scene = scene; + this.players = players; + this.radius = radius; + this.boundaries = boundaries; + + this.geometry = new THREE.SphereGeometry(this.radius); + this.material = state.mat.ball; + this.mesh = new THREE.Mesh(this.geometry, this.material); + + this.velocity.multiplyScalar(this.speed); + + this.raycaster = new THREE.Raycaster(); + this.raycaster.near = 0; + this.raycaster.far = this.boundaries.y * 2.5; + + this.mesh.castShadow = true; + // this.pointCollision = new THREE.Mesh( + // new THREE.SphereGeometry(1), + // new THREE.MeshBasicMaterial({ color: 0xff0000 }) + // ); + // state.game.add(this.pointCollision); + state.game.add(this.mesh); + state.ball = this; + } + + resetSpeed() { + this.speed = 25; + this.velocity.normalize().multiplyScalar(this.speed); + } + + update(dt) { + // Skip local physics if disabled for multiplayer + if (this.disableLocalPhysics) { + return; + } + + // Calculate number of sub-steps based on ball speed + const substeps = Math.max(1, Math.ceil(this.speed / 15)); + const subDt = dt / substeps; + + // Execute multiple smaller steps + for (let i = 0; i < substeps; i++) { + this.updateStep(subDt); + } + } + + updateStep(dt) { + const s = this.velocity.clone().multiplyScalar(dt); + const tPos = this.mesh.position.clone().add(s); + + this.dir = this.velocity.clone().normalize(); + this.raycaster.set(this.mesh.position, this.dir); + + // Debug ball parameters + // Regular boundary collision code... + const dx = + this.boundaries.x - this.radius - Math.abs(this.mesh.position.x); + const dz = + this.boundaries.y - this.radius - Math.abs(this.mesh.position.z); + if (dx <= 0) { + const message = this.mesh.position.x > 0 ? "p1" : "p2"; + this.dispatchEvent({ type: "score", message: message }); + tPos.set(0, 0, 0); + this.resetSpeed(); + this.velocity.x *= -1; + } + if (dz <= 0) { + this.velocity.z *= -1; + tPos.z = + (this.boundaries.y - this.radius + dz) * + Math.sign(this.mesh.position.z); + } + + // Debug target selection + const playerIndex = this.velocity.x > 0 ? 1 : 0; + // console.log("Selected player index:", playerIndex); + // console.log("Players array:", state.players); + + const target = state.players[playerIndex]; + // console.log("Target player:", target); + + // Check collision with helper mesh + if (target && target.mesh) { + const helperMesh = target.mesh.children[0]; + + // Get paddle dimensions + const paddleWidth = state.p.width + state.ball_radius; + const paddleHeight = state.p.height + state.ball_radius; + const paddleHalfWidth = paddleWidth / 2; + const paddleHalfHeight = paddleHeight / 2; + + // Calculate relative position of ball to paddle + const relativePos = this.mesh.position + .clone() + .sub(target.mesh.position); + + // Inner face X position (facing the playing field) + const innerFaceX = + playerIndex === 0 ? paddleHalfWidth : -paddleHalfWidth; + // Bottom edge Z position + const bottomEdgeZ = -paddleHalfHeight; + + // Distance from inner face and bottom edge + const distFromInnerFace = Math.abs(relativePos.x - innerFaceX); + const distFromBottom = Math.abs(relativePos.z - bottomEdgeZ); + + // Check if ball is in front of the inner face or above/below the bottom edge + const isInFrontOfInnerFace = + (playerIndex === 0 && relativePos.x > innerFaceX) || + (playerIndex === 1 && relativePos.x < innerFaceX); + const isNearBottom = + Math.abs(relativePos.z - bottomEdgeZ) < this.radius; + + // Check if the ball is within the paddle's height range (for inner face collision) + const isWithinPaddleHeight = + Math.abs(relativePos.z) <= paddleHalfHeight; + + // Check if the ball is within the paddle's width range (for bottom edge collision) + const isWithinPaddleWidth = + Math.abs(relativePos.x) <= paddleHalfWidth; + + // Collision with inner face + if ( + isInFrontOfInnerFace && + isWithinPaddleHeight && + distFromInnerFace <= this.radius + ) { + console.log("COLLISION DETECTED on inner face!"); + + // Visual feedback + const collisionPoint = target.mesh.position.clone(); + collisionPoint.x += innerFaceX; + collisionPoint.z += relativePos.z; + // this.pointCollision.position.copy(collisionPoint); + + // Calculate hit position relative to paddle center (range: -1 to 1) + const hitPosition = relativePos.z / paddleHalfHeight; + + // Bounce angle varies based on hit position (center = straight, edges = angled) + // Max angle is about 75 degrees (1.3 radians) + const bounceAngle = hitPosition * 1.3; + + // Calculate new velocity components + const speed = this.speed; + + // FIX: Reverse X direction based on which player was hit + // If player 0 (left), ball should go right (positive X) + // If player 1 (right), ball should go left (negative X) + const xDir = playerIndex === 0 ? 1 : -1; + + // Set new velocity based on bounce angle + this.velocity.x = xDir * Math.cos(bounceAngle) * speed; + this.velocity.z = Math.sin(bounceAngle) * speed; + + // Slightly increase speed with each hit to prevent endless rallies + this.speed *= 1.1; + console.log("New speed after hit:", this.speed); + // this.speed = Math.min(this.speed, 90); // Cap max speed + } + } else { + console.warn("Target or target.mesh not found"); + } + + this.mesh.position.copy(tPos); + } +} diff --git a/Front-End/public/pong/locale/src/Light.js b/Front-End/public/pong/locale/src/Light.js new file mode 100644 index 00000000..46369e5f --- /dev/null +++ b/Front-End/public/pong/locale/src/Light.js @@ -0,0 +1,54 @@ +import * as THREE from "three"; +import { state } from "../state.js"; + +export function initLights() { + // Remove any previous lights + if (state.lights && Array.isArray(state.lights)) { + state.lights.forEach((light) => { + if (light && light.parent) light.parent.remove(light); + }); + } + state.lights = []; + + // Ambient light for base visibility + const ambientLight = new THREE.AmbientLight(0xffffff, 1); + state.scene.add(ambientLight); + state.lights.push(ambientLight); + + // Directional light that will point to the ball + const dirLight = new THREE.DirectionalLight(0xffffff, 2.0); + dirLight.position.set(0, 50, 50); + dirLight.castShadow = true; + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + dirLight.shadow.camera.left = -100; + dirLight.shadow.camera.right = 100; + dirLight.shadow.camera.top = 100; + dirLight.shadow.camera.bottom = -100; + dirLight.shadow.camera.near = 1; + dirLight.shadow.camera.far = 200; + + const cameraHelper = new THREE.CameraHelper(dirLight.shadow.camera); + // state.scene.add(cameraHelper); + // state.lights.push(cameraHelper); + + // Create a target object for the light + const lightTarget = new THREE.Object3D(); + state.scene.add(lightTarget); + dirLight.target = lightTarget; + state.lightTarget = lightTarget; + + state.scene.add(dirLight); + state.lights.push(dirLight); + + // Set the light's shadow properties + const dirLightHelper = new THREE.DirectionalLightHelper( + dirLight, + 10, + 0xffaa00 + ); + // state.scene.add(dirLightHelper); + // state.lights.push(dirLightHelper); + + // Store the target for updates +} diff --git a/Front-End/public/pong/locale/src/Player.js b/Front-End/public/pong/locale/src/Player.js new file mode 100644 index 00000000..950115ef --- /dev/null +++ b/Front-End/public/pong/locale/src/Player.js @@ -0,0 +1,62 @@ +import * as THREE from "three"; +import { state } from "../state.js"; + +const GEOMETRY = new THREE.BoxGeometry( + state.p.width, + state.p.height, + state.p.depth +); +const HELPER_GEOMETRY = new THREE.BoxGeometry( + state.p.width + state.ball_radius, + state.p.height + state.ball_radius, + state.p.depth + state.ball_radius +); +GEOMETRY.rotateX(Math.PI / 2); +HELPER_GEOMETRY.rotateX(Math.PI / 2); +const MATERIAL = [state.mat.p1, state.mat.p2]; + +const HELPER_MATERIAL = state.mat.helper; + +export default class Player { + constructor(position, n) { + this.scene = state.scene; + this.geometry = GEOMETRY; + this.material = MATERIAL; + this.mesh = new THREE.Mesh(GEOMETRY, MATERIAL[n]); + + this.helperMesh = new THREE.Mesh(HELPER_GEOMETRY, HELPER_MATERIAL); + + this.mesh.add(this.helperMesh); + this.mesh.position.copy(position); + + this.mesh.castShadow = true; + + state.game.add(this.mesh); + state.players.push(this); + } + + checkBoundaries(moveY) { + const nextPosition = this.mesh.position.z + moveY; + const halfHeight = state.p.height / 2; + const topBoundary = state.boundaries.y - halfHeight; + const bottomBoundary = -state.boundaries.y + halfHeight; + + if (nextPosition <= bottomBoundary) { + this.mesh.position.z = bottomBoundary; + return false; + } + if (nextPosition >= topBoundary) { + this.mesh.position.z = topBoundary; + return false; + } + + return true; + } + + // Add movement method + move(moveY) { + if (this.checkBoundaries(moveY)) { + this.mesh.position.z += moveY; + } + } +} diff --git a/Front-End/public/pong/locale/src/Ring.js b/Front-End/public/pong/locale/src/Ring.js new file mode 100644 index 00000000..b0bf1c0f --- /dev/null +++ b/Front-End/public/pong/locale/src/Ring.js @@ -0,0 +1,87 @@ +// Ring setup +import * as THREE from "three"; +import { state } from "../state.js"; + +export function setupRing() { + state.boundaries = new THREE.Vector2( + state.ring.length / 2, + state.ring.height / 2 + ); + const planeGeometry = new THREE.PlaneGeometry( + state.boundaries.x * 2, + state.boundaries.y * 2 + ); + planeGeometry.rotateX(-Math.PI / 2); + state.plane = new THREE.Mesh(planeGeometry, state.mat.plane); + state.plane.position.y = -state.ring.thickness - 0.1; + state.plane.receiveShadow = true; + state.game.add(state.plane); + + state.r_bottom = new THREE.Mesh( + new THREE.BoxGeometry( + state.ring.length + state.ring.thickness * 2, + state.ring.thickness, + state.ring.depth + ), + state.mat.ring + ); + state.r_top = new THREE.Mesh( + new THREE.BoxGeometry( + state.ring.length + state.ring.thickness * 2, + state.ring.thickness, + state.ring.depth + ), + state.mat.ring + ); + state.r_left = new THREE.Mesh( + new THREE.BoxGeometry( + state.ring.thickness, + state.ring.height, + state.ring.depth + ), + state.mat.ring + ); + state.r_right = new THREE.Mesh( + new THREE.BoxGeometry( + state.ring.thickness, + state.ring.height, + state.ring.depth + ), + state.mat.ring + ); + + // state.r_bottom.castShadow = true; + // state.r_top.castShadow = true; + // state.r_left.castShadow = true; + // state.r_right.castShadow = true; + state.r_bottom.receiveShadow = true; + state.r_top.receiveShadow = true; + state.r_left.receiveShadow = true; + state.r_right.receiveShadow = true; + + state.r_bottom.position.set( + 0, + -((state.ring.height + state.ring.thickness) / 2), + 0 + ); + state.r_top.position.set( + 0, + (state.ring.height + state.ring.thickness) / 2, + 0 + ); + state.r_left.position.set( + -((state.ring.length + state.ring.thickness) / 2), + 0, + 0 + ); + state.r_right.position.set( + (state.ring.length + state.ring.thickness) / 2, + 0, + 0 + ); + state.ring3D = new THREE.Group(); + state.ring3D.add(state.r_bottom, state.r_top, state.r_left, state.r_right); + + state.ring3D.rotateX(-Math.PI / 2); + state.game.add(state.ring3D); +} diff --git a/Front-End/public/pong/locale/src/Score.js b/Front-End/public/pong/locale/src/Score.js new file mode 100644 index 00000000..4f867ebf --- /dev/null +++ b/Front-End/public/pong/locale/src/Score.js @@ -0,0 +1,81 @@ +import * as THREE from "three"; +import { state } from "../state.js"; +import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; +import { FontLoader } from "three/addons/loaders/FontLoader.js"; + +export function createScore() { + const fontLoader = new FontLoader(); + const fontUrl = + "https://threejs.org/examples/fonts/helvetiker_regular.typeface.json"; + fontLoader.load(fontUrl, function (font) { + const textGeometry = new TextGeometry("0", { + font: font, + size: 20, + depth: 0.5, + curveSegments: 12, + bevelEnabled: false, + bevelThickness: 0.1, + bevelSize: 0.1, + bevelOffset: 0, + bevelSegments: 5, + }); + textGeometry.center(); + + state.scoreMesh.p1 = new THREE.Mesh(textGeometry, state.mat.score); + state.scoreMesh.p2 = new THREE.Mesh(textGeometry, state.mat.score); + state.scoreMesh.p1.position.x = -state.boundaries.x * 0.6; + state.scoreMesh.p2.position.x = state.boundaries.x * 0.6; + state.scoreMesh.p1.position.y = state.boundaries.y * 1.3; + state.scoreMesh.p2.position.y = state.boundaries.y * 1.3; + // state.scoreMesh.p1.rotateX(-Math.PI / 2); + // state.scoreMesh.p2.rotateX(-Math.PI / 2); + // state.game.add(state.scoreMesh.p1, state.scoreMesh.p2); + state.scene.add(state.scoreMesh.p1, state.scoreMesh.p2); + }); +} + +export function updateScore(player) { + const scoreValue = player === "p1" ? state.p1_score : state.p2_score; + const scoreMesh = player === "p1" ? state.scoreMesh.p1 : state.scoreMesh.p2; + + // TODO: Implement mobile score display update functionality if required. + + // Don't proceed if the score mesh isn't created yet + if (!scoreMesh) { + console.warn("Score mesh not initialized yet"); + return; + } + + const fontLoader = new FontLoader(); + const fontUrl = + "https://threejs.org/examples/fonts/helvetiker_regular.typeface.json"; + + fontLoader.load(fontUrl, function (font) { + // Create new text geometry with current score + const textGeometry = new TextGeometry(scoreValue.toString(), { + font: font, + size: 20, + depth: 0.5, + curveSegments: 12, + bevelEnabled: false, + bevelThickness: 0.1, + bevelSize: 0.1, + bevelOffset: 0, + bevelSegments: 5, + }); + + textGeometry.center(); + + // Store the original position and rotation + const originalPosition = scoreMesh.position.clone(); + const originalRotation = scoreMesh.rotation.clone(); + + // Remove old geometry and replace with new one + scoreMesh.geometry.dispose(); + scoreMesh.geometry = textGeometry; + + // Restore original position and rotation + scoreMesh.position.copy(originalPosition); + scoreMesh.rotation.copy(originalRotation); + }); +} diff --git a/Front-End/public/pong/locale/state.js b/Front-End/public/pong/locale/state.js new file mode 100644 index 00000000..b241be6f --- /dev/null +++ b/Front-End/public/pong/locale/state.js @@ -0,0 +1,104 @@ +import * as THREE from "three"; +import Stats from "three/addons/libs/stats.module.js"; + +export let state = { + look: { x: 0, y: 0, z: 0 }, + cam: { x: 0, y: 20, z: 100 }, + ring: { + height: 0, + length: (window.innerHeight * 15) / 100, + depth: 10, + thickness: 3, + }, + p: { height: 0, width: 2.5, depth: 2.5 }, + mat: { + plane: new THREE.MeshPhongMaterial({ color: 0x089c00, shininess: 100 }), + ring: new THREE.MeshPhongMaterial({ color: 0xffe700, shininess: 100 }), + p1: new THREE.MeshPhongMaterial({ + color: 0x4deeea, + shininess: 100, + }), + p2: new THREE.MeshPhongMaterial({ + color: 0x4deeea, + shininess: 100, + }), + helper: new THREE.MeshPhongMaterial({ + color: 0xff2b2b, + transparent: true, + opacity: 0.5, + visible: false, + }), + ball: new THREE.MeshPhongMaterial({ + color: 0x8c5fb3, + shininess: 100, + }), + score: new THREE.MeshPhongMaterial({ color: 0xff2b2b, shininess: 100 }), + }, + P1cursor: null, + P2cursor: null, + isPaused: true, + isStarted: false, + IAisActive: false, + wallHitPosition: 0, + player_speed: 0, + p1_score: 0, + p2_score: 0, + p1_move_y: 0, + p2_move_y: 0, + ball_radius: 0, + ball_speed: 0, + angle: 0, + hit_position: 0, + powerUps: [], + powerUpInterval: 10000, + spawnPowerUpFlag: false, + lastPowerUpSpawnTime: 0, + scene: null, + camera: null, + renderer: null, + boundaries: null, + lightTarget: null, + p1: null, + p2: null, + players: [], + ball: null, + ring3D: null, + ground: null, + stats: null, + game: new THREE.Group(), + scoreText: null, + winnerText: null, + r_bottom: null, + r_top: null, + r_left: null, + r_right: null, + keys: { + w: false, + s: false, + ArrowUp: false, + ArrowDown: false, + }, + controls: null, + animationFrameId: null, + score: { + p1: 0, + p2: 0, + }, + scoreMesh: { + p1: null, + p2: null, + }, + lights: [], + plane: null, + maxScore: 1, +}; + +state.ring.height = (9 / 16) * state.ring.length; +state.p.height = state.ring.height / 6; +state.player_speed = state.ring.length / 80; +state.ball_radius = state.ring.length / 80; +state.ball_speed = state.ring.length / 150; +state.angle = Math.floor(Math.random() * 70); +if (state.angle % 2) state.angle *= -1; +if (state.angle % 3) state.angle += 180; +state.stats = new Stats(); diff --git a/Front-End/public/pong/locale/utils.js b/Front-End/public/pong/locale/utils.js new file mode 100644 index 00000000..2e86a3bd --- /dev/null +++ b/Front-End/public/pong/locale/utils.js @@ -0,0 +1,13 @@ +import * as THREE from "three"; +import { state } from "./state.js"; +import * as SETTINGS from "./settings.js"; + +export function game_over() { + state.isStarted = false; + state.isPaused = true; + + const winner = state.p1_score >= state.maxScore ? "Player 1" : "Player 2"; + + // Show game over menu instead of main menu + SETTINGS.showGameOverMenu(winner); +} diff --git a/Front-End/public/pong/matchHistory.js b/Front-End/public/pong/matchHistory.js new file mode 100644 index 00000000..cd2a861e --- /dev/null +++ b/Front-End/public/pong/matchHistory.js @@ -0,0 +1,130 @@ +import { getCookie } from "../cookie.js"; +import { getVariables } from "../var.js"; +import { showAlertForXSeconds } from "../alert/alert.js"; + +const pongContainerCSS = document.createElement("link"); +pongContainerCSS.rel = "stylesheet"; +pongContainerCSS.href = "/pongContainer/pongContainer.css"; +document.head.appendChild(pongContainerCSS); + +export function renderMatchHistory() { + console.warn("/***********matchHistoryContainer************/"); + + const matchHistoryContainer = document.getElementById("matchHistoryContainer"); + + matchHistoryContainer.innerHTML = ` +
+
+
Match History
+
+
+
+ +
+
+
+ `; + + matchHistory(); +} + +async function matchHistory(page = 1) { + let matchHistory = []; + const { token, url_api, userId } = getVariables(); + const pageSize = 5; + try { + console.log("[PongHistory] Fetching game history for user_id:", userId, "page:", page); + const response = await fetch( + `${url_api}/pong/games/history?user_id=${userId}&page_size=${pageSize}&page=${page}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + } + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + console.log("[PongHistory] API response:", data); + matchHistory = data.results; + + const matchHistoryList = document.getElementById("matchHistoryList"); + if (!matchHistoryList) { + console.error("[PongHistory] Match history list not found"); + return; + } + matchHistoryList.innerHTML = ""; // Clear existing items + if (matchHistory.length === 0) { + matchHistoryList.innerHTML = `

No match history available.

`; + } + + for (const match of matchHistory) { + const winner = match.winner == null ? "not finished" : match.winner.username || "Unknown"; + const opponent = match.player_1.user_id == userId ? match.player_2 : match.player_1; + const opponentScore = match.player_1.user_id == userId ? match.player_2_score : match.player_1_score; + const myScore = match.player_1.user_id == userId ? match.player_1_score : match.player_2_score; + const scores = `${myScore} - ${opponentScore}`; + const status = match.status || "Unknown"; + const tournament = match.tournament_id == null ? "" : "tournament: " + match.tournament_id; + const winnerColor = match.winner == null ? "text-bg-secondary" : match.winner.user_id == userId ? "text-bg-success" : "text-bg-danger"; + + const matchItem = document.createElement("div"); + matchItem.className = "match-history-item"; + matchItem.innerHTML = ` +
+
+ + ${new Date(match.begin_date).toLocaleDateString()} +
+
winner: ${winner}
+
+
+
Score: ${scores} vs ${opponent.username || "Unknown"}
+
${tournament}
+
+
+ `; + matchHistoryList.appendChild(matchItem); + } + + // PAGINAZIONE SEMPLICE: solo freccia sinistra e destra + // PAGINAZIONE accanto a
Match History
+ const pagination = document.getElementById("matchPagination"); + if (pagination) { + const prevDisabled = !data.previous ? "disabled" : ""; + const nextDisabled = !data.next ? "disabled" : ""; + pagination.innerHTML = ` + + + `; + // Event listeners + document.getElementById("prevPageBtn").onclick = () => { + if (data.previous) { + let prevPage = new URL(data.previous).searchParams.get("page"); + prevPage = prevPage ? Number(prevPage) : 1; + if (prevPage < 1) prevPage = 1; + window.matchHistory(prevPage); + } + }; + document.getElementById("nextPageBtn").onclick = () => { + if (data.next) { + const nextPage = new URL(data.next).searchParams.get("page"); + window.matchHistory(Number(nextPage)); + } + }; + } + } catch (error) { + console.error("[PongHistory] API error:", error); + return; + } +} + +window.matchHistory = matchHistory; \ No newline at end of file diff --git a/Front-End/public/pong/mobile-controls.css b/Front-End/public/pong/mobile-controls.css new file mode 100644 index 00000000..744cc7ed --- /dev/null +++ b/Front-End/public/pong/mobile-controls.css @@ -0,0 +1,173 @@ +/* Mobile Pong Controls */ +.mobile-pong-controls { + position: fixed; + bottom: 20px; + left: 0; + right: 0; + z-index: 1000; + display: none; + justify-content: space-between; + align-items: center; + padding: 0 20px; + pointer-events: none; +} + +.mobile-pong-controls.show { + display: flex; +} + +.mobile-controls-left { + display: flex; + flex-direction: column; + gap: 15px; + pointer-events: auto; +} + +.mobile-controls-center { + pointer-events: auto; +} + +.mobile-controls-right { + width: 60px; /* Reserve space for symmetry */ +} + +.mobile-btn { + width: 60px; + height: 60px; + border-radius: 50%; + border: 3px solid #00ff41; + background: rgba(0, 0, 0, 0.8); + color: #00ff41; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 15px rgba(0, 255, 65, 0.3); + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.mobile-btn:hover, +.mobile-btn:focus { + background: rgba(0, 255, 65, 0.1); + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(0, 255, 65, 0.4); +} + +.mobile-btn:active, +.mobile-btn.active { + background: rgba(0, 255, 65, 0.3); + transform: scale(0.95); + box-shadow: 0 2px 10px rgba(0, 255, 65, 0.6); +} + +.mobile-btn-up i { + margin-bottom: 2px; +} + +.mobile-btn-down i { + margin-top: 2px; +} + +.mobile-score-display { + background: rgba(0, 0, 0, 0.9); + border: 2px solid #00ff41; + border-radius: 25px; + padding: 10px 20px; + color: #00ff41; + font-size: 24px; + font-weight: bold; + font-family: 'Courier New', monospace; + text-align: center; + min-width: 100px; + box-shadow: 0 4px 15px rgba(0, 255, 65, 0.3); +} + +/* Responsive adjustments */ +@media (max-width: 480px) { + .mobile-pong-controls { + padding: 0 15px; + bottom: 15px; + } + + .mobile-btn { + width: 50px; + height: 50px; + font-size: 20px; + } + + .mobile-controls-left, + .mobile-controls-right { + gap: 10px; + } + + .mobile-score-display { + font-size: 20px; + padding: 8px 15px; + min-width: 80px; + } +} + +@media (max-width: 320px) { + .mobile-btn { + width: 45px; + height: 45px; + font-size: 18px; + } + + .mobile-score-display { + font-size: 18px; + padding: 6px 12px; + } +} + +/* Show mobile controls on all touch devices */ +@media (hover: none) and (pointer: coarse) { + .mobile-pong-controls { + display: flex !important; + } +} + +/* Animation for showing/hiding */ +.mobile-pong-controls { + opacity: 0; + transform: translateY(20px); + transition: all 0.3s ease; +} + +.mobile-pong-controls.show { + opacity: 1; + transform: translateY(0); +} + +/* Prevent text selection on mobile controls */ +.mobile-pong-controls * { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Custom button states for better feedback */ +.mobile-btn:before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(0, 255, 65, 0.4); + transform: translate(-50%, -50%); + transition: all 0.3s ease; +} + +.mobile-btn.active:before { + width: 100%; + height: 100%; +} diff --git a/Front-End/public/pong/multiplayer/multiplayerPong.js b/Front-End/public/pong/multiplayer/multiplayerPong.js new file mode 100644 index 00000000..39a22bd2 --- /dev/null +++ b/Front-End/public/pong/multiplayer/multiplayerPong.js @@ -0,0 +1,402 @@ +import { state } from "../locale/state.js"; +import * as SETUP from "../locale/setup.js"; +import * as GAME from "../locale/gameLogic.js"; +import * as SETTINGS from "../locale/settings.js"; +import { + createGame, + sendPlayerReady, + sendPlayerMovement, +} from "./serverSide.js"; +import { getVariables } from "../../var.js"; +import { showNotification } from "../pongContainer.js"; + +export function renderMultiplayerPong( + opponentId, + opponentName, + existingRoomId = null, + tournamentId = null +) { + window.multiplayerGameInfo = { + opponentId, + opponentName, + roomId: existingRoomId, + tournamentId, + }; + + // Import locale pong CSS + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "/pong/locale/pong.css"; + document.head.appendChild(link); + + // Load Bootstrap + const bootstrapCSS = document.createElement("link"); + bootstrapCSS.rel = "stylesheet"; + bootstrapCSS.href = + "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"; + document.head.appendChild(bootstrapCSS); + + const contentDiv = document.getElementById("content"); + contentDiv.innerHTML = ` +
+
+
+ + +
+
+ + vs ${opponentName} +
+
+ Connecting... +
+
+ + +
+
+ +
+

๐ŸŽฎ Multiplayer Pong

+
๐ŸฅŠ You vs ${opponentName}
+
+ + +
+
+
+
+ + You: + + Not Ready +
+
+
+
+ + ${opponentName}: + + Not Ready +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+

๐ŸŽฎ Controls:

+
+ W Move Up + S Move Down +
+
+
+
+
+ + + +
+
+ `; + + // Initialize game after DOM is ready + setTimeout(() => { + initializeMultiplayerGame( + opponentId, + opponentName, + existingRoomId, + tournamentId + ); + }, 100); +} + +async function initializeMultiplayerGame( + opponentId, + opponentName, + existingRoomId = null, + tournamentId = null +) { + try { + // Setup the game scene (same as local pong) + SETUP.setupGame(); + + // Set multiplayer specific state + state.isMultiplayer = true; + state.isStarted = false; + state.isPaused = true; + state.IAisActive = false; // Disable AI for multiplayer + + // Use client-side prediction for smoother gameplay instead of disabling physics + if (state.ball) { + state.ball.disableLocalPhysics = false; // Keep physics for prediction + state.ball.isMultiplayer = true; // Flag for server reconciliation + } + + // Update connection status + updateConnectionStatus("connecting", "Connecting to server..."); + + if (existingRoomId) { + // Join existing game room (from URL parameters) + const { initializeWebSocket } = await import("./serverSide.js"); + initializeWebSocket(existingRoomId, opponentId, opponentName); + } else { + // Create new game session (fallback) + const { userId } = getVariables(); + await createGame(parseInt(userId), parseInt(opponentId), tournamentId); + } + + // Setup event listeners + setupMultiplayerEventListeners(); + + // Start game animation (paused until both players ready) + GAME.animate(); + } catch (error) { + console.error("Failed to initialize multiplayer game:", error); + updateConnectionStatus("error", "Connection failed"); + showGameError("Failed to connect to multiplayer game. Please try again."); + } +} + +// function sendGameInitialization() { +// try { +// const { sendGameInit } = import("./serverSide.js"); + +// // Send current game dimensions to backend +// const gameConfig = { +// ring_length: state.ring.length || 160, +// ring_height: state.ring.height || 90, +// ring_width: state.ring.width || 200, +// ring_thickness: state.ring.thickness || 3, +// p_length: state.p.height || 15, +// p_width: state.p.width || 2, +// p_height: state.p.depth || 2, +// ball_radius: state.ball_radius || 2.5, +// player_1_pos: [-60, 0], // Left paddle position +// player_2_pos: [60, 0], // Right paddle position +// ball_speed: state.ball_speed || 1, +// p_speed: state.player_speed || 2, +// }; + +// sendGameInit(gameConfig); +// } catch (error) { +// console.error("Failed to send game initialization:", error); +// } +// } + +function setupMultiplayerEventListeners() { + // Ready button + const readyBtn = document.getElementById("ready-button"); + if (readyBtn) { + readyBtn.addEventListener("click", handlePlayerReady); + } + + // Leave game button + const leaveBtn = document.getElementById("leave-game-btn"); + if (leaveBtn) { + leaveBtn.addEventListener("click", () => { + if (confirm("Are you sure you want to leave the game?")) { + exitMultiplayerGame(); + } + }); + } + + // Keyboard controls (same as local pong but sends to server) + document.addEventListener("keydown", handleMultiplayerKeyDown); + document.addEventListener("keyup", handleMultiplayerKeyUp); + + // Store references for cleanup + window.multiplayerKeyDown = handleMultiplayerKeyDown; + window.multiplayerKeyUp = handleMultiplayerKeyUp; +} + +function handlePlayerReady() { + const readyBtn = document.getElementById("ready-button"); + const playerStatus = document.getElementById("player-ready-status"); + + if (readyBtn && playerStatus) { + // Update UI + readyBtn.disabled = true; + readyBtn.innerHTML = + 'Waiting for opponent...'; + + // Update player status with proper structure + playerStatus.innerHTML = ` + + You: + + Ready + `; + + // Send ready signal to server + try { + const playerId = window.multiplayerInfo?.playerId || 0; + sendPlayerReady(playerId); + showNotification("You are ready! Waiting for opponent...", "info"); + } catch (error) { + console.error("Failed to send ready signal:", error); + showNotification("Failed to send ready signal. Try again.", "error"); + + // Revert UI + readyBtn.disabled = false; + readyBtn.innerHTML = + 'Ready to Play'; + playerStatus.innerHTML = ` + + You: + + Not Ready + `; + } + } +} + +function handleMultiplayerKeyDown(event) { + if (!state.isStarted || state.isPaused) { + return; + } + + const playerId = window.multiplayerInfo?.playerId || 0; + + try { + if (event.key.toLowerCase() === "w") { + sendPlayerMovement(playerId, "up"); + } else if (event.key.toLowerCase() === "s") { + sendPlayerMovement(playerId, "down"); + } else if (event.key === "Escape") { + togglePause(); + } + } catch (error) { + console.error("Failed to send movement:", error); + } +} + +function handleMultiplayerKeyUp(event) { + if (!state.isStarted || state.isPaused) return; + + const playerId = window.multiplayerInfo?.playerId || 0; + + try { + if (event.key.toLowerCase() === "w" || event.key.toLowerCase() === "s") { + sendPlayerMovement(playerId, "stop"); + } + } catch (error) { + console.error("Failed to send movement:", error); + } +} + +function togglePause() { + state.isPaused = !state.isPaused; + + if (state.isPaused) { + showNotification("Game Paused", "info"); + // Show controls info when paused + const controlsInfo = document.getElementById("controls-info"); + if (controlsInfo) { + controlsInfo.style.display = "block"; + } + } else { + showNotification("Game Resumed", "info"); + // Hide controls info when resumed + const controlsInfo = document.getElementById("controls-info"); + if (controlsInfo) { + controlsInfo.style.display = "none"; + } + } +} + +function updateConnectionStatus(status, message) { + const connectionStatus = document.getElementById("connection-status"); + if (!connectionStatus) return; + + const statusBadges = { + connecting: 'Connecting...', + connected: 'Connected', + error: 'Connection Error', + disconnected: 'Disconnected', + }; + + connectionStatus.innerHTML = + statusBadges[status] || statusBadges.disconnected; + + if (message) { + connectionStatus.title = message; + } +} + +function exitMultiplayerGame() { + // Cleanup event listeners + if (window.multiplayerKeyDown) { + document.removeEventListener("keydown", window.multiplayerKeyDown); + document.removeEventListener("keyup", window.multiplayerKeyUp); + window.multiplayerKeyDown = null; + window.multiplayerKeyUp = null; + } + + // Stop game + state.isStarted = false; + state.isPaused = true; + state.isMultiplayer = false; + + // Clean up game state + SETTINGS.cleanupPong(); + + // Navigate back to home + window.navigateTo("#home"); + + showNotification("Left multiplayer game", "info"); +} + +function showGameError(message) { + const readyScreen = document.getElementById("ready-screen"); + if (readyScreen) { + readyScreen.innerHTML = ` +
+

Connection Error

+

${message}

+
+ + +
+
+ `; + } +} + +// Export functions for global access +export { + initializeMultiplayerGame, + setupMultiplayerEventListeners, + handlePlayerReady, + exitMultiplayerGame, + updateConnectionStatus, +}; + +// Make functions globally available for onclick handlers +window.handlePlayerReady = handlePlayerReady; +window.exitMultiplayerGame = exitMultiplayerGame; diff --git a/Front-End/public/pong/multiplayer/serverSide.js b/Front-End/public/pong/multiplayer/serverSide.js new file mode 100644 index 00000000..68a022db --- /dev/null +++ b/Front-End/public/pong/multiplayer/serverSide.js @@ -0,0 +1,596 @@ +import * as THREE from "three"; +import { state } from "../locale/state.js"; +import { getVariables } from "../../var.js"; +import { renderPong } from "../locale/pong.js"; +import * as GAME from "../locale/gameLogic.js"; +import * as SETUP from "../locale/setup.js"; +import * as UTILS from "../locale/utils.js"; +import { getCookie } from "../../cookie.js"; +import { showNotification } from "../pongContainer.js"; +import { updateScore } from "../locale/src/Score.js"; + +let socket; + +// Throttling to prevent too frequent updates +let lastGameStateUpdate = 0; +const GAME_STATE_THROTTLE = 20; // Minimum 50ms between game state updates (20fps max) + +function sendPlayerMovement(playerId, direction) { + if (!socket || socket.readyState !== WebSocket.OPEN) { + console.error("WebSocket not connected!"); + throw new Error("WebSocket not connected"); + } + + const movementMessage = { + type: direction, + }; + + socket.send(JSON.stringify(movementMessage)); + return true; +} + +async function createGame(player_1, player_2, tournamentId = null) { + const { url_api, token } = getVariables(); + + if (!url_api || !token) { + throw new Error("Missing API URL or authentication token"); + } + + // Ensure player IDs are sent as integers, not arrays + const gameData = { + player_1: parseInt(player_1), + player_2: parseInt(player_2), + tournament_id: tournamentId ? parseInt(tournamentId) : null, + }; + + try { + const response = await fetch(`${url_api}/pong/game`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(gameData), + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error("Game creation failed:", errorData); + throw new Error(`HTTP ${response.status}: ${JSON.stringify(errorData)}`); + } + + const data = await response.json(); + + // Extract room_id and initialize WebSocket + const room_id = data.room_id || data.id; + if (room_id) { + initializeWebSocket(room_id, player_1, player_2); + } else { + throw new Error("No room_id received from server"); + } + + return data; + } catch (error) { + console.error("Error creating game:", error); + throw error; + } +} + +function hideAllMenusAndStartGame() { + // Start the game using local game logic + state.isStarted = true; + state.isPaused = false; + state.IAisActive = false; // Disable AI for multiplayer + + // Hide ready screen + const readyScreen = document.getElementById("ready-screen"); + if (readyScreen) { + readyScreen.style.display = "none"; + } + + // Hide any local pong menus that might interfere + const menusToHide = [ + "menu", + "nbrOfPlayerMenu", + "settingsMenu", + "pauseMenu", + "gameOverMenu", + "readyMenu", + ]; + + menusToHide.forEach((menuId) => { + const menu = document.getElementById(menuId); + if (menu) { + menu.style.display = "none"; + } + }); + + // Hide connection status overlay + const connectionOverlay = document.querySelector( + ".position-absolute.top-0.end-0" + ); + if (connectionOverlay) { + connectionOverlay.style.display = "none"; + } + + // Show in-game controls info + const controlsInfo = document.getElementById("controls-info"); + if (controlsInfo) { + controlsInfo.style.display = "block"; + } + + // Make sure the game canvas is visible and on top + const gameContainer = document.querySelector(".gamecontainer"); + const threejsContainer = document.getElementById("threejs-container"); + + if (gameContainer) { + gameContainer.style.position = "relative"; + gameContainer.style.width = "100%"; + gameContainer.style.height = "100vh"; + gameContainer.style.background = "#000"; + } + + if (threejsContainer) { + threejsContainer.style.position = "absolute"; + threejsContainer.style.top = "0"; + threejsContainer.style.left = "0"; + threejsContainer.style.width = "100%"; + threejsContainer.style.height = "100%"; + threejsContainer.style.zIndex = "1"; + } + + // Start game animation if not already running + if (!state.animationFrameId) { + GAME.animate(); + } + + // Show mobile controls if on mobile device + if (window.initializeMobileControls) { + window.initializeMobileControls(); + if (window.showMobileControls) { + window.showMobileControls(); + } + } +} + +function initializeWebSocket(room_id, player1, player2) { + const { token, wss_api } = getVariables(); + + const wsUrl = `${wss_api}/pong/ws/pong/${room_id}/?token=${token}`; + + socket = new WebSocket(wsUrl); + if (!window.activeWebSockets) window.activeWebSockets = []; + window.activeWebSockets.push(socket); + + socket.onopen = function () { + updateConnectionStatus("connected", "Connected to game server"); + }; + + socket.onmessage = function (event) { + const message = JSON.parse(event.data); + + if (message.type === "game_state") { + try { + // console.table(message.game_state); + updateGameState(message.game_state); + } catch (error) { + console.error("Error processing game state:", error); + } + } else if (message.type === "connection_success") { + // Store player info in window object + window.multiplayerInfo = { + playerId: message.player_id, + roomId: room_id, + }; + + // Update connection status in UI + updateConnectionStatus("connected", "Connected to game server"); + + // Initialize the game scene + syncMultiplayerWithLocalGame(); + } else if ( + message.type === "all_players_ready" || + message.message === "All players are ready!" + ) { + // Add game-active class to body + document.body.classList.add("game-active"); + + // let player_1_pos = [state.players[0].mesh.position.x, state.players[0].mesh.position.y, state.players[0].mesh.position.z]; + // let player_2_pos = [state.players[1].mesh.position.x, state.players[1].mesh.position.y, state.players[1].mesh.position.z]; + // // Send game initialization to backend + try { + // const gameConfig = { + // ring_length: state.ring.length || 160, + // ring_height: state.ring.height || 90, + // ring_width: state.ring.depth || 10, + // ring_thickness: state.ring.thickness || 3, + // p_length: 20, + // p_width: state.p.width || 2.5, + // p_height: state.p.depth || 2.5, + // ball_radius: state.ball.radius || 2.5, + // player_1_pos: player_1_pos || [-75, 0], // Left side: [x, y, z] where y=0 is center + // player_2_pos: player_2_pos || [75, 0], // Right side: [x, y, z] where y=0 is center + // ball_pos: state.ball_pos || [0, 0, 0], + // ball_speed: state.ball_speed || 1.2, + // p_speed: state.p_speed || 1.5, + // }; + // console.table(gameConfig); + + // sendGameInit(gameConfig); + + // Wait a moment for initialization to process + setTimeout(() => { + hideAllMenusAndStartGame(); + showNotification("Game Started! Good luck!", "success"); + }, 200); + } catch (error) { + console.error("Failed to initialize game:", error); + // Still try to start the game + hideAllMenusAndStartGame(); + showNotification("Game Started! Good luck!", "success"); + } + } else if (message.message === "Waiting for players to be ready...") { + updateOpponentStatus("waiting"); + } else if (message.type === "quit_game") { + // Remove game-active class + document.body.classList.remove("game-active"); + + showNotification(message.message, "warning"); + + // Stop the game and show ready screen + state.isStarted = false; + state.isPaused = true; + showReadyScreen(); + } else if (message.message === "Game Over!") { + // Remove game-active class + document.body.classList.remove("game-active"); + + handleGameOver(); + } + }; + + socket.onerror = function (error) { + console.error("WebSocket error occurred:", error); + }; + + socket.onclose = function (event) { + updateConnectionStatus("disconnected", "Connection closed"); + }; +} + +function sendPlayerReady(playerId) { + if (!socket || socket.readyState !== WebSocket.OPEN) { + throw new Error("WebSocket not connected"); + } + + const readyMessage = { + type: "player_ready", + player: playerId, + }; + + socket.send(JSON.stringify(readyMessage)); + return true; +} + +function sendGameInit(gameConfig) { + if (!socket || socket.readyState !== WebSocket.OPEN) { + throw new Error("WebSocket not connected"); + } + + const initMessage = { + type: "game_init", + ...gameConfig, + }; + + socket.send(JSON.stringify(initMessage)); + return true; +} + +function sendChatMessage(message) { + if (!socket || socket.readyState !== WebSocket.OPEN) { + throw new Error("WebSocket not connected"); + } + + const chatMessage = { + type: "chat_message", + message: message, + }; + + socket.send(JSON.stringify(chatMessage)); + return true; +} + +function updateConnectionStatus(status, message) { + const connectionStatus = document.getElementById("connection-status"); + if (!connectionStatus) return; + + const statusBadges = { + connecting: 'Connecting...', + connected: 'Connected', + error: 'Connection Error', + disconnected: 'Disconnected', + }; + + connectionStatus.innerHTML = + statusBadges[status] || statusBadges.disconnected; + + if (message) { + connectionStatus.title = message; + } +} + +function syncMultiplayerWithLocalGame() { + if (!state.scene || !state.camera || !state.renderer) { + SETUP.setupGame(); + } + + // Disable local controls since we're using WebSocket + state.keys = { + w: false, + s: false, + ArrowUp: false, + ArrowDown: false, + }; + + // Set multiplayer mode + state.isMultiplayer = true; + state.isStarted = false; + state.isPaused = true; + + // Disable local physics calculations for better performance + if (state.ball) { + state.ball.disableLocalPhysics = true; + state.ball.isMultiplayer = true; + } +} + +const gameStateChecker = setInterval(() => { + if (state.isMultiplayer && (!state.isStarted || state.isPaused)) { + state.isStarted = true; + state.isPaused = false; + } +}, 1000); + +// Clean up on page navigation +window.addEventListener("hashchange", () => { + clearInterval(gameStateChecker); + + // Remove mobile controls when navigating away from pong + if (typeof window.removeMobileControls === "function") { + window.removeMobileControls(); + } +}); + +function updateGameState(gameStateData) { + if (!gameStateData) { + return; + } + + // Update state with backend values for consistency + if (gameStateData.player_1_score !== undefined) { + state.p1_score = gameStateData.player_1_score; + updateScore("p1"); + } + + if (gameStateData.player_2_score !== undefined) { + state.p2_score = gameStateData.player_2_score; + updateScore("p2"); + } + // Update mobile score display + + // Update ring dimensions only when provided (they're sent less frequently now for performance) + if ( + gameStateData.ring_length !== undefined && + gameStateData.ring_length !== null + ) { + state.ring.length = gameStateData.ring_length; + } + if ( + gameStateData.ring_height !== undefined && + gameStateData.ring_height !== null + ) { + state.ring.height = gameStateData.ring_height; + } + // if (gameStateData.ring_width !== undefined) state.ring.depth = gameStateData.ring_width; + // if (gameStateData.ring_thickness !== undefined) state.ring.thickness = gameStateData.ring_thickness; + // if (gameStateData.p_length !== undefined) state.p.depth = gameStateData.p_length; + // if (gameStateData.p_height !== undefined) state.p.height = gameStateData.p_height; + // if (gameStateData.p_width !== undefined) state.p.width = gameStateData.p_width; + // if (gameStateData.ball_radius !== undefined) state.ball.radius = gameStateData.ball_radius; + // if (gameStateData.p_speed !== undefined) state.p.speed = gameStateData.p_speed; + + // // Debug log the updated state values periodically + // if (Math.random() < 0.01) { // Log ~1% of the time to avoid spam + // console.log("๐ŸŽฎ Updated state from backend:", { + // ring_length: state.ring.length, + // ring_height: state.ring.height, + // ring_width: state.ring.depth, + // ring_thickness: state.ring.thickness, + // p_length: state.p.depth, + // p_height: state.p.height, + // p_width: state.p.width, + // ball_radius: state.ball.radius, + // p_speed: state.p.speed + // }); + // } + + // PLAYER 1 POSITION UPDATE + if (gameStateData.player_1_pos && state.players && state.players[0]?.mesh) { + // player_1_pos is [x, percentage] where percentage is 0-100 + const [posX, percentY] = gameStateData.player_1_pos; + + // Scale X position from backend to frontend dimensions + if ( + typeof posX === "number" && + !isNaN(posX) && + gameStateData.ring_length > 0 + ) { + const scaledX = (posX / gameStateData.ring_length) * state.ring.length; + state.players[0].mesh.position.x = scaledX; + } + + // Validate that we have valid numbers to prevent NaN + if ( + typeof percentY === "number" && + !isNaN(percentY) && + state.ring.height > 0 + ) { + // Convert percentage (0-100) to actual position relative to FRONTEND ring dimensions + // 0% = top of ring (-state.ring.height/2), 100% = bottom of ring (state.ring.height/2) + const actualY = + (percentY / 100) * state.ring.height - state.ring.height / 2; + + // Update position (Z in ThreeJS is Y in backend) + state.players[0].mesh.position.z = actualY; + } else { + console.warn( + "Player 1 - Invalid percentY:", + percentY, + "or frontend ring_height:", + state.ring.height + ); + } + } + + // PLAYER 2 POSITION UPDATE + if (gameStateData.player_2_pos && state.players && state.players[1]?.mesh) { + // player_2_pos is [x, percentage] where percentage is 0-100 + const [posX, percentY] = gameStateData.player_2_pos; + + // Scale X position from backend to frontend dimensions + if ( + typeof posX === "number" && + !isNaN(posX) && + gameStateData.ring_length > 0 + ) { + const scaledX = (posX / gameStateData.ring_length) * state.ring.length; + state.players[1].mesh.position.x = scaledX; + } + + // Validate that we have valid numbers to prevent NaN + if ( + typeof percentY === "number" && + !isNaN(percentY) && + state.ring.height > 0 + ) { + // Convert percentage (0-100) to actual position relative to FRONTEND ring dimensions + // 0% = top of ring (-state.ring.height/2), 100% = bottom of ring (state.ring.height/2) + const actualY = + (percentY / 100) * state.ring.height - state.ring.height / 2; + + // Update position (Z in ThreeJS is Y in backend) + state.players[1].mesh.position.z = actualY; + } else { + console.warn( + "Player 2 - Invalid percentY:", + percentY, + "or frontend ring_height:", + state.ring.height + ); + } + } + + // BALL POSITION AND PHYSICS UPDATE - Use server authority with client-side prediction + if (gameStateData.ball_pos && state.ball?.mesh) { + const [ballX, ballY] = gameStateData.ball_pos; + + // Update ball position directly from server (authoritative) + state.ball.mesh.position.set(ballX, 0, ballY); + + // Update ball physics from server for client-side prediction + if (gameStateData.ball_speed !== undefined) { + state.ball_speed = gameStateData.ball_speed; + } + + if (gameStateData.angle !== undefined) { + state.angle = gameStateData.angle; + + // Update ball velocity based on server angle and speed for prediction + if (state.ball_speed > 0) { + const angleRad = (state.angle * Math.PI) / 180; + state.ball_velocity = { + x: state.ball_speed * Math.cos(angleRad), + y: state.ball_speed * -Math.sin(angleRad), // Negative because backend uses -sin + }; + } + } + + // Enable client-side prediction flag + state.ball.isMultiplayer = true; + } + + // Force render scene + if (state.renderer && state.scene && state.camera) { + state.renderer.render(state.scene, state.camera); + } +} + +function showReadyScreen() { + const readyScreen = document.getElementById("ready-screen"); + if (readyScreen) { + readyScreen.style.display = "flex"; + } + + const controlsInfo = document.getElementById("controls-info"); + if (controlsInfo) { + controlsInfo.style.display = "none"; + } +} + +function updateOpponentStatus(status) { + const opponentStatus = document.getElementById("opponent-ready-status"); + if (opponentStatus) { + const statusBadge = + status === "waiting" + ? 'Not Ready' + : 'Ready'; + + // Get opponent name from the multiplayerGameInfo + const opponentName = window.multiplayerGameInfo?.opponentName || "Opponent"; + + opponentStatus.innerHTML = ` + + ${opponentName}: + + ${statusBadge} + `; + } +} + +function handleGameOver() { + state.isStarted = false; + state.isPaused = true; + + // Remove mobile controls + if (typeof window.removeMobileControls === "function") { + window.removeMobileControls(); + } + + // Determine winner + const winner = state.p1_score >= state.maxScore ? "Player 1" : "Player 2"; + + // Show game over with multiplayer context + showNotification(`Game Over! ${winner} wins!`, "info"); + + window.navigateTo("#home"); + // Show ready screen for potential rematch + setTimeout(() => { + showReadyScreen(); + }, 2000); +} + +export { + createGame, + initializeWebSocket, + socket, + sendPlayerReady, + sendGameInit, + sendPlayerMovement, + sendChatMessage, + syncMultiplayerWithLocalGame, + updateGameState, + showReadyScreen, + updateOpponentStatus, + handleGameOver, + hideAllMenusAndStartGame, +}; diff --git a/Front-End/public/pong/pong.css b/Front-End/public/pong/pong.css deleted file mode 100644 index 8ca9737d..00000000 --- a/Front-End/public/pong/pong.css +++ /dev/null @@ -1,60 +0,0 @@ -body { - margin: 0; - overflow: hidden; -} - -#menu, #nbrOfPlayerMenu, #settingsMenu, #pauseMenu, #modeMenu { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.8); - padding: 20px; - border-radius: 10px; - text-align: center; - display: none; - color: blueviolet; -} - -#menu button, #nbrOfPlayerMenu button, #settingsMenu button, #pauseMenu button, #modeMenu button { - display: block; - margin: 10px auto; - padding: 10px 20px; - font-size: 16px; -} - -#settingsMenu .color-setting { - margin: 10px 0; -} - -#settingsMenu .color-setting label { - display: block; - margin-bottom: 5px; - font-size: 16px; -} - -#settingsMenu .color-pickers { - display: flex; - justify-content: space-between; - align-items: center; -} - -#settingsMenu .color-pickers input { - margin: 0 5px; - font-size: 16px; -} - -#gameOverImage { - position: absolute; - top: 0%; - max-width: 10%; - max-height: 10%; - display: none; -} - -.gamecontainer { - width: 100%; - display: flex; - justify-content: center; - align-items: center; -} \ No newline at end of file diff --git a/Front-End/public/pong/pong.js b/Front-End/public/pong/pong.js deleted file mode 100644 index f1a373e0..00000000 --- a/Front-End/public/pong/pong.js +++ /dev/null @@ -1,234 +0,0 @@ -import * as THREE from "three"; -import { FontLoader } from "three/addons/loaders/FontLoader.js"; -import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; -import { Group, remove } from "three/addons/libs/tween.module.js"; -import { state } from "./state.js"; -import * as IA from "./ai.js"; -import * as UTILS from "./utils.js"; -import * as SETUP from "./setup.js"; -import * as SETTINGS from "./settings.js"; -import * as GAME from "./gameLogic.js"; -import Stats from "three/addons/libs/stats.module.js"; - -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/pong/pong.css'; -document.head.appendChild(link); - -export function renderPong() { - const contentDiv = document.getElementById("content"); - contentDiv.innerHTML = ` -
- - - - - - -
-
- `; - - document - .getElementById("newGameButton") - .addEventListener("click", SETTINGS.shownbrOfPlayerMenu); - document - .getElementById("settingsButton") - .addEventListener("click", SETTINGS.showSettingsMenu); - document - .getElementById("exitButton") - .addEventListener("click", SETTINGS.exitGame); - document - .getElementById("onePlayerButton") - .addEventListener("click", SETTINGS.startOnePlayerGame); - document - .getElementById("twoPlayerButton") - .addEventListener("click", SETTINGS.startTwoPlayerGame); - document - .getElementById("backButton") - .addEventListener("click", SETTINGS.showMainMenu); - - document - .getElementById("classicModeButton") - .addEventListener("click", SETTINGS.startClassicmode); - - document - .getElementById("crazyModeButton") - .addEventListener("click", SETTINGS.startCrazymode); - - document - .getElementById("backFromModeButton") - .addEventListener("click", SETTINGS.shownbrOfPlayerMenu); - - document - .getElementById("saveSettingsButton") - .addEventListener("click", SETTINGS.saveSettings); - document - .getElementById("resetSettingsButton") - .addEventListener("click", SETTINGS.resetSettings); - document - .getElementById("backFromSettingsButton") - .addEventListener("click", SETTINGS.showMainMenu); - document - .getElementById("resumeButton") - .addEventListener("click", SETTINGS.resumeGame); - document - .getElementById("exitButtonPause") - .addEventListener("click", SETTINGS.exitGame); - document.getElementById("player1Color").addEventListener("input", (event) => { - state.mat.p1.color.set(event.target.value); - }); - - document - .getElementById("player1Emissive") - .addEventListener("input", (event) => { - state.mat.p1.emissive.set(event.target.value); - }); - - document.getElementById("player2Color").addEventListener("input", (event) => { - state.mat.p2.color.set(event.target.value); - }); - - document - .getElementById("player2Emissive") - .addEventListener("input", (event) => { - state.mat.p2.emissive.set(event.target.value); - }); - - document.getElementById("ballColor").addEventListener("input", (event) => { - state.mat.ball.color.set(event.target.value); - }); - - document.getElementById("ballEmissive").addEventListener("input", (event) => { - state.mat.ball.emissive.set(event.target.value); - }); - - document.getElementById("ringColor").addEventListener("input", (event) => { - state.mat.ring.color.set(event.target.value); - }); - - document.getElementById("ringEmissive").addEventListener("input", (event) => { - state.mat.ring.emissive.set(event.target.value); - }); - - document.getElementById("showStats").addEventListener("change", (event) => { - UTILS.toggleStats(event.target.checked); - }); - - document.getElementById("menu").style.display = "block"; - -//--TODO merge pong - // Inizializza il renderer di Three.js - if (!state.renderer) { - state.renderer = new THREE.WebGLRenderer({ antialias: true }); - state.renderer.setSize(window.innerWidth, window.innerHeight); - } - - // Inizializza la scena e la camera - if (!state.scene) { - state.scene = new THREE.Scene(); - } - - if (!state.camera) { - state.camera = new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ); - state.camera.position.z = 5; - } - - // Aggiungi il canvas di Three.js al DOM - const threejsContainer = document.getElementById("threejs-container"); - if (threejsContainer && state.renderer.domElement) { - threejsContainer.appendChild(state.renderer.domElement); - } else { - console.error("threejsContainer o state.renderer.domElement non trovato"); - } -} -//-- fine TODO merge pong - -// Inizializza il gioco -SETUP.setupGame(); -//--GAME.animate(); - - -//--} - -// GAME.animate(); -// requestAnimationFrame(GAME.animate); - -//Resize handler -window.addEventListener("resize", () => { - state.camera.aspect = window.innerWidth / window.innerHeight; - state.camera.updateProjectionMatrix(); - state.renderer.setSize(window.innerWidth, window.innerHeight); - state.renderer.render(state.scene, state.camera); //--TODO -}); - - -document.addEventListener("wheel", function (event) { - state.cam.z += event.deltaY / 10; - state.camera.position.set(state.cam.x, state.cam.y, state.cam.z); -}); - -// document.addEventListener("mousemove", function (event) { -// const rect = state.renderer.domElement.getBoundingClientRect(); -// const mouse = { -// x: ((event.clientX - rect.left) / rect.width) * 2 - 1, -// y: -((event.clientY - rect.top) / rect.height) * 2 + 1, -// }; -// // console.log(mouse); -// }); diff --git a/Front-End/public/pong/pongContainer.css b/Front-End/public/pong/pongContainer.css index 71a5d561..51f9adeb 100644 --- a/Front-End/public/pong/pongContainer.css +++ b/Front-End/public/pong/pongContainer.css @@ -1,7 +1,6 @@ .pong-card { width: 100%; height: 100%; - background-color: #f5f5f5f0; border-radius: 8px; flex-grow: 1; padding: 10px; @@ -21,7 +20,7 @@ background-color: rgba(0, 0, 0, 0.5); } -.login_box { +/* .login_box { background-color: #fefefe; padding: 20px; border: 1px solid #888; @@ -29,7 +28,7 @@ max-width: 400px; border-radius: 8px; position: relative; -} +} */ .close-button { color: #aaa; @@ -44,4 +43,23 @@ color: black; text-decoration: none; cursor: pointer; +} + +.tournament { + padding: 10px; +} + +.matchHistoryContainer { + width: 100%; + height: 100%; + border-radius: 8px; + flex-grow: 1; + padding: 10px; +} + +.match-details { + display: flex; + flex-direction: row; + align-items: start; + justify-content: space-between; } \ No newline at end of file diff --git a/Front-End/public/pong/pongContainer.js b/Front-End/public/pong/pongContainer.js index 0c68cf80..abd22a92 100644 --- a/Front-End/public/pong/pongContainer.js +++ b/Front-End/public/pong/pongContainer.js @@ -1,16 +1,36 @@ -import { setVariables, getVariables } from '../var.js'; -import { getCookie } from '../cookie.js'; -import { loginUser } from '../login/login.js'; -import { registerUser } from '../register/register.js'; -import { createGame } from './serverSide.js'; +import { setVariables, getVariables } from "../var.js"; +import { getCookie } from "../cookie.js"; +import { loginUser } from "../login/login.js"; +import { registerUser } from "../register/register.js"; +import { renderPong } from "./locale/pong.js"; -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.href = '/public/pongContainer/pongContainer.css'; -document.head.appendChild(link); +// Load CSS files +const pongContainerCSS = document.createElement("link"); +pongContainerCSS.rel = "stylesheet"; +pongContainerCSS.href = "/pongContainer/pongContainer.css"; +document.head.appendChild(pongContainerCSS); +const friendListCSS = document.createElement("link"); +friendListCSS.rel = "stylesheet"; +friendListCSS.href = "/pong/friendlist.css"; +document.head.appendChild(friendListCSS); + +// Load mobile controls CSS +const mobileControlsCSS = document.createElement("link"); +mobileControlsCSS.rel = "stylesheet"; +mobileControlsCSS.href = "/pong/mobile-controls.css"; +document.head.appendChild(mobileControlsCSS); + +// Load FontAwesome for icons +const fontAwesome = document.createElement("link"); +fontAwesome.rel = "stylesheet"; +fontAwesome.href = + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"; +document.head.appendChild(fontAwesome); + +// Update your existing renderPongInfo function function renderPongInfo() { - const pongInfoContainer = document.getElementById('pongContainer'); + const pongInfoContainer = document.getElementById("pongContainer"); pongInfoContainer.innerHTML = `
@@ -18,26 +38,331 @@ function renderPongInfo() {

A simple pong game

- - + + +
+
`; + + matchHistory(); } -function handleLocalePong() { - // TODO comunque chiamata a gu anche se in singolo e Redirect to pong game - createGame(9, 10); - window.navigateTo('#pong'); +async function PongStatistic() { + try { + const { token, url_api, userId } = getVariables(); + console.log("[PongStatistic] Fetching player stats for user_id:", userId); + const response = await fetch( + // `${url_api}/user/user/user?user_id=15`, + `${url_api}/pong/player/stats?user_id=${userId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + } + ); + const data = await response.json(); + console.log("[PongStatistic] API response:", data); + } catch (error) { + console.error("[PongStatistic] API error:", error); + } +} + +window.PongStatistic = PongStatistic; + +async function handleLocalePong() { + window.navigateTo("#pong"); } function handleMultiPong() { - showLoginBox(); + openFriendList(); +} + +// Load HTML template +async function loadFriendListTemplate() { + try { + const response = await fetch("/pong/friendlist.html"); + if (!response.ok) throw new Error("Template not found"); + return await response.text(); + } catch (error) { + console.error("Error loading friend list template:", error); + showNotification( + "Failed to load friend list. Please try again.", + "error" + ); + return null; + } +} + +// Get user's initials for avatar +// TODO cambiare con l'avatar del profilo +function getUserInitials(username) { + if (!username) return "?"; + return username + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); +} + +// Create friend item HTML +function createFriendItemHTML(friendship) { + const friend = friendship.friend_info; + const username = friend?.username || "Unknown"; + const userId = friend?.user_id; + const initials = getUserInitials(username); + + return ` +
+
+
+ ${initials} +
+
+
${username}
+
+
+ +
+
+
+ `; +} + +// Send game invitation +// // Uncomment and update the inviteToGame function +async function inviteToGame(friendId, friendName) { + try { + console.log( + `๐ŸŽฎ Starting multiplayer game with ${friendName} (ID: ${friendId})` + ); + + // Show loading state + const inviteBtn = document.querySelector( + `[data-friend-id="${friendId}"] .btn-game-invite` + ); + if (inviteBtn) { + inviteBtn.innerHTML = + 'Connecting...'; + inviteBtn.disabled = true; + } + + // Close friend list + closeFriendList(); + + window.navigateTo(`#pongmulti?opponent=${friendId}&opponentName=${encodeURIComponent(friendName)}`); + } catch (error) { + console.error("๐Ÿ’ฅ Error starting multiplayer game:", error); + showNotification("โŒ Failed to start multiplayer game", "error"); + + + // Reset button state + const inviteBtn = document.querySelector( + `[data-friend-id="${friendId}"] .btn-game-invite` + ); + if (inviteBtn) { + inviteBtn.innerHTML = 'Invite'; + inviteBtn.disabled = false; + } + } +} + +// Show Bootstrap toast notification +function showNotification(message, type = "info") { + // Remove existing notifications + const existingNotifications = document.querySelectorAll( + ".unified-notification" + ); + existingNotifications.forEach((notification) => notification.remove()); + + const typeIcons = { + success: "โœ…", + error: "โŒ", + info: "โ„น๏ธ", + warning: "โš ๏ธ", + }; + + const typeNames = { + success: "Success", + error: "Error", + info: "Information", + warning: "Warning", + }; + + + const notificationHTML = ` +
+
+ ${typeIcons[type]} + ${typeNames[type]} + +
+
+ ${message} +
+
+
+ `; + + + document.body.insertAdjacentHTML("beforeend", notificationHTML); + + const notificationElement = document.querySelector( + ".unified-notification:last-child" + ); + + // Show with animation + setTimeout(() => { + notificationElement.classList.add("show"); + }, 100); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + if (notificationElement.parentNode) { + notificationElement.classList.remove("show"); + notificationElement.classList.add("hide"); + setTimeout(() => { + if (notificationElement.parentNode) { + notificationElement.remove(); + } + }, 300); + } + }, 5000); + + // Animate progress bar + const progressBar = notificationElement.querySelector( + ".unified-notification-progress" + ); + if (progressBar) { + progressBar.style.transition = "width 5000ms linear"; + progressBar.style.width = "0%"; + } +} + +async function openFriendList() { + let friendListModal = document.getElementById("friendListModal"); + if (friendListModal) return; // Already open + + // Load template + const template = await loadFriendListTemplate(); + if (!template) return; + + // Create modal container + friendListModal = document.createElement("div"); + friendListModal.id = "friendListModal"; + friendListModal.className = "friend-list-modal"; + friendListModal.innerHTML = template; + document.body.appendChild(friendListModal); + + // Add event listeners + const closeBtn = friendListModal.querySelector("#closeFriendListBtn"); + closeBtn.addEventListener("click", closeFriendList); + + // Click outside to close + friendListModal.addEventListener("click", function (e) { + if (e.target === friendListModal) { + closeFriendList(); + } + }); + + // Load friends data + await loadFriendsData(); +} + +async function loadFriendsData() { + const loadingState = document.getElementById("loadingState"); + const friendListContainer = document.getElementById("friendListContainer"); + const emptyState = document.getElementById("emptyState"); + const errorState = document.getElementById("errorState"); + + try { + const { token, url_api } = getVariables(); + const response = await fetch( + `${url_api}/user/user/friend?status=accepted`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": getCookie("csrftoken"), + }, + } + ); + + // Hide loading state + loadingState.style.display = "none"; + + if (response.ok) { + const friendsData = await response.json(); + console.log("Friends data:", friendsData); + + if (friendsData.length === 0) { + emptyState.style.display = "block"; + } else { + friendListContainer.style.display = "block"; + friendListContainer.innerHTML = friendsData + .map(createFriendItemHTML) + .join(""); + } + } else if (response.status === 404) { + emptyState.style.display = "block"; + } else { + throw new Error(`HTTP ${response.status}`); + } + } catch (error) { + console.error("Error fetching friends:", error); + loadingState.style.display = "none"; + errorState.style.display = "block"; + } +} + +function closeFriendList() { + const friendListModal = document.getElementById("friendListModal"); + if (friendListModal) { + friendListModal.style.animation = "fadeIn 0.3s ease-in reverse"; + setTimeout(() => friendListModal.remove(), 300); + } +} + +// ... rest of your existing functions (showLoginBox, etc.) ... + +async function onHandleSubmit(e, email, password) { + e.preventDefault(); + const csrftoken = getCookie("csrftoken"); + const loginSuccess = await loginUser(email, password, csrftoken, false); + if (loginSuccess) { + window.navigateTo("#pong"); + } +} + +async function onHandleRegisterSubmit(e, username, email, password) { + e.preventDefault(); + const registerSuccess = await registerUser(username, email, password, false); + if (registerSuccess) { + showLoginBox(); + } } function showLoginBox() { - const loginBox = document.createElement('div'); - loginBox.className = 'login-box-modal'; + const loginBox = document.createElement("div"); + loginBox.className = "login-box-modal"; loginBox.innerHTML = `