diff --git a/.dockerignore b/.dockerignore
@@ -0,0 +1,11 @@
+**/node_modules
+__pycache__
+*.pyc
+*.pyo
+*.pyd
+.env*
+db.sqlite3
+db.sqlite3-journal
+*.log
+v2ray-core
+xray-core
\ No newline at end of file
diff --git a/.env.example b/.env.example
@@ -0,0 +1,50 @@
+UVICORN_HOST = "0.0.0.0"
+UVICORN_PORT = 8000
+
+
+## We highly recommend add admin using `marzban cli` tool and do not use
+## the following variables which is somehow hard codded infrmation.
+# SUDO_USERNAME = "admin"
+# SUDO_PASSWORD = "admin"
+
+# UVICORN_UDS: "/run/marzban.socket"
+# UVICORN_SSL_CERTFILE = "/var/lib/marzban/certs/example.com/fullchain.pem"
+# UVICORN_SSL_KEYFILE = "/var/lib/marzban/certs/example.com/key.pem"
+
+
+# XRAY_JSON = "xray_config.json"
+# XRAY_SUBSCRIPTION_URL_PREFIX = "https://example.com"
+# XRAY_SUBSCRIPTION_PATH = sub
+# XRAY_EXECUTABLE_PATH = "/usr/local/bin/xray"
+# XRAY_ASSETS_PATH = "/usr/local/share/xray"
+# XRAY_EXCLUDE_INBOUND_TAGS = "INBOUND_X INBOUND_Y"
+# XRAY_FALLBACKS_INBOUND_TAG = "INBOUND_X"
+
+
+# TELEGRAM_API_TOKEN = 123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+# TELEGRAM_ADMIN_ID = 987654321, 123456789
+# TELEGRAM_LOGGER_CHANNEL_ID = -1234567890123
+# TELEGRAM_DEFAULT_VLESS_FLOW = "xtls-rprx-vision"
+# TELEGRAM_PROXY_URL = "http://localhost:8080"
+
+# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/xxxxxxx"
+
+# CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzban/templates/"
+# CLASH_SUBSCRIPTION_TEMPLATE="clash/my-custom-template.yml"
+# SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"
+# HOME_PAGE_TEMPLATE="home/index.html"
+
+# Set headers for subscription
+# SUB_PROFILE_TITLE = "Susbcription"
+# SUB_SUPPORT_URL = "https://t.me/support"
+# SUB_UPDATE_INTERVAL = "12"
+
+# SQLALCHEMY_DATABASE_URL = "sqlite:///db.sqlite3"
+
+### for developers
+# DOCS=true
+# DEBUG=true
+# WEBHOOK_ADDRESS = "http://127.0.0.1:9000/"
+# WEBHOOK_SECRET = "something-very-very-secret"
+# VITE_BASE_API="https://example.com/api/"
+# JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 1440
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: Bug title
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Machine details (please complete the following information):**
+ - OS: [e.g. ubuntu 20]
+- Python version: [e.g 3.8]
+- Nodejs version: [e.g 16.17]
+- Browser [e.g. chrome, safari]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: Feature title
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
@@ -0,0 +1,44 @@
+name: Build
+
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+
+jobs:
+ build-docker-image:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ gozargah/marzban:latest
+ gozargah/marzban:${{github.ref_name}}
+ ghcr.io/gozargah/marzban:latest
+ ghcr.io/gozargah/marzban:${{github.ref_name}}
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
@@ -0,0 +1,24 @@
+name: Pylint
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10"]
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install -r requirements.txt
+ - name: Analysing the code with pylint
+ run: |
+ pylint $(git ls-files '*.py')
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,198 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+*.sqlite*
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.venv
+venv/
+venv.bak/
+.vscode/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+.env
+v2ray-core
+xray-core
+
+# don't track custom/dev.json/yaml/yml configs, such as xray-custom.json or clash-dev.yaml
+*-custom.json
+*-custom.yaml
+*-custom.yml
+*-dev.json
+*-dev.yml
+*-dev.yaml
+*-debug.*
+
+# Python virtual environment
+bin
+pyvenv.cfg
+lib64
+include/site/python3.11/greenlet
+share/man/man1
+
+# Frontend
+
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
+
+stats.html
+yarn.lock
+
+app/dashboard/build/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -0,0 +1,68 @@
+# Contribute to Marzban
+Thanks for considering contributing to Marzban!
+
+## Questions
+
+Please don't ask your questions in issues. Instead, use one of the following ways to ask:
+- Ask on our telegram group: [@Gozargah_Marzban](https://t.me/gozargah_marzban)
+- Ask on our [GitHub Discussions](https://github.com/gozargah/marzban/discussions) for long term discussion or larger questions.
+
+
+## Reporting issues
+
+Include the following information in your post:
+- Describe what you expected to happen.
+- Describe what actually happened. Include server logs or any error that browser shows.
+- If possible, post your xray json config file and what you have set in env (by censoring critical information).
+- Also tell the version of Marzban, Xray and docker (if you use docker) you are using.
+
+
+# Submitting a Pull Request
+If there is not an open issue for what you want to submit, prefer opening one for discussion before working on a PR. You can work on any issue that doesn't have an open PR linked to it or a maintainer assigned to it. These show up in the sidebar. No need to ask if you can work on an issue that interests you.
+
+## Branches
+When starting development on this project, please make sure to create a new branch off the `dev` branch. This helps to keep the `master` branch stable and free of any development work that may not be complete or fully tested.
+
+## Project Structure
+```
+.
+├── app # Backend code (FastAPI - Python)
+│ └── dashboard # Frontend code (React - Typescript)
+├── cli # CLI code (Typer - Python)
+└── xray_api # Client of Xray's gRPC API
+```
+
+## Backend
+Backend is built using FastAPI and uses SQLAlchemy as the ORM for database operations. All Pydantic models can be found in the `app/models` directory, while all database-related operations and models are in the `app/db` directory. The migration scripts for the database (Alembic) can be found in the `app/db/migrations` directory.
+
+### Python Code Formatting
+To maintain consistency in the codebase, we require all code to be formatted using
+```bash
+autopep8 <file> --max-line-length 120
+```
+
+## Frontend
+Frontend is pre-built and served by FastAPI from the `app/dashboard/build` directory. To rebuild the frontend, first make sure you have the necessary dependencies installed by running `npm install` in the `app/dashboard` directory. Then, simply remove the `app/dashboard/build` directory and run the Python code again, and it will rebuild the frontend automatically.
+
+### Components Library
+Frontend uses `Chakra-UI` as the component library, so please adhere to the Chakra-UI approach when contributing. Strive to create components that are cohesive and serve a single purpose. Keep in mind that readability and maintainability are more important than brevity, so prioritize those factors when writing your code.
+
+## Marzban CLI
+Marzban CLI is built using [Typer](https://typer.tiangolo.com/), and its commands' code can be found in `cli` directory. Its documentation is generated using [Typer CLI](https://typer.tiangolo.com/typer-cli/) which can be re-generated by navigating to project's root directory and running the following command:
+
+```bash
+$ PYTHONPATH=$(pwd) typer marzban-cli.py utils docs --name "marzban-cli" --output ./cli/README.md
+```
+
+## Debug Mode
+To run the project in debug mode with auto-reload, you can set the environment variable `DEBUG` to `true`. then by running the `main.py`, the backend and frontend will run separately on different ports.
+
+Note that you must first install the necessary npm packages by running npm install inside the app/dashboard directory before running in debug mode.
+```bash
+cd app/dashboard
+npm install
+cd ../..
+```
+
+If you run the project with debug mode off and delete the `app/dashboard/build` directory, the frontend will be rebuilt automatically on the next run. However, no rebuild will occur while inside debug mode."
+
diff --git a/Dockerfile b/Dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.10-slim
+
+ENV PYTHONUNBUFFERED 1
+
+WORKDIR /code
+
+RUN apt-get update \
+ && apt-get install -y curl unzip gcc python3-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN bash -c "$(curl -L https://github.com/Gozargah/Marzban-scripts/raw/master/install_latest_xray.sh)"
+
+COPY ./requirements.txt /code/
+RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
+
+COPY . /code
+
+RUN apt-get remove -y curl unzip gcc python3-dev
+
+RUN ln -s /code/marzban-cli.py /usr/bin/marzban-cli \
+ && chmod +x /usr/bin/marzban-cli \
+ && marzban-cli completion install --shell bash
+
+CMD ["bash", "-c", "alembic upgrade head; python main.py"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 21 January 2023
+
+ Copyright (C) 2023 Gozargah organization.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.
diff --git a/README-fa.md b/README-fa.md
@@ -0,0 +1,370 @@
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer">
+ <picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-dark.png">
+ <img width="160" height="160" src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-light.png">
+ </picture>
+ </a>
+</p>
+
+<h1 align="center"/>مرزبان</h1>
+
+<p align="center">
+ راه حل یکپارچه برای مدیریت پروتکل های مختلف. قدرت گرفته از <a href="https://github.com/XTLS/Xray-core">Xray</a>
+</p>
+
+<br/>
+<p align="center">
+ <a href="#">
+ <img src="https://img.shields.io/github/actions/workflow/status/gozargah/marzban/build.yml?style=flat-square" />
+ </a>
+ <a href="https://hub.docker.com/r/gozargah/marzban" target="_blank">
+ <img src="https://img.shields.io/docker/pulls/gozargah/marzban?style=flat-square&logo=docker" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/license/gozargah/marzban?style=flat-square" />
+ </a>
+ <a href="https://t.me/gozargah_marzban" target="_blank">
+ <img src="https://img.shields.io/badge/telegram-group-blue?style=flat-square&logo=telegram" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/badge/twitter-commiunity-blue?style=flat-square&logo=twitter" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/stars/gozargah/marzban?style=social" />
+ </a>
+</p>
+
+<p align="center">
+ <a href="./README.md">
+ English
+ </a>
+ /
+ <a href="./README-fa.md">
+ فارسی
+ </a>
+ /
+ <a href="./README-zh-cn.md">
+ 简体中文
+ </a>
+ /
+ <a href="./README-ru.md">
+ Русский
+ </a>
+</p>
+
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer" >
+ <img src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/preview.png" alt="Elk screenshots" width="600" height="auto">
+ </a>
+</p>
+
+
+## فهرست مطالب
+- [بررسی اجمالی](#بررسی-اجمالی)
+ - [چرا مرزبان؟](#چرا-مرزبان)
+ - [امکانات](#امکانات)
+- [راهنمای نصب](#راهنمای-نصب)
+- [تنظیمات](#تنظیمات)
+- [استفاده از API](#استفاده-از-api)
+- [پشتیبان گیری از مرزبان](#پشتیبان-گیری-از-مرزبان)
+- [ربات تلگرام](#ربات-تلگرام)
+- [رابط خط فرمان (CLI) مرزبان](#رابط-خط-فرمان-cli-مرزبان)
+- [ارسال اعلانها به آدرس وبهوک](#ارسال-اعلانها-به-آدرس-وبهوک)
+- [کمک مالی](#کمک-مالی)
+- [لایسنس](#لایسنس)
+- [مشارکت در توسعه](#مشارکت-در-توسعه)
+
+
+# بررسی اجمالی
+
+مرزبان یک نرم افزار (وب اپلیکیشن) مدیریت پروکسی است که امکان مدیریت چند صد حساب پروکسی را با قدرت و دسترسی بالا فراهم میکند. مرزبان از [Xray-core](https://github.com/XTLS/Xray-core) قدرت گرفته و با Python و React پیاده سازی شده است.
+
+## چرا مرزبان؟
+
+مرزبان دارای یک رابط کاربری ساده است که قابلیت های زیادی دارد. مرزبان امکان ایجاد چند نوع پروکسی برای کاربر ها را فراهم میکند بدون اینکه به تنظیمات پیچیده ای نیاز داشته باشید. به کمک رابط کاربری تحت وب مرزبان، شما میتوانید کاربران را مانیتور، ویرایش و در صورت نیاز، محدود کنید.
+
+### امکانات
+
+- **رابط کاربری تحت وب** آماده
+- به صورت **REST API** پیاده سازی شده
+- پشتیبانی از پروتکل های **Vmess**, **VLESS**, **Trojan** و **Shadowsocks**
+- امکان فعالسازی **چندین پروتکل** برای هر یوزر
+- امکان ساخت **چندین کاربر** بر روی یک inbound
+- پشتیبانی از **چندین inbound** بر روی **یک port** (به کمک fallbacks)
+- محدودیت بر اساس مصرف **ترافیک** و **تاریخ انقضا**
+- محدودیت **ترافیک دوره ای** (به عنوان مثال روزانه، هفتگی و غیره)
+- پشتیبانی از **Subscription link** سازگار با **V2ray** _(مثل نرم افزار های V2RayNG, OneClick, Nekoray و...)_ و **Clash**
+- ساخت **لینک اشتراک گذاری** و **QRcode** به صورت خودکار
+- مانیتورینگ منابع سرور و **مصرف ترافیک**
+- پشتیبانی از تنظیمات xray
+- پشتیبانی از **TLS**
+- **ربات تلگرام**
+- **رابط خط فرمان (CLI)** داخلی
+- قابلیت ایجاد **چندین مدیر** (تکمیل نشده است)
+
+# راهنمای نصب
+
+برای نصب کافیه دستور زیر رو اجرا کنید
+
+```bash
+sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install
+```
+
+وقتی نصب تمام شد:
+
+- شما لاگ های مرزبان رو مشاهده میکنید که میتوانید با بستن ترمینال یا فشار دادن `Ctrl+C` از آن خارج شوید
+- فایل های مرزبان در پوشه `/opt/marzban` قرار میگیرند
+- فایل تنظیمات در مسیر `/opt/marzban/.env` قرار میگیرد ([تنظیمات](#تنظیمات) را مشاهده کنید)
+- فایل های مهم (اطلاعات) مرزبان در مسیر `/usr/lib/marzban` قرار میگیرند
+- شما از طریق آدرس `http://YOUR_SERVER_IP:8000/dashboard/` میتوانید وارد داشبورد مرزبان شوید (YOUR_SERVER_IP را با آیپی سرور خود عوض کنید)
+
+در مرحله بعد, باید یک ادمین سودو بسازید
+
+```bash
+marzban cli admin create --sudo
+```
+
+تمام! حالا با این اطلاعات میتوانید وارد مرزبان شوید
+
+برای مشاهده راهنمای اسکریپت مرزبان دستور زیر را اجرا کنید
+
+```bash
+marzban --help
+```
+
+اگر مشتاق هستید که مرزبان رو با پایتون و به صورت دستی اجرا کنید، مراحل زیر را مشاهده کنید
+<details markdown="1">
+<summary><h3>نصب به صورت دستی (پیچیده)</h3></summary>
+
+لطفا xray را نصب کنید.
+شما میتواند به کمک [Xray-install](https://github.com/XTLS/Xray-install) این کار را انجام دهید.
+
+```bash
+bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
+```
+
+پروژه را clone کنید و dependency ها را نصب کنید. دقت کنید که نسخه پایتون شما Python>=3.8 باشد.
+
+```bash
+git clone https://github.com/Gozargah/Marzban.git
+cd Marzban
+wget -qO- https://bootstrap.pypa.io/get-pip.py | python3 -
+python3 -m pip install -r requirements.txt
+```
+
+همچنین میتواند از , [Python Virtualenv](https://pypi.org/project/virtualenv/) هم استفاده کنید.
+
+سپس کامند زیر را اجرا کنید تا دیتابیس تنظیم شود.
+
+```bash
+alembic upgrade head
+```
+
+اگر می خواهید از `marzban-cli` استفاده کنید، باید آن را به یک فایل در `$PATH` خود لینک و قابل اجرا (executable) کنید. سپس تکمیل خودکار (auto-completion) آن را نصب کنید:
+
+```bash
+sudo ln -s $(pwd)/marzban-cli.py /usr/bin/marzban-cli
+sudo chmod +x /usr/bin/marzban-cli
+marzban-cli completion install
+```
+
+حالا یک کپی از `.env.example` با نام `.env` بسازید و با یک ادیتور آن را باز کنید و تنظیمات دلخواه خود را انجام دهید. یه عنوان مثال نام کاربری و رمز عبور را می توانید در این فایل تغییر دهید.
+
+```bash
+cp .env.example .env
+nano .env
+```
+
+> برای اطلاعات بیشتر بخش [تنظیمات](#تنظیمات) را مطالعه کنید.
+
+در انتها, مرزبان را به کمک دستور زیر اجرا کنید.
+
+```bash
+python3 main.py
+```
+
+اجرا با استفاده از systemctl در لینوکس
+```
+systemctl enable /var/lib/marzban/marzban.service
+systemctl start marzban
+```
+
+اجرا با nginx
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location ~* /(dashboard|api|docs|redoc|openapi.json) {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+or
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name marzban.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location / {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+به صورت پیشفرض مرزبان در آدرس `http://localhost:8000/dashboard` اجرا میشود. شما میتوانید با تغییر `UVICORN_HOST` و `UVICORN_PORT`، هاست و پورت را تغییر دهید.
+</details>
+
+# تنظیمات
+
+> متغیر های زیر در فایل `env` یا `.env` استفاده میشوند. شما می توانید با تعریف و تغییر آن ها، تنظیمات مرزبان را تغییر دهید.
+
+| توضیحات | متغیر |
+| ----------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------: |
+| نام کاربری مدیر کل | SUDO_USERNAME |
+| رمز عبور مدیر کل | SUDO_PASSWORD |
+| آدرس دیتابیس ([بر اساس مستندات SQLAlchemy](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)) | SQLALCHEMY_DATABASE_URL |
+| آدرس هاستی که مرزبان روی آن اجرا میشود (پیشفرض: `0.0.0.0`) | UVICORN_HOST |
+| پورتی که مرزبان روی آن اجرا میشود (پیشفرض: `8000`) | UVICORN_PORT |
+| اجرای مرزبان بر روی یک Unix domain socket | UVICORN_UDS |
+| آدرس گواهی SSL به جهت ایمن کردن پنل مرزبان | UVICORN_SSL_CERTFILE |
+| آدرس کلید گواهی SSL | UVICORN_SSL_KEYFILE |
+| مسیر فایل json تنظیمات xray (پیشفرض: `xray_config.json`) | XRAY_JSON |
+| مسیر باینری xray (پیشفرض: `/usr/local/bin/xray`) | XRAY_EXECUTABLE_PATH |
+| مسیر asset های xray (پیشفرض: `/usr/local/share/xray`) | XRAY_ASSETS_PATH |
+| پیشوند (یا هاست) آدرس های اشتراکی (زمانی کاربرد دارد که نیاز دارید دامنه subscription link ها با دامنه پنل متفاوت باشد) | XRAY_SUBSCRIPTION_URL_PREFIX |
+| تگ inboundای که به عنوان fallback استفاده میشود. | XRAY_FALLBACKS_INBOUND_TAG |
+| تگ های inbound ای که لازم نیست در کانفیگ های ساخته شده وجود داشته باشند. | XRAY_EXCLUDE_INBOUND_TAGS |
+| آدرس محل template های شخصی سازی شده کاربر | CUSTOM_TEMPLATES_DIRECTORY |
+| تمپلیت مورد استفاده برای تولید کانفیگ های Clash (پیشفرض: `clash/default.yml`) | CLASH_SUBSCRIPTION_TEMPLATE |
+| تمپلیت صفحه اطلاعات اشتراک کاربر (پیشفرض `subscription/index.html`) | SUBSCRIPTION_PAGE_TEMPLATE |
+| تمپلیت صفحه اول (پیشفرض: `home/index.html`) | HOME_PAGE_TEMPLATE |
+| توکن ربات تلگرام (دریافت از [@botfather](https://t.me/botfather)) | TELEGRAM_API_TOKEN |
+| آیدی عددی ادمین در تلگرام (دریافت از [@userinfobot](https://t.me/userinfobot)) | TELEGRAM_ADMIN_ID |
+| اجرای ربات از طریق پروکسی | TELEGRAM_PROXY_URL |
+| مدت زمان انقضا توکن دسترسی به پنل مرزبان, `0` به معنای بدون تاریخ انقضا است (پیشفرض: `1440`) | JWT_ACCESS_TOKEN_EXPIRE_MINUTES |
+| فعال سازی داکیومنتیشن به آدرس `/docs` و `/redoc`(پیشفرض: `False`) | DOCS |
+| فعالسازی حالت توسعه (development) (پیشفرض: `False`) | DEBUG |
+| WEBHOOK_ADDRESS | آدرس webhook که تغییرات حالت یک کاربر به آن ارسال میشوند. اگر این متغیر مقدار داشته باشد، ارسال پیامها انجام میشوند. |
+| WEBHOOK_SECRET | متغیری که به عنوان `x-webhook-secret` در header ارسال میشود. (پیشفرض: `None`) |
+| تعداد دفعاتی که برای ارسال یک پیام، در صورت تشخیص خطا در ارسال تلاش دوباره شود (پیشفرض `3`) | NUMBER_OF_RECURRENT_NOTIFICATIONS |
+| مدت زمان بین هر ارسال دوباره پیام در صورت تشخیص خطا در ارسال به ثانیه (پیشفرض: `180`) | RECURRENT_NOTIFICATIONS_TIMEOUT |
+| هنگام رسیدن مصرف کاربر به چه درصدی پیام اخطار به آدرس وبهوک ارسال شود (پیشفرض: `80`) | NOTIFY_REACHED_USAGE_PERCENT |
+| چند روز مانده به انتهای سرویس پیام اخطار به آدرس وبهوک ارسال شود (پیشفرض: `3`) | NOTIFY_DAYS_LEFT |
+
+
+# استفاده از API
+مرزبان به توسعه دهندگانAPI REST ارائه می دهد. برای مشاهده اسناد API در قالب Swagger UI یا ReDoc، متغیر `DOCS=True` را در تنظیمات خود ست کنید و در مرورگر به مسیر `/docs` و `/redoc` بروید.
+
+
+# پشتیبان گیری از مرزبان
+بهتر است همیشه از فایل های مرزبان خود نسخه پشتیبان تهیه کنید تا در صورت خرابی سیستم یا حذف تصادفی اطلاعات از دست نروند. مراحل تهیه نسخه پشتیبان از مرزبان به شرح زیر است:
+
+1. به طور پیش فرض، تمام فایل های مهم مرزبان در `/var/lib/marzban` ذخیره می شوند (در نسخه داکر). کل پوشه `/var/lib/marzban` را در یک مکان پشتیبان مورد نظر خود، مانند هارد دیسک خارجی یا فضای ذخیره سازی ابری کپی کنید.
+2. علاوه بر این، مطمئن شوید که از فایل env خود که حاوی متغیرهای تنظیمات شما است و همچنین فایل پیکربندی Xray خود نسخه پشتیبان تهیه کنید.
+
+با انجام این مراحل، می توانید اطمینان حاصل کنید که از تمام فایل ها و داده های مرزبان خود یک نسخه پشتیبان تهیه کرده اید. به خاطر داشته باشید که نسخه های پشتیبان خود را به طور مرتب به روز کنید تا آنها را به روز نگه دارید.
+
+
+# ربات تلگرام
+مرزبان دارای یک ربات تلگرام داخلی است که می تواند مدیریت سرور، ایجاد و حذف کاربر و ارسال نوتیفیکیشن را انجام دهد. این ربات را می توان با انجام چند مرحله ساده به راحتی فعال کرد
+
+برای فعال کردن ربات تلگرام:
+
+1. در تنظیمات، متغیر`TELEGRAM_API_TOKEN` را به API TOKEN ربات تلگرام خود تنظیم کنید.
+2. همینطور، متغیر`TELEGRAM_ADMIN_ID` را به شناسه عددی حساب تلگرام خود تنظیم کنید. شما میتوانید شناسه خود را از [@userinfobot](https://t.me/userinfobot) دریافت کنید.
+
+
+# رابط خط فرمان (CLI) مرزبان
+مرزبان دارای یک رابط خط فرمان (Command Line Interface / CLI) داخلی است که به مدیران اجازه می دهد با مرزبان ارتباط مستقیم داشته باشند.
+
+اگر از Docker برای مرزبان استفاده می کنید، بهتر است از دستور های `docker exec` یا `docker-compose exec` استفاده کنید تا به پوسته (shell) تعاملی کانتینر مرزبان دسترسی پیدا کنید.
+
+برای مثال، به پوشه ی `docker-compose.yml` مرزبان بروید و دستور زیر را اجرا کنید:
+
+```bash
+$ sudo docker-compose exec -it marzban bash
+```
+
+رابط خط فرمان (CLI) مرزبان از طریق دستور `marzban-cli` هرکجا در دسترس خواهد بود!
+
+برای کسب اطلاعات بیشتر می توانید [مستندات CLI مرزبان](./cli/README.md) را مطالعه کنید.
+
+
+# ارسال اعلانها به آدرس وبهوک
+شما میتوانید آدرسی را برای مرزبان فراهم کنید تا تغییرات کاربران را به صورت اعلان برای شما ارسال کند.
+
+اعلانها به صورت یک درخواست POST به آدرسی که در `WEBHOOK_ADDRESS` فراهم شده به همراه مقدار تعیین شده در `WEBHOOK_SECRET` به عنوان `x-webhook-secret` در header درخواست ارسال میشوند.
+
+نمونهای از درخواست ارسال شده توسط مرزبان:
+
+```
+Headers:
+Host: 0.0.0.0:9000
+User-Agent: python-requests/2.28.1
+Accept-Encoding: gzip, deflate
+Accept: */*
+Connection: keep-alive
+x-webhook-secret: something-very-very-secret
+Content-Length: 107
+Content-Type: application/json
+
+
+
+Body:
+{"username": "marzban_test_user", "action": "user_updated", "enqueued_at": 1680506457.636369, "tries": 0}
+```
+
+انواع مختلف actionهایی که مرزبان ارسال میکند: `user_created`, `user_updated`, `user_deleted`, `user_limited`, `user_expired`, `user_disabled`, `user_enabled`
+
+
+# کمک مالی
+اگر مرزبان را برای شما مفید بوده و میخواهید از توسعه آن حمایت کنید، میتوانید در یکی از طریق یکی از شبکه های کریپتو زیر کمک مالی کنید:
+
+- شبکه TRON (TRX): `TX8kJoDcowQPBFTYHAJR36GyoUKP1Xwzkb`
+- شبکه ETH، BNB، MATIC: `0xFdc9ad32454FA4fc4733270FCc12ddBFb68b83F7`
+- شبکه بیت کوین: `bc1qpys2nefgsjjgae3g3gqy9crsv3h3rm96tlkz0v`
+- شبکه Dogecoin: `DJAocBAu8y6LwhDKUktLAyzV8xyoFeHH6R`
+- شبکه TON: `EQAVf-7hAXHlF-jmrKE44oBwN7HGQFVBLAtrOsev5K4qR4P8`
+
+
+از حمایت شما متشکرم!
+
+
+# لایسنس
+
+توسعه یافته شده در [ناشناس!] و منتشر شده تحت لایسنس [AGPL-3.0](./LICENSE).
+
+
+# مشارکت در توسعه
+این ❤️🔥 تقدیم به همهی کسایی که در توسعه مرزبان مشارکت میکنند! اگر میخواهید مشارکت داشته باشید، لطفاً [دستورالعملهای مشارکت](CONTRIBUTING.md) ما را بررسی کنید و در صورت تمایل Pull Request ارسال کنید یا یک Issue باز کنید. همچنین از شما برای پیوستن به گروه [تلگرام](https://t.me/gozargah_marzban) ما برای حمایت یا کمک به راهنمایی استقبال می کنیم.
+
+لطفا اگر امکانش رو دارید، با بررسی [لیست کار ها](https://github.com/gozargah/marzban/issues) به ما در بهبود مرزبان کمک کنید. کمک های شما با آغوش باز پذیرفته میشه.
+
+<p align="center">
+با تشکر از همه همکارانی که به بهبود مرزبان کمک کردند:
+</p>
+<p align="center">
+<a href="https://github.com/Gozargah/Marzban/graphs/contributors">
+ <img src="https://contrib.rocks/image?repo=Gozargah/Marzban" />
+</a>
+</p>
+<p align="center">
+ ساخته شده با <a rel="noopener noreferrer" target="_blank" href="https://contrib.rocks">contrib.rocks</a>
+</p>
+
diff --git a/README-ru.md b/README-ru.md
@@ -0,0 +1,397 @@
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer">
+ <picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-dark.png">
+ <img width="160" height="160" src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-light.png">
+ </picture>
+ </a>
+</p>
+
+<h1 align="center"/>Marzban</h1>
+
+<p align="center">
+ Унифицированное решение с графическим интерфейсом, устойчивое к цензуре, на базе <a href="https://github.com/XTLS/Xray-core">Xray</a>
+</p>
+
+<br/>
+<p align="center">
+ <a href="#">
+ <img src="https://img.shields.io/github/actions/workflow/status/gozargah/marzban/build.yml?style=flat-square" />
+ </a>
+ <a href="https://hub.docker.com/r/gozargah/marzban" target="_blank">
+ <img src="https://img.shields.io/docker/pulls/gozargah/marzban?style=flat-square&logo=docker" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/license/gozargah/marzban?style=flat-square" />
+ </a>
+ <a href="https://t.me/gozargah_marzban" target="_blank">
+ <img src="https://img.shields.io/badge/telegram-group-blue?style=flat-square&logo=telegram" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/badge/twitter-commiunity-blue?style=flat-square&logo=twitter" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/stars/gozargah/marzban?style=social" />
+ </a>
+</p>
+
+<p align="center">
+ <a href="./README.md">
+ English
+ </a>
+ /
+ <a href="./README-fa.md">
+ فارسی
+ </a>
+ /
+ <a href="./README-zh-cn.md">
+ 简体中文
+ </a>
+ /
+ <a href="./README-ru.md">
+ Русский
+ </a>
+</p>
+
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer" >
+ <img src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/preview.png" alt="Marzban screenshots" width="600" height="auto">
+ </a>
+</p>
+
+## Оглавление
+
+- [Введение](#введение)
+ - [Почему Marzban](#почему-marzban)
+ - [Функции](#функции)
+- [Руководство по установке](#руководство-по-установке)
+- [Конфигурация](#конфигурация)
+- [API](#api)
+- [Backup](#backup)
+- [Telegram бот](#telegram-bot)
+- [Marzban CLI](#marzban-cli)
+- [Marzban Node](#marzban-node)
+- [Webhook уведомления](#webhook-уведомления)
+- [Поддержка](#поддержка)
+- [Лицензия](#лицензия)
+- [Участники](#участники)
+
+# Введение
+
+Marzban (Персидское слово "Пограничник" - произносится /mærz'ban/) — это инструмент управления прокси-серверами, который предоставляет простой и удобный пользовательский интерфейс для управления сотнями учетных записей прокси на базе [Xray-core](https://github.com/XTLS/Xray-core) и созданный с использованием Python и ReactJS.
+
+## Почему Marzban?
+
+Marzban удобен в использовании, многофункционален и надежен. Он позволяет создавать различные прокси для пользователей без сложной настройки. С помощью встроенного веб-интерфейса можно контролировать, изменять и ограничивать пользователей.
+
+### Функции
+
+- Готовый **Web UI**
+- **REST API** бэкэнд
+- Поддержка [**множества узлов**](#marzban-node) (для распределения инфраструктуры и масштабируемости)
+- Поддержка протоколов **Vmess**, **VLESS**, **Trojan** и **Shadowsocks**
+- Возможность активации **нескольких протоколов** для каждого пользователя
+- **Несколько пользователей** на одном inbound
+- **Несколько inbound** на **одном порту** (поддержка fallbacks)
+- Ограничения на основе **количества трафика** и **срока действия**
+- Ограничение трафика по **периодам** (например выдавать трафик на день, неделю и т. д.)
+- Поддержка **ссылок-подписок** совместимых с **V2ray** _(такие как V2RayNG, OneClick, Nekoray, и др.)_, **Clash** и **ClashMeta**
+- Автоматическая генерация **Ссылок** и **QRcode**
+- Мониторинг ресурсов сервера и **использования трафика**
+- Настраиваемые конфигурации xray
+- Поддержка **TLS** и **REALITY**
+- Встроенный **Telegram Bot**
+- Встроенный **Command Line Interface (CLI)**
+- **Несколько языков**
+- Поддержка **Нескольких администраторов** (WIP)
+
+# Руководство по установке
+
+Выполните быструю установку с помощью следующей команды:
+
+```bash
+sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install
+```
+
+Когда установка будет завершена:
+- Вы увидите логи, которые можно остановить, нажав `Ctrl+C` или закрыв терминал.
+- Файлы Marzban будут размещены по адресу `/opt/marzban`.
+- Файл конфигурации будет размещен по адресу `/opt/marzban/.env` (см. [Конфигурация](#конфигурация)).
+- Файлы с данными будут размещены по адресу `/var/lib/marzban`.
+- Вы можете получить доступ к панели управления, введя в адресной строке `http://YOUR_SERVER_IP:8000/dashboard/` (заменив YOUR_SERVER_IP на актуальный IP адрес вашего сервера).
+
+Далее, Вам нужно создать главного администратора для входа в панель управления Marzban, выполнив следующую команду:
+
+```bash
+marzban cli admin create --sudo
+```
+
+Готово! Теперь Вы можете войти, используя данные своей учетной записи.
+
+Для того, чтобы увидеть справочное сообщение от скрипта Marzban, выполните команду:
+
+```bash
+marzban --help
+```
+
+Если Вы хотите запустить проект, используя его исходный код, обратитесь к разделу ниже
+<details markdown="1">
+<summary><h3>Ручная установка</h3></summary>
+
+Установите xray на Ваш сервер.
+
+Вы можете сделать это, используя [Xray-install](https://github.com/XTLS/Xray-install):
+
+```bash
+bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
+```
+
+Клонируйте этот проект и установите зависимости (Вам нужен Python >= 3.8):
+
+```bash
+git clone https://github.com/Gozargah/Marzban.git
+cd Marzban
+wget -qO- https://bootstrap.pypa.io/get-pip.py | python3 -
+python3 -m pip install -r requirements.txt
+```
+
+В качестве альтернативы для создания виртуальной среды можно использовать [Python Virtualenv](https://pypi.org/project/virtualenv/).
+
+Затем выполните следующую команду для запуска скрипта миграции базы данных:
+
+```bash
+alembic upgrade head
+```
+
+Если Вы хотите использовать `marzban-cli`, необходимо связать его с файлом в `$PATH`, сделать его исполняемым и установить:
+
+```bash
+sudo ln -s $(pwd)/marzban-cli.py /usr/bin/marzban-cli
+sudo chmod +x /usr/bin/marzban-cli
+marzban-cli completion install
+```
+
+Теперь настало время настройки.
+
+Создайте копию файла `.env.example`, посмотрите его и отредактируйте с помощью текстового редактора,например `nano`.
+
+Возможно, вам захочется изменить учетные данные администратора.
+
+```bash
+cp .env.example .env
+nano .env
+```
+
+> Проверьте раздел [Конфигурации](#конфигурация) для получения большей информации.
+
+В завершение запустите приложение с помощью следующей команды:
+
+```bash
+python3 main.py
+```
+
+Для запуска с помощью linux systemctl (скопируйте файл marzban.service в `/var/lib/marzban/marzban.service`):
+
+```
+systemctl enable /var/lib/marzban/marzban.service
+systemctl start marzban
+```
+
+Для использования с nginx:
+
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location ~* /(dashboard|api|docs|redoc|openapi.json) {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # xray-core ws-path: /
+ # client ws-path: /marzban/me/2087
+ #
+ # All traffic is proxed through port 443, and send to the xray port(2087, 2088 etc.).
+ # The '/marzban' in location regex path can changed any characters by yourself.
+ #
+ # /${path}/${username}/${xray-port}
+ location ~* /marzban/.+/(.+)$ {
+ proxy_redirect off;
+ proxy_pass http://127.0.0.1:$1/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+или:
+
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name marzban.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location / {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+По умолчанию приложение будет запускаться на `http://localhost:8000/dashboard`. Вы можете настроить его, изменив переменные окружения `UVICORN_HOST` и `UVICORN_PORT`.
+</details>
+
+# Конфигурация
+
+> Ниже приведены настройки, которые можно задать с помощью переменных окружения поместив их в файл `.env`.
+
+| Перменная | Описание |
+| --------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| SUDO_USERNAME | Имя пользователя главного администратора |
+| SUDO_PASSWORD | Пароль главного администратора |
+| SQLALCHEMY_DATABASE_URL | Путь к файлу БД ([SQLAlchemy's docs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)) |
+| UVICORN_HOST | Привязка приложения к хосту (по умолчанию: `0.0.0.0`) |
+| UVICORN_PORT | Привязка приложения к порту (по умолчанию: `8000`) |
+| UVICORN_UDS | Привязка приложения к UNIX domain socket |
+| UVICORN_SSL_CERTFILE | Адрес файла сертификата SSL |
+| UVICORN_SSL_KEYFILE | Адрес файла ключа SSL |
+| XRAY_JSON | Адрес файла JSON конфигурации Xray. (по умолчанию: `xray_config.json`) |
+| XRAY_EXECUTABLE_PATH | Путь к бинарникам Xray (по умолчанию: `/usr/local/bin/xray`) |
+| XRAY_ASSETS_PATH | Путь к папке с рессурсными файлами для Xray (файлы geoip.dat и geosite.dat) (по умолчанию: `/usr/local/share/xray`) |
+| XRAY_SUBSCRIPTION_URL_PREFIX | Префикс адреса подписки |
+| XRAY_FALLBACKS_INBOUND_TAG | Если вы используете входящее соединение с несколькими резервными вариантами, укажите здесь его тег
+| XRAY_EXCLUDE_INBOUND_TAGS | Теги входящих соединений, которые не требуют управления и не должны быть включены в список прокси
+| CUSTOM_TEMPLATES_DIRECTORY | Путь к папке с пользовательскими шаблонами (по умолчанию: `app/templates`) |
+| CLASH_SUBSCRIPTION_TEMPLATE | Шаблон для создания конфигурации Clash (по умолчанию: `clash/default.yml`) |
+| SUBSCRIPTION_PAGE_TEMPLATE | Шаблон для страницы подписки (по умолчанию: `subscription/index.html`) |
+| HOME_PAGE_TEMPLATE | Шаблон главной страницы (по умолчанию: `home/index.html`) |
+| TELEGRAM_API_TOKEN | Токен Telegram-бота (полученный от [@botfather](https://t.me/botfather)) |
+| TELEGRAM_ADMIN_ID | Числовой идентификатор администратора в Telegram (полученный от [@userinfobot](https://t.me/userinfobot)) |
+| TELEGRAM_PROXY_URL | URL прокси для запуска Telegram-бота (если серверы Telegram заблокированы на вашем сервере). |
+| JWT_ACCESS_TOKEN_EXPIRE_MINUTES | Время истечения срока действия доступного токена в минутах, `0` означает "без истечения срока действия" (по умолчанию: `1440`) |
+| DOCS | Активация документации API по адресам `/docs` и `/redoc`. (по умолчанию: `False`) |
+| DEBUG | Активация режима разработки (development) (по умолчанию: `False`) |
+| WEBHOOK_ADDRESS | Адрес Webhook для отправки уведомлений. Уведомления Webhook будут отправляться, если это значение было установлено |
+| WEBHOOK_SECRET | Webhook secret будет передаваться с каждым запросом в виде `x-webhook-secret` в заголовке (по умолчанию: `None`) |
+| NUMBER_OF_RECURRENT_NOTIFICATIONS | Сколько раз повторять попытку отправки уведомления при обнаружении ошибки (по умолчанию: `3`) |
+| RECURRENT_NOTIFICATIONS_TIMEOUT | Тайм-аут между каждым повторным запросом при обнаружении ошибки в секундах (по умолчанию: `180`) |
+| NOTIFY_REACHED_USAGE_PERCENT | При каком проценте использования отправлять предупреждение (по умолчанию: `80`) |
+| NOTIFY_DAYS_LEFT | Когда отправлять предупреждение об истечении срока действия (по умолчанию: `3`) |
+
+# API
+
+Marzban предоставляет REST API, позволяющий разработчикам программно взаимодействовать с сервисами Marzban. Для просмотра документации по API в Swagger UI или ReDoc установите переменную `DOCS=True` и перейдите по ссылкам `/docs` и `/redoc`.
+
+# Backup
+
+Всегда полезно регулярно создавать резервные копии файлов Marzban, чтобы предотвратить потерю данных в случае системных сбоев или случайного удаления. Ниже приведены шаги для создания резервной копии Marzban:
+
+1. По умолчанию все важные файлы Marzban сохраняются в папке `/var/lib/marzban` (в версиях Docker). Скопируйте весь каталог `/var/lib/marzban` в выбранное вами место резервного копирования, например на внешний жесткий диск или в облачное хранилище.
+2. Кроме того, не забудьте сделать резервную копию файла env, содержащего переменные конфигурации, а также файла конфигурации Xray. Если вы устанавливали Marzban с помощью marzban-scripts (рекомендуемый подход к установке), то env и другие конфигурации должны находиться в каталоге `/opt/marzban/`.
+
+Выполнив эти действия, вы сможете обеспечить резервное копирование всех файлов и данных Marzban, а также переменных конфигурации и конфигурации Xray на случай, если в будущем потребуется их восстановить. Не забывайте регулярно обновлять резервные копии, чтобы поддерживать их в актуальном состоянии.
+
+# Telegram Bot
+
+Marzban поставляется с встроенным ботом Telegram, который может управлять сервером, создавать и удалять пользователей, а также отправлять уведомления. Этот бот можно легко включить, выполнив несколько простых шагов, и он предоставляет удобный способ взаимодействия с Marzban без необходимости каждый раз заходить на сервер.
+
+Чтобы включить Telegram-бота, выполните следующие действия:
+
+1. установите `TELEGRAM_API_TOKEN` в качестве API-токена вашего бота.
+2. установите `TELEGRAM_ADMIN_ID` в качестве цифрового ID вашего Telegram-аккаунта, который вы можете получить от [@userinfobot](https://t.me/userinfobot)
+
+# Marzban CLI
+
+Marzban поставляется с встроенным CLI под названием `marzban-cli`, который позволяет администраторам напрямую взаимодействовать с ним.
+
+Если вы установили Marzban с помощью скрипта установки, то доступ к командам cli можно получить, выполнив команду:
+
+```bash
+marzban cli [OPTIONS] COMMAND [ARGS]...
+```
+
+Для получения дополнительной информации можно ознакомиться с [документацией по Marzban CLI](./cli/README.md).
+
+# Marzban Node
+
+Проект Marzban представляет [Marzban-node](https://github.com/gozargah/marzban-node), который помогает Вам в распределении инфраструктуры. С помощью Marzban-node можно распределить инфраструктуру по нескольким узлам, получив такие преимущества, как высокая доступность, масштабируемость и гибкость. Marzban-node позволяет пользователям подключаться к различным серверам, предоставляя им гибкость в выборе, а не ограничиваться только одним сервером.
+Более подробная информация и инструкции по установке приведены в [официальной документации Marzban-node](https://github.com/gozargah/marzban-node).
+
+
+# Webhook уведомления
+
+Вы можете задать адрес webhook, и Marzban будет отправлять уведомления на этот адрес.
+
+Запросы будут отправляться в виде POST-запроса на адрес, указанный в `WEBHOOK_ADDRESS`, с `WEBHOOK_SECRET` в качестве `x-webhook-secret` в заголовках.
+
+Пример запроса, отправленного Marzban:
+
+```
+Headers:
+Host: 0.0.0.0:9000
+User-Agent: python-requests/2.28.1
+Accept-Encoding: gzip, deflate
+Accept: */*
+Connection: keep-alive
+x-webhook-secret: something-very-very-secret
+Content-Length: 107
+Content-Type: application/json
+
+
+
+Body:
+{"username": "marzban_test_user", "action": "user_updated", "enqueued_at": 1680506457.636369, "tries": 0}
+```
+
+Различные типы действий: `user_created`, `user_updated`, `user_deleted`, `user_limited`, `user_expired`, `user_disabled`, `user_enabled`
+
+# Поддержка
+
+Если вы нашли Marzban полезным и хотели бы поддержать его развитие, вы можете сделать пожертвование в одной из следующих криптовалютных сетей:
+
+- TRON network (TRC20): `TX8kJoDcowQPBFTYHAJR36GyoUKP1Xwzkb`
+- ETH, BNB, MATIC network (ERC20, BEP20): `0xFdc9ad32454FA4fc4733270FCc12ddBFb68b83F7`
+- Bitcoin network: `bc1qpys2nefgsjjgae3g3gqy9crsv3h3rm96tlkz0v`
+- Dogecoin network: `DJAocBAu8y6LwhDKUktLAyzV8xyoFeHH6R`
+- TON network: `EQAVf-7hAXHlF-jmrKE44oBwN7HGQFVBLAtrOsev5K4qR4P8`
+
+Спасибо за поддержку!
+
+# Лицензия
+
+Сделано в [Unknown!] и опубликовано под [AGPL-3.0](./LICENSE).
+
+# Участники
+
+Мы ❤️🔥 участников проекта! Если вы хотите внести свой вклад, пожалуйста, ознакомьтесь с нашим [Contributing Guidelines](CONTRIBUTING.md) и не стесняйтесь отправлять запросы на исправление ошибок или сообщить о проблеме. Мы также приглашаем вас присоединиться к нашей группе [Telegram](https://t.me/gozargah_marzban) для получения поддержки.
+
+Проверьте [open issues](https://github.com/gozargah/marzban/issues), чтобы помочь развитию этого проекта.
+
+<p align="center">
+Спасибо всем участникам, благодаря которым Marzban становится лучше:
+</p>
+<p align="center">
+<a href="https://github.com/Gozargah/Marzban/graphs/contributors">
+ <img src="https://contrib.rocks/image?repo=Gozargah/Marzban" />
+</a>
+</p>
+<p align="center">
+ Made with <a rel="noopener noreferrer" target="_blank" href="https://contrib.rocks">contrib.rocks</a>
+</p>
diff --git a/README-zh-cn.md b/README-zh-cn.md
@@ -0,0 +1,340 @@
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer">
+ <picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-dark.png">
+ <img width="160" height="160" src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-light.png">
+ </picture>
+ </a>
+</p>
+
+<h1 align="center"/>Marzban</h1>
+
+<p align="center">
+ Unified GUI Censorship Resistant Solution Powered by <a href="https://github.com/XTLS/Xray-core">Xray</a>
+</p>
+
+<br/>
+<p align="center">
+ <a href="#">
+ <img src="https://img.shields.io/github/actions/workflow/status/gozargah/marzban/build.yml?style=flat-square" />
+ </a>
+ <a href="https://hub.docker.com/r/gozargah/marzban" target="_blank">
+ <img src="https://img.shields.io/docker/pulls/gozargah/marzban?style=flat-square&logo=docker" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/license/gozargah/marzban?style=flat-square" />
+ </a>
+ <a href="https://t.me/gozargah_marzban" target="_blank">
+ <img src="https://img.shields.io/badge/telegram-group-blue?style=flat-square&logo=telegram" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/badge/twitter-commiunity-blue?style=flat-square&logo=twitter" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/stars/gozargah/marzban?style=social" />
+ </a>
+</p>
+
+<p align="center">
+ <a href="./README.md">
+ English
+ </a>
+ /
+ <a href="./README-fa.md">
+ فارسی
+ </a>
+ /
+ <a href="./README-zh-cn.md">
+ 简体中文
+ </a>
+ /
+ <a href="./README-ru.md">
+ Русский
+ </a>
+</p>
+
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer" >
+ <img src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/preview.png" alt="Marzban screenshots" width="600" height="auto">
+ </a>
+</p>
+
+
+## 目录
+- [概览](#概览)
+ - [为什么要使用 Marzban?](#为什么要使用-marzban)
+ - [特性](#特性)
+- [安装指南](#安装指南)
+- [配置](#配置)
+- [如何使用 API](#如何使用-api)
+- [如何备份 Marzban](#如何备份-marzban)
+- [Telegram bot](#telegram-bot)
+- [捐赠](#捐赠)
+- [许可](#许可)
+- [贡献者](#贡献者)
+
+
+# 概览
+
+Marzban(Marzban一词源自波斯语,意为“边境警卫”,发音为 /mærz'ban/)是一个代理管理工具,提供简单易用的用户界面,可管理数百个代理账户,由 [Xray-core](https://github.com/XTLS/Xray-core) 提供支持,使用 Python 和 Reactjs 构建。
+
+
+
+## 为什么要使用 Marzban?
+
+Marzban 是一个用户友好、功能丰富且可靠的工具。它让您可以为用户创建不同的代理,无需进行任何复杂的配置。通过其内置的 Web 界面,您可以监视、修改和限制用户。
+
+### 特性
+
+- 内置 **Web 界面**
+- 完全支持 **REST API** 的后端
+- 支持 **Vmess**、**VLESS**、**Trojan** 和 **Shadowsocks** 协议
+- 单用户的**多协议**支持
+- 单入站的**多用户**支持
+- 单端口的**多入站**支持(使用 fallbacks)
+- **流量**和**过期日期**限制
+- 周期性的流量限制(例如每天、每周等)
+- 兼容 **V2ray** 的**订阅链接**(例如 V2RayNG、OneClick、Nekoray 等)和 **Clash**
+- 自动化的**分享链接**和**二维码**生成器
+- 系统监控和**流量统计**
+- 可自定义的 xray 配置
+- **TLS** 支持
+- 集成的 **Telegram Bot**
+- **多管理员**支持(WIP)
+
+
+# 安装指南
+Run the following command
+
+```bash
+sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install
+```
+
+Once the installation is complete:
+
+- You will see the logs that you can stop watching them by closing the terminal or pressing `Ctrl+C`
+- The Marzban files will be located at `/opt/marzban`
+- The configuration file can be found at `/opt/marzban/.env` (refer to [configurations](#configuration) section to see variables)
+- The data files will be placed at `/usr/lib/marzban`
+- You can access the Marzban dashboard by opening a web browser and navigating to `http://YOUR_SERVER_IP:8000/dashboard/` (replace YOUR_SERVER_IP with the actual IP address of your server)
+
+Next, you need to create a sudo admin for logging into the Marzban dashboard by the following command
+
+```bash
+marzban cli admin create --sudo
+```
+
+That's it! You can login to your dashboard using these credentials
+
+To see the help message of the Marzban script, run the following command
+
+```bash
+marzban --help
+```
+
+If you are eager to run the project using the source code, check the section below
+<details markdown="1">
+<summary><h3>手动安装(高级)</h3></summary>
+
+在您的机器上安装 xray
+
+您可以使用 [Xray-install](https://github.com/XTLS/Xray-install) 脚本进行安装:
+
+```bash
+bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
+```
+
+克隆项目并安装依赖项。
+
+您需要 Python>=3.8 版本。
+
+```bash
+git clone https://github.com/Gozargah/Marzban.git
+cd Marzban
+wget -qO- https://bootstrap.pypa.io/get-pip.py | python3 -
+python3 -m pip install -r requirements.txt
+```
+
+另外,为了拥有一个隔离的环境,您可以使用 [Python Virtualenv](https://pypi.org/project/virtualenv/)。
+
+然后运行以下命令运行数据库迁移脚本:
+
+```bash
+alembic upgrade head
+```
+
+现在开始配置:
+
+复制 `.env.example` 文件,查看并使用文本编辑器(如`nano`)进行编辑。
+
+您可能想要修改管理员凭据。
+
+```bash
+cp .env.example .env
+nano .env
+```
+
+> 请查看[配置](#配置)部分以获取更多信息。
+
+最终,使用以下命令启动应用程序:
+
+```bash
+python3 main.py
+```
+
+也可使用 linux systemctl 启动:
+```
+systemctl enable /var/lib/marzban/marzban.service
+systemctl start marzban
+```
+
+配合 nginx 使用:
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location ~* /(dashboard|api|docs|redoc|openapi.json) {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # xray-core ws-path: /
+ # client ws-path: /marzban/me/2087
+ #
+ # 所有流量通过 443 端口进行代理,然后分发至真正的 xray 端口(2087、2088 等等)。
+ # 路径中的 “/marzban” 可以改为任意合法 URL 字符.
+ #
+ # /${path}/${username}/${xray-port}
+ location ~* /marzban/.+/(.+)$ {
+ proxy_redirect off;
+ proxy_pass http://127.0.0.1:$1/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+或
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name marzban.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location / {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+默认情况下,应用将在 `http://localhost:8000/dashboard` 上运行。您可以通过更改 `UVICORN_HOST` 和 `UVICORN_PORT` 环境变量来进行配置。
+</details>
+
+# 配置
+
+> 您可以使用环境变量或将其放置在 `env` 或 `.env` 文件中来设置以下设置。
+
+| 变量 | 描述 |
+| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| SUDO_USERNAME | 管理员用户名(默认: admin) |
+| SUDO_PASSWORD | 管理员密码(默认: admin) |
+| SQLALCHEMY_DATABASE_URL | 数据库文档([SQLAlchemy's docs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)) |
+| UVICORN_HOST | 绑定应用程序到此主机(默认为 `0.0.0.0`) |
+| UVICORN_PORT | 绑定应用程序到此端口(默认为 `8000`) |
+| UVICORN_UDS | 将应用程序绑定到一个 UNIX 域套接字 |
+| UVICORN_SSL_CERTFILE | SSL 证书文件路径 |
+| UVICORN_SSL_KEYFILE | SSL 密钥文件路径 |
+| XRAY_JSON | Xray 的 json 配置文件路径(默认: `xray_config.json`) |
+| XRAY_EXECUTABLE_PATH | Xray 的执行程序路径: `/usr/local/bin/xray`) |
+| XRAY_ASSETS_PATH | Xray 的资源目录: `/usr/local/share/xray`) |
+| XRAY_SUBSCRIPTION_URL_PREFIX | 订阅URL的前缀 |
+| XRAY_FALLBACKS_INBOUND_TAG | 包含 fallbacks 的入站标记, 在您需要使用 fallbacks 配置此项 |
+| XRAY_EXCLUDE_INBOUND_TAGS | 不需要此应用程序管理或在链接中包含的入站标记 |
+| CLASH_SUBSCRIPTION_TEMPLATE | 将用于生成冲突配置的模板(默认值:`clash/default.yml`) |
+| SUBSCRIPTION_PAGE_TEMPLATE | 用于生成订阅信息页面的模板(默认:`subscription/index.html`) |
+| HOME_PAGE_TEMPLATE | 诱饵页面模板(默认:`home/index.html`) |
+| TELEGRAM_API_TOKEN | Telegram bot API 令牌(可以从 [@botfather](https://t.me/botfather) 获取) |
+| TELEGRAM_ADMIN_ID | 管理员的 Telegram ID(可以使用 [@userinfobot](https://t.me/userinfobot) 查找您的 ID) |
+| TELEGRAM_PROXY_URL | 在代理下运行 Telegram bot。 |
+| JWT_ACCESS_TOKEN_EXPIRE_MINUTES | Access Tokens 的过期时间,以分钟为单位,`0` 表示无限期(默认为 `1440` 分钟) |
+| DOCS | API 文档是否应该在 `/docs` 和 `/redoc` 上提供(默认为 `False` |
+| DEBUG | 开发者调试模式(默认: `False`) |
+
+
+# 如何使用 API
+Marzban 提供了 REST API,使开发人员能够以编程方式与 Marzban 服务进行交互。要在 Swagger UI 或 ReDoc 中查看 API 文档,设置配置变量 `DOCS=True`,然后导航到 `/docs` 和 `/redoc`。
+
+
+# 如何备份 Marzban
+
+定期备份 Marzban 文件是预防系统故障或意外删除导致数据丢失的好习惯。以下是备份 Marzban 的步骤:
+
+1. 默认情况下,所有重要的 Marzban 文件都保存在 `/var/lib/marzban` ( Docker 版本)中。将整个 `/var/lib/marzban` 目录复制到您选择的备份位置,比如外部硬盘或云存储。
+2. 此外,请确保备份您的 `env` 文件,其中包含您的配置变量,以及您的 `Xray` 配置文件。
+
+按照这些步骤,您可以确保有备份所有 Marzban 文件和数据,以及您的配置变量和 Xray 配置,以备将来恢复使用。请记得定期更新备份,以保持它们的最新性。
+
+
+# Telegram bot
+
+Marzban 配备了一个集成的 Telegram bot,可以处理服务器管理、用户创建和删除,以及发送通知。通过几个简单的步骤,您可以轻松地启用这个机器人,并提供了一种方便的方式与 Marzban 交互,而不需要每次都登录到服务器上。
+
+启用 Telegram bot:
+
+1. 将 `TELEGRAM_API_TOKEN` 设置为您的 bot API Token。
+2. 将 `TELEGRAM_ADMIN_ID` 设置为您的 Telegram ID,您可以从 [@userinfobot](https://t.me/userinfobot) 中获取自己的 ID。
+
+
+# 捐赠
+
+如果您认为 Marzban 有用,并想支持其发展,可以在以下加密网络之一进行捐赠:
+
+- TRON(TRX) 网络:`TX8kJoDcowQPBFTYHAJR36GyoUKP1Xwzkb`
+- ETH、BNB、MATIC 网络:`0xFdc9ad32454FA4fc4733270FCc12ddBFb68b83F7`
+- 比特币网络:`bc1qpys2nefgsjjgae3g3gqy9crsv3h3rm96tlkz0v`
+- Dogecoin 网络:`DJAocBAu8y6LwhDKUktLAyzV8xyoFeHH6R`
+- TON 网络:`EQAVf-7hAXHlF-jmrKE44oBwN7HGQFVBLAtrOsev5K4qR4P8`
+
+
+感谢您的支持!
+
+# 许可
+
+制作于 [Unknown!] 并在 [AGPL-3.0](./LICENSE) 下发布。
+
+# 贡献者
+
+我们热爱贡献者!如果您想做出贡献,请查看我们的[贡献指南](CONTRIBUTING.md)并随时提交拉取请求或打开问题。我们也欢迎您加入我们的 [Telegram](https://t.me/gozargah_marzban) 群组,以获得支持或贡献指导。
+
+查看 [issues](https://github.com/gozargah/marzban/issues) 以帮助改进这个项目。
+
+
+
+<p align="center">
+感谢所有为改善 Marzban 做出贡献的贡献者们:
+</p>
+<p align="center">
+<a href="https://github.com/Gozargah/Marzban/graphs/contributors">
+ <img src="https://contrib.rocks/image?repo=Gozargah/Marzban" />
+</a>
+</p>
+<p align="center">
+ Made with <a rel="noopener noreferrer" target="_blank" href="https://contrib.rocks">contrib.rocks</a>
+</p>
+
diff --git a/README.md b/README.md
@@ -0,0 +1,397 @@
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer">
+ <picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-dark.png">
+ <img width="160" height="160" src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/logo-light.png">
+ </picture>
+ </a>
+</p>
+
+<h1 align="center"/>Marzban</h1>
+
+<p align="center">
+ Unified GUI Censorship Resistant Solution Powered by <a href="https://github.com/XTLS/Xray-core">Xray</a>
+</p>
+
+<br/>
+<p align="center">
+ <a href="#">
+ <img src="https://img.shields.io/github/actions/workflow/status/gozargah/marzban/build.yml?style=flat-square" />
+ </a>
+ <a href="https://hub.docker.com/r/gozargah/marzban" target="_blank">
+ <img src="https://img.shields.io/docker/pulls/gozargah/marzban?style=flat-square&logo=docker" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/license/gozargah/marzban?style=flat-square" />
+ </a>
+ <a href="https://t.me/gozargah_marzban" target="_blank">
+ <img src="https://img.shields.io/badge/telegram-group-blue?style=flat-square&logo=telegram" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/badge/twitter-commiunity-blue?style=flat-square&logo=twitter" />
+ </a>
+ <a href="#">
+ <img src="https://img.shields.io/github/stars/gozargah/marzban?style=social" />
+ </a>
+</p>
+
+<p align="center">
+ <a href="./README.md">
+ English
+ </a>
+ /
+ <a href="./README-fa.md">
+ فارسی
+ </a>
+ /
+ <a href="./README-zh-cn.md">
+ 简体中文
+ </a>
+ /
+ <a href="./README-ru.md">
+ Русский
+ </a>
+</p>
+
+<p align="center">
+ <a href="https://github.com/gozargah/marzban" target="_blank" rel="noopener noreferrer" >
+ <img src="https://github.com/Gozargah/Marzban-docs/raw/master/screenshots/preview.png" alt="Marzban screenshots" width="600" height="auto">
+ </a>
+</p>
+
+## Table of Contents
+
+- [Overview](#overview)
+ - [Why using Marzban?](#why-using-marzban)
+ - [Features](#features)
+- [Installation guide](#installation-guide)
+- [Configuration](#configuration)
+- [API](#api)
+- [Backup](#backup)
+- [Telegram Bot](#telegram-bot)
+- [Marzban CLI](#marzban-cli)
+- [Marzban Node](#marzban-node)
+- [Webhook notifications](#webhook-notifications)
+- [Donation](#donation)
+- [License](#license)
+- [Contributors](#contributors)
+
+# Overview
+
+Marzban (the Persian word for "border guard" - pronounced /mærz'ban/) is a proxy management tool that provides a simple and easy-to-use user interface for managing hundreds of proxy accounts powered by [Xray-core](https://github.com/XTLS/Xray-core) and built using Python and Reactjs.
+
+## Why using Marzban?
+
+Marzban is user-friendly, feature-rich and reliable. It lets you to create different proxies for your users without any complicated configuration. Using its built-in web UI, you are able to monitor, modify and limit users.
+
+### Features
+
+- Built-in **Web UI**
+- Fully **REST API** backend
+- [**Multiple Nodes**](#marzban-node) support (for infrastructure distribution & scalability)
+- Supports protocols **Vmess**, **VLESS**, **Trojan** and **Shadowsocks**
+- **Multi-protocol** for a single user
+- **Multi-user** on a single inbound
+- **Multi-inbound** on a **single port** (fallbacks support)
+- **Traffic** and **expiry date** limitations
+- **Periodic** traffic limit (e.g. daily, weekly, etc.)
+- **Subscription link** compatible with **V2ray** _(such as V2RayNG, OneClick, Nekoray, etc.)_, **Clash** and **ClashMeta**
+- Automated **Share link** and **QRcode** generator
+- System monitoring and **traffic statistics**
+- Customizable xray configuration
+- **TLS** and **REALITY** support
+- Integrated **Telegram Bot**
+- Integrated **Command Line Interface (CLI)**
+- **Multi-language**
+- **Multi-admin** support (WIP)
+
+# Installation guide
+
+Run the following command
+
+```bash
+sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install
+```
+
+Once the installation is complete:
+
+- You will see the logs that you can stop watching them by closing the terminal or pressing `Ctrl+C`
+- The Marzban files will be located at `/opt/marzban`
+- The configuration file can be found at `/opt/marzban/.env` (refer to [configurations](#configuration) section to see variables)
+- The data files will be placed at `/var/lib/marzban`
+- You can access the Marzban dashboard by opening a web browser and navigating to `http://YOUR_SERVER_IP:8000/dashboard/` (replace YOUR_SERVER_IP with the actual IP address of your server)
+
+Next, you need to create a sudo admin for logging into the Marzban dashboard by the following command
+
+```bash
+marzban cli admin create --sudo
+```
+
+That's it! You can login to your dashboard using these credentials
+
+To see the help message of the Marzban script, run the following command
+
+```bash
+marzban --help
+```
+
+If you are eager to run the project using the source code, check the section below
+<details markdown="1">
+<summary><h3>Manual install (advanced)</h3></summary>
+
+Install xray on your machine
+
+You can install it using [Xray-install](https://github.com/XTLS/Xray-install)
+
+```bash
+bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
+```
+
+Clone this project and install the dependencies (you need Python >= 3.8)
+
+```bash
+git clone https://github.com/Gozargah/Marzban.git
+cd Marzban
+wget -qO- https://bootstrap.pypa.io/get-pip.py | python3 -
+python3 -m pip install -r requirements.txt
+```
+
+Alternatively, to have an isolated environment you can use [Python Virtualenv](https://pypi.org/project/virtualenv/)
+
+Then run the following command to run the database migration scripts
+
+```bash
+alembic upgrade head
+```
+
+If you want to use `marzban-cli`, you should link it to a file in your `$PATH`, make it executable, and install the auto-completion:
+
+```bash
+sudo ln -s $(pwd)/marzban-cli.py /usr/bin/marzban-cli
+sudo chmod +x /usr/bin/marzban-cli
+marzban-cli completion install
+```
+
+Now it's time to configuration
+
+Make a copy of `.env.example` file, take a look and edit it using a text editor like `nano`.
+
+You probably like to modify the admin credentials.
+
+```bash
+cp .env.example .env
+nano .env
+```
+
+> Check [configurations](#configuration) section for more information
+
+Eventually, launch the application using command below
+
+```bash
+python3 main.py
+```
+
+To launch with linux systemctl (copy marzban.service file to `/var/lib/marzban/marzban.service`)
+
+```
+systemctl enable /var/lib/marzban/marzban.service
+systemctl start marzban
+```
+
+To use with nginx
+
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location ~* /(dashboard|api|docs|redoc|openapi.json) {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # xray-core ws-path: /
+ # client ws-path: /marzban/me/2087
+ #
+ # All traffic is proxed through port 443, and send to the xray port(2087, 2088 etc.).
+ # The '/marzban' in location regex path can changed any characters by yourself.
+ #
+ # /${path}/${username}/${xray-port}
+ location ~* /marzban/.+/(.+)$ {
+ proxy_redirect off;
+ proxy_pass http://127.0.0.1:$1/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+or
+
+```
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name marzban.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
+
+ location / {
+ proxy_pass http://0.0.0.0:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
+```
+
+By default the app will be run on `http://localhost:8000/dashboard`. You can configure it using changing the `UVICORN_HOST` and `UVICORN_PORT` environment variables.
+</details>
+
+# Configuration
+
+> You can set settings below using environment variables or placing them in `.env` file.
+
+| Variable | Description |
+| --------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| SUDO_USERNAME | Superuser's username |
+| SUDO_PASSWORD | Superuser's password |
+| SQLALCHEMY_DATABASE_URL | Database URL ([SQLAlchemy's docs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)) |
+| UVICORN_HOST | Bind application to this host (default: `0.0.0.0`) |
+| UVICORN_PORT | Bind application to this port (default: `8000`) |
+| UVICORN_UDS | Bind application to a UNIX domain socket |
+| UVICORN_SSL_CERTFILE | SSL certificate file to have application on https |
+| UVICORN_SSL_KEYFILE | SSL key file to have application on https |
+| XRAY_JSON | Path of Xray's json config file (default: `xray_config.json`) |
+| XRAY_EXECUTABLE_PATH | Path of Xray binary (default: `/usr/local/bin/xray`) |
+| XRAY_ASSETS_PATH | Path of Xray assets (default: `/usr/local/share/xray`) |
+| XRAY_SUBSCRIPTION_URL_PREFIX | Prefix of subscription URLs |
+| XRAY_FALLBACKS_INBOUND_TAG | Tag of the inbound that includes fallbacks, needed in the case you're using fallbacks |
+| XRAY_EXCLUDE_INBOUND_TAGS | Tags of the inbounds that shouldn't be managed and included in links by application |
+| CUSTOM_TEMPLATES_DIRECTORY | Customized templates directory (default: `app/templates`) |
+| CLASH_SUBSCRIPTION_TEMPLATE | The template that will be used for generating clash configs (default: `clash/default.yml`) |
+| SUBSCRIPTION_PAGE_TEMPLATE | The template used for generating subscription info page (default: `subscription/index.html`) |
+| HOME_PAGE_TEMPLATE | Decoy page template (default: `home/index.html`) |
+| TELEGRAM_API_TOKEN | Telegram bot API token (get token from [@botfather](https://t.me/botfather)) |
+| TELEGRAM_ADMIN_ID | Numeric Telegram ID of admin (use [@userinfobot](https://t.me/userinfobot) to found your ID) |
+| TELEGRAM_PROXY_URL | Run Telegram Bot over proxy |
+| JWT_ACCESS_TOKEN_EXPIRE_MINUTES | Expire time for the Access Tokens in minutes, `0` considered as infinite (default: `1440`) |
+| DOCS | Whether API documents should be available on `/docs` and `/redoc` or not (default: `False`) |
+| DEBUG | Debug mode for development (default: `False`) |
+| WEBHOOK_ADDRESS | Webhook address to send notifications to. Webhook notifications will be sent if this value was set. |
+| WEBHOOK_SECRET | Webhook secret will be sent with each request as `x-webhook-secret` in the header (default: `None`) |
+| NUMBER_OF_RECURRENT_NOTIFICATIONS | How many times to retry if an error detected in sending a notification (default: `3`) |
+| RECURRENT_NOTIFICATIONS_TIMEOUT | Timeout between each retry if an error detected in sending a notification in seconds (default: `180`) |
+| NOTIFY_REACHED_USAGE_PERCENT | At which percentage of usage to send the warning notification (default: `80`) |
+| NOTIFY_DAYS_LEFT | When to send warning notifaction about expiration (default: `3`) |
+
+# API
+
+Marzban provides a REST API that enables developers to interact with Marzban services programmatically. To view the API documentation in Swagger UI or ReDoc, set the configuration variable `DOCS=True` and navigate to the `/docs` and `/redoc`.
+
+# Backup
+
+It's always a good idea to backup your Marzban files regularly to prevent data loss in case of system failures or accidental deletion. Here are the steps to backup Marzban:
+
+1. By default, all Marzban important files are saved in `/var/lib/marzban` (Docker versions). Copy the entire `/var/lib/marzban` directory to a backup location of your choice, such as an external hard drive or cloud storage.
+2. Additionally, make sure to backup your env file, which contains your configuration variables, and also, your Xray config file. If you installed Marzban using marzban-scripts (recommended installation approach), the env and other configurations should be inside `/opt/marzban/` directory.
+
+By following these steps, you can ensure that you have a backup of all your Marzban files and data, as well as your configuration variables and Xray configuration, in case you need to restore them in the future. Remember to update your backups regularly to keep them up-to-date.
+
+# Telegram Bot
+
+Marzban comes with an integrated Telegram bot that can handle server management, user creation and removal, and send notifications. This bot can be easily enabled by following a few simple steps, and it provides a convenient way to interact with Marzban without having to log in to the server every time.
+
+To enable Telegram Bot:
+
+1. set `TELEGRAM_API_TOKEN` to your bot's API Token
+2. set `TELEGRAM_ADMIN_ID` to your Telegram account's numeric ID, you can get your ID from [@userinfobot](https://t.me/userinfobot)
+
+# Marzban CLI
+
+Marzban comes with an integrated CLI named `marzban-cli` which allows administrators to have direct interaction with it.
+
+If you've installed Marzban using easy install script, you can access the cli commands by running
+
+```bash
+marzban cli [OPTIONS] COMMAND [ARGS]...
+```
+
+For more information, You can read [Marzban CLI's documentation](./cli/README.md).
+
+# Marzban Node
+
+The Marzban project introduces the [Marzban-node](https://github.com/gozargah/marzban-node), which revolutionizes infrastructure distribution. With Marzban-node, you can distribute your infrastructure across multiple locations, unlocking benefits such as redundancy, high availability, scalability, flexibility. Marzban-node empowers users to connect to different servers, offering them the flexibility to choose and connect to multiple servers instead of being limited to only one server.
+For more detailed information and installation instructions, please refer to the [Marzban-node official documentation](https://github.com/gozargah/marzban-node)
+
+# Webhook notifications
+
+You can set a webhook address and Marzban will send the notifications to that address.
+
+the requests will be sent as a post request to the adress provided by `WEBHOOK_ADDRESS` with `WEBHOOK_SECRET` as `x-webhook-secret` in the headers.
+
+Example request sent from Marzban:
+
+```
+Headers:
+Host: 0.0.0.0:9000
+User-Agent: python-requests/2.28.1
+Accept-Encoding: gzip, deflate
+Accept: */*
+Connection: keep-alive
+x-webhook-secret: something-very-very-secret
+Content-Length: 107
+Content-Type: application/json
+
+
+
+Body:
+{"username": "marzban_test_user", "action": "user_updated", "enqueued_at": 1680506457.636369, "tries": 0}
+```
+
+Different action typs are: `user_created`, `user_updated`, `user_deleted`, `user_limited`, `user_expired`, `user_disabled`, `user_enabled`
+
+# Donation
+
+If you found Marzban useful and would like to support its development, you can make a donation in one of the following crypto networks:
+
+- TRON network (TRC20): `TX8kJoDcowQPBFTYHAJR36GyoUKP1Xwzkb`
+- ETH, BNB, MATIC network (ERC20, BEP20): `0xFdc9ad32454FA4fc4733270FCc12ddBFb68b83F7`
+- Bitcoin network: `bc1qpys2nefgsjjgae3g3gqy9crsv3h3rm96tlkz0v`
+- Dogecoin network: `DJAocBAu8y6LwhDKUktLAyzV8xyoFeHH6R`
+- TON network: `EQAVf-7hAXHlF-jmrKE44oBwN7HGQFVBLAtrOsev5K4qR4P8`
+
+Thank you for your support!
+
+# License
+
+Made in [Unknown!] and Published under [AGPL-3.0](./LICENSE).
+
+# Contributors
+
+We ❤️🔥 contributors! If you'd like to contribute, please check out our [Contributing Guidelines](CONTRIBUTING.md) and feel free to submit a pull request or open an issue. We also welcome you to join our [Telegram](https://t.me/gozargah_marzban) group for either support or contributing guidance.
+
+Check [open issues](https://github.com/gozargah/marzban/issues) to help the progress of this project.
+
+<p align="center">
+Thanks to the all contributors who have helped improve Marzban:
+</p>
+<p align="center">
+<a href="https://github.com/Gozargah/Marzban/graphs/contributors">
+ <img src="https://contrib.rocks/image?repo=Gozargah/Marzban" />
+</a>
+</p>
+<p align="center">
+ Made with <a rel="noopener noreferrer" target="_blank" href="https://contrib.rocks">contrib.rocks</a>
+</p>
diff --git a/alembic.ini b/alembic.ini
@@ -0,0 +1,99 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = app/db/migrations
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to app/db/migrations/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:app/db/migrations/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/app/__init__.py b/app/__init__.py
@@ -0,0 +1,70 @@
+import logging
+
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+from fastapi import FastAPI, Request, status
+from fastapi.encoders import jsonable_encoder
+from fastapi.exceptions import RequestValidationError
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from fastapi.routing import APIRoute
+from fastapi_responses import custom_openapi
+
+from config import DOCS, XRAY_SUBSCRIPTION_PATH
+
+__version__ = "0.4.2"
+
+
+app = FastAPI(
+ title="MarzbanAPI",
+ description="Unified GUI Censorship Resistant Solution Powered by Xray",
+ version=__version__,
+ docs_url='/docs' if DOCS else None,
+ redoc_url='/redoc' if DOCS else None
+)
+app.openapi = custom_openapi(app)
+scheduler = AsyncIOScheduler(timezone='UTC')
+logger = logging.getLogger('uvicorn.error')
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+from app import dashboard, views, jobs # , telegram # noqa
+
+
+def use_route_names_as_operation_ids(app: FastAPI) -> None:
+ for route in app.routes:
+ if isinstance(route, APIRoute):
+ route.operation_id = route.name
+
+
+use_route_names_as_operation_ids(app)
+
+
+@app.on_event("startup")
+def on_startup():
+ paths = [f"{r.path}/" for r in app.routes]
+ paths.append("/api/")
+ if f"/{XRAY_SUBSCRIPTION_PATH}/" in paths:
+ raise ValueError(f"you can't use /{XRAY_SUBSCRIPTION_PATH}/ as subscription path it reserved for {app.title}")
+ scheduler.start()
+
+
+@app.on_event("shutdown")
+def on_shutdown():
+ scheduler.shutdown()
+
+
+@app.exception_handler(RequestValidationError)
+def validation_exception_handler(request: Request, exc: RequestValidationError):
+ details = {}
+ for error in exc.errors():
+ details[error["loc"][-1]] = error.get("msg")
+ return JSONResponse(
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ content=jsonable_encoder({"detail": details}),
+ )
diff --git a/app/dashboard/.prettierrc.json b/app/dashboard/.prettierrc.json
@@ -0,0 +1,3 @@
+{
+ "printWidth": 80
+}
diff --git a/app/dashboard/README.md b/app/dashboard/README.md
@@ -0,0 +1,40 @@
+# Dashboard UI for marzban
+
+## Requirements
+
+For development, you will only need Node.js installed on your environement.
+
+### Node
+
+[Node](http://nodejs.org/) is really easy to install & now include [NPM](https://npmjs.org/).
+This project has been developed on the Nodejs v16.17.0 so if you faced any issue during installation that may related to the node version, install Node with version >= v16.17.0.
+
+## Install
+
+ git clone https://github.com/gozargah/marz-manager.git
+ cd marz-manager
+ yarn install
+
+### Configure app
+
+Copy `example.env` to `.env` then set the backend api address:
+
+ VITE_BASE_API=https://somewhere.com/
+
+#### Environment variables
+
+| Name | Description |
+| ------------- | ------------------------------------------------------------------------------------ |
+| VITE_BASE_API | The api url of the deployed backend ([Marzban](https://github.com/gozargah/Marzban)) |
+
+## Start development server
+
+ yarn dev
+
+## Simple build for production
+
+ yarn build
+
+## Contribution
+
+Feel free to contribute. Go on and fork the project. After commiting the changes, make a PR. It means a lot to us.
diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py
@@ -0,0 +1,54 @@
+import atexit
+import os
+import subprocess
+from pathlib import Path
+
+from app import app
+from config import DEBUG, VITE_BASE_API
+from fastapi.staticfiles import StaticFiles
+
+path = '/dashboard/'
+base_dir = Path(__file__).parent
+build_dir = base_dir / 'build'
+
+
+def build():
+ proc = subprocess.Popen(
+ ['npm', 'run', 'build', '--', '--base', path, '--outDir', build_dir],
+ env={**os.environ, 'VITE_BASE_API': VITE_BASE_API},
+ cwd=base_dir
+ )
+ proc.wait()
+ with open(build_dir / 'index.html', 'r') as file:
+ html = file.read()
+ with open(build_dir / '404.html', 'w') as file:
+ file.write(html)
+
+
+def run_dev():
+ proc = subprocess.Popen(
+ ['npm', 'run', 'dev', '--', '--host', '0.0.0.0', '--base', path, '--clearScreen', 'false'],
+ env={**os.environ, 'VITE_BASE_API': VITE_BASE_API},
+ cwd=base_dir
+ )
+
+ atexit.register(proc.terminate)
+
+
+def run_build():
+ if not build_dir.is_dir():
+ build()
+
+ app.mount(
+ path,
+ StaticFiles(directory=build_dir, html=True),
+ name="dashboard"
+ )
+
+
+@app.on_event("startup")
+def startup():
+ if DEBUG is True:
+ run_dev()
+ else:
+ run_build()
diff --git a/app/dashboard/chakra.config.ts b/app/dashboard/chakra.config.ts
@@ -0,0 +1,168 @@
+import { extendTheme } from "@chakra-ui/react";
+export const theme = extendTheme({
+ shadows: { outline: "0 0 0 2px var(--chakra-colors-primary-200)" },
+ fonts: {
+ body: `Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif`,
+ },
+ colors: {
+ "light-border": "#d2d2d4",
+ primary: {
+ 50: "#9cb7f2",
+ 100: "#88a9ef",
+ 200: "#749aec",
+ 300: "#618ce9",
+ 400: "#4d7de7",
+ 500: "#396fe4",
+ 600: "#3364cd",
+ 700: "#2e59b6",
+ 800: "#284ea0",
+ 900: "#224389",
+ },
+ gray: {
+ 750: "#222C3B",
+ },
+ },
+ components: {
+ Alert: {
+ baseStyle: {
+ container: {
+ borderRadius: "6px",
+ fontSize: "sm",
+ },
+ },
+ },
+ Select: {
+ baseStyle: {
+ field: {
+ _dark: {
+ borderColor: "gray.600",
+ borderRadius: "6px",
+ },
+ _light: {
+ borderRadius: "6px",
+ },
+ },
+ },
+ },
+ FormHelperText: {
+ baseStyle: {
+ fontSize: "xs",
+ },
+ },
+ FormLabel: {
+ baseStyle: {
+ fontSize: "sm",
+ fontWeight: "medium",
+ mb: "1",
+ _dark: { color: "gray.300" },
+ },
+ },
+ Input: {
+ baseStyle: {
+ addon: {
+ _dark: {
+ borderColor: "gray.600",
+ _placeholder: {
+ color: "gray.500",
+ },
+ },
+ },
+ field: {
+ _focusVisible: {
+ boxShadow: "none",
+ borderColor: "primary.200",
+ outlineColor: "primary.200",
+ },
+ _dark: {
+ borderColor: "gray.600",
+ _disabled: {
+ color: "gray.400",
+ borderColor: "gray.500",
+ },
+ _placeholder: {
+ color: "gray.500",
+ },
+ },
+ },
+ },
+ },
+ Table: {
+ baseStyle: {
+ table: {
+ borderCollapse: "separate",
+ borderSpacing: 0,
+ },
+ thead: {
+ borderBottomColor: "light-border",
+ },
+ th: {
+ background: "#F9FAFB",
+ borderColor: "light-border !important",
+ borderBottomColor: "light-border !important",
+ borderTop: "1px solid ",
+ borderTopColor: "light-border !important",
+ _first: {
+ borderLeft: "1px solid",
+ borderColor: "light-border !important",
+ },
+ _last: {
+ borderRight: "1px solid",
+ borderColor: "light-border !important",
+ },
+ _dark: {
+ borderColor: "gray.600 !important",
+ background: "gray.750",
+ },
+ },
+ td: {
+ transition: "all .1s ease-out",
+ borderColor: "light-border",
+ borderBottomColor: "light-border !important",
+ _first: {
+ borderLeft: "1px solid",
+ borderColor: "light-border",
+ _dark: {
+ borderColor: "gray.600",
+ },
+ },
+ _last: {
+ borderRight: "1px solid",
+ borderColor: "light-border",
+ _dark: {
+ borderColor: "gray.600",
+ },
+ },
+ _dark: {
+ borderColor: "gray.600",
+ borderBottomColor: "gray.600 !important",
+ },
+ },
+ tr: {
+ "&.interactive": {
+ cursor: "pointer",
+ _hover: {
+ "& > td": {
+ bg: "gray.200",
+ },
+ _dark: {
+ "& > td": {
+ bg: "gray.750",
+ },
+ },
+ },
+ },
+ _last: {
+ "& > td": {
+ _first: {
+ borderBottomLeftRadius: "8px",
+ },
+ _last: {
+ borderBottomRightRadius: "8px",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+});
diff --git a/app/dashboard/example.env b/app/dashboard/example.env
@@ -0,0 +1 @@
+VITE_BASE_API=
diff --git a/app/dashboard/index.html b/app/dashboard/index.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Marzban</title>
+ <link
+ rel="apple-touch-icon"
+ sizes="180x180"
+ href="/favicon/apple-touch-icon.png"
+ />
+ <link
+ rel="icon"
+ type="image/png"
+ sizes="32x32"
+ href="/favicon/favicon-32x32.png"
+ />
+ <link
+ rel="icon"
+ type="image/png"
+ sizes="16x16"
+ href="/favicon/favicon-16x16.png"
+ />
+ <link rel="manifest" href="/favicon/site.webmanifest" />
+ <link
+ rel="mask-icon"
+ href="/favicon/safari-pinned-tab.svg"
+ color="#5bbad5"
+ />
+ <link rel="shortcut icon" href="/favicon/favicon.ico" />
+ <meta name="apple-mobile-web-app-title" content="Marzban" />
+ <meta name="application-name" content="Marzban" />
+ <meta name="msapplication-TileColor" content="#2b5797" />
+ <meta name="msapplication-config" content="/favicon/browserconfig.xml" />
+ <meta name="theme-color" content="#3B81F6" />
+ </head>
+ <body>
+ <div id="root"></div>
+ <script type="module" src="/src/index.tsx"></script>
+ </body>
+</html>
diff --git a/app/dashboard/package-lock.json b/app/dashboard/package-lock.json
@@ -0,0 +1,9758 @@
+{
+ "name": "marzban-dashboard",
+ "version": "0.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "marzban-dashboard",
+ "version": "0.0.0",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@chakra-ui/react": "^2.5.5",
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@heroicons/react": "^2.0.12",
+ "@hookform/resolvers": "^2.9.10",
+ "apexcharts": "^3.40.0",
+ "classnames": "^2.3.2",
+ "dayjs": "^1.11.6",
+ "framer-motion": "^7.6.6",
+ "i18next": "^22.4.14",
+ "i18next-browser-languagedetector": "^7.0.1",
+ "i18next-http-backend": "^2.2.0",
+ "jsoneditor": "^9.10.1",
+ "lodash.debounce": "^4.0.8",
+ "ohmyfetch": "^0.4.21",
+ "qrcode.react": "^3.1.0",
+ "react": "^18.2.0",
+ "react-apexcharts": "^1.4.0",
+ "react-copy-to-clipboard": "^5.1.0",
+ "react-datepicker": "^4.8.0",
+ "react-dom": "^18.2.0",
+ "react-github-btn": "^1.4.0",
+ "react-hook-form": "^7.39.1",
+ "react-i18next": "^12.2.0",
+ "react-json-editor-ajrm": "^2.5.14",
+ "react-loading-skeleton": "^3.1.0",
+ "react-query": "^3.39.3",
+ "react-router-dom": "^6.4.3",
+ "react-slick": "^0.29.0",
+ "react-use-websocket": "^4.3.1",
+ "rollup-plugin-visualizer": "^5.9.0",
+ "slick-carousel": "^1.8.1",
+ "vite-tsconfig-paths": "^4.0.3",
+ "zod": "^3.19.1",
+ "zustand": "^4.4.6",
+ "zustand-computed": "^1.3.3"
+ },
+ "devDependencies": {
+ "@chakra-ui/cli": "^2.3.0",
+ "@types/jsoneditor": "^9.9.0",
+ "@types/lodash.debounce": "^4.0.7",
+ "@types/react": "^18.0.17",
+ "@types/react-copy-to-clipboard": "^5.0.4",
+ "@types/react-datepicker": "^4.8.0",
+ "@types/react-dom": "^18.0.6",
+ "@types/react-json-editor-ajrm": "^2.5.3",
+ "@types/react-slick": "^0.23.10",
+ "@vitejs/plugin-react": "^2.1.0",
+ "autoprefixer": "^10.4.12",
+ "sass": "^1.57.1",
+ "typescript": "^4.9.5",
+ "vite": "^3.1.0",
+ "vite-plugin-svgr": "^2.2.2"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
+ "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.1.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "dependencies": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz",
+ "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz",
+ "integrity": "sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.21.0",
+ "@babel/helper-compilation-targets": "^7.20.7",
+ "@babel/helper-module-transforms": "^7.21.0",
+ "@babel/helpers": "^7.21.0",
+ "@babel/parser": "^7.21.0",
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.0",
+ "@babel/types": "^7.21.0",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.2",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz",
+ "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.23.3",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+ "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
+ "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.20.5",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.21.3",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.21.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz",
+ "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.20.2",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.2",
+ "@babel/types": "^7.21.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
+ "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
+ "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
+ "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz",
+ "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.0",
+ "@babel/types": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
+ "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
+ "dev": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz",
+ "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz",
+ "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-jsx": "^7.18.6",
+ "@babel/types": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-development": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz",
+ "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz",
+ "integrity": "sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.20.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.19.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz",
+ "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.19.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
+ "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz",
+ "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.3",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.3",
+ "@babel/types": "^7.23.3",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz",
+ "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@chakra-ui/accordion": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.1.11.tgz",
+ "integrity": "sha512-mfVPmqETp9pyRDHJ33AdF19oHv/LyxVzQJtlxUByuvs8Cj9QQZ2LQLg5kejm+b3mj03A7A6yfbuo3RNaI4Bhsg==",
+ "dependencies": {
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/alert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-2.1.0.tgz",
+ "integrity": "sha512-OcfHwoXI5VrmM+tHJTHT62Bx6TfyfCxSa0PWUOueJzSyhlUOKBND5we6UtrOB7D0jwX45qKKEDJOLG5yCG21jQ==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/spinner": "2.0.13"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/anatomy": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.1.2.tgz",
+ "integrity": "sha512-pKfOS/mztc4sUXHNc8ypJ1gPWSolWT770jrgVRfolVbYlki8y5Y+As996zMF6k5lewTu6j9DQequ7Cc9a69IVQ=="
+ },
+ "node_modules/@chakra-ui/avatar": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-2.2.8.tgz",
+ "integrity": "sha512-uBs9PMrqyK111tPIYIKnOM4n3mwgKqGpvYmtwBnnbQLTNLg4gtiWWVbpTuNMpyu1av0xQYomjUt8Doed8w6p8g==",
+ "dependencies": {
+ "@chakra-ui/image": "2.0.15",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/breadcrumb": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-2.1.5.tgz",
+ "integrity": "sha512-p3eQQrHQBkRB69xOmNyBJqEdfCrMt+e0eOH+Pm/DjFWfIVIbnIaFbmDCeWClqlLa21Ypc6h1hR9jEmvg8kmOog==",
+ "dependencies": {
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/breakpoint-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz",
+ "integrity": "sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "node_modules/@chakra-ui/button": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-2.0.18.tgz",
+ "integrity": "sha512-E3c99+lOm6ou4nQVOTLkG+IdOPMjsQK+Qe7VyP8A/xeAMFONuibrWPRPpprr4ZkB4kEoLMfNuyH2+aEza3ScUA==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/spinner": "2.0.13"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/card": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/card/-/card-2.1.6.tgz",
+ "integrity": "sha512-fFd/WAdRNVY/WOSQv4skpy0WeVhhI0f7dTY1Sm0jVl0KLmuP/GnpsWtKtqWjNcV00K963EXDyhlk6+9oxbP4gw==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/checkbox": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-2.2.14.tgz",
+ "integrity": "sha512-uqo6lFWLqYBujPglrvRhTAErtuIXpmdpc5w0W4bjK7kyvLhxOpUh1hlDb2WoqlNpfRn/OaNeF6VinPnf9BJL8w==",
+ "dependencies": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/visually-hidden": "2.0.15",
+ "@zag-js/focus-visible": "0.2.2"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/cli": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-2.4.1.tgz",
+ "integrity": "sha512-GZZuHUA1cXJWpmYNiVTLPihvY4VhIssRl+AXgw/0IbeodTMop3jWlIioPKLAQeXu5CwvRA6iESyGjnu1V8Zykg==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.3",
+ "cli-check-node": "^1.3.4",
+ "cli-handle-unhandled": "^1.1.1",
+ "cli-welcome": "^2.2.2",
+ "commander": "^9.3.0",
+ "esbuild": "^0.17.18",
+ "prettier": "^2.8.8"
+ },
+ "bin": {
+ "chakra-cli": "bin/index.js"
+ }
+ },
+ "node_modules/@chakra-ui/cli/node_modules/@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@chakra-ui/cli/node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@chakra-ui/cli/node_modules/esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/@chakra-ui/clickable": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-2.0.14.tgz",
+ "integrity": "sha512-jfsM1qaD74ZykLHmvmsKRhDyokLUxEfL8Il1VoZMNX5RBI0xW/56vKpLTFF/v/+vLPLS+Te2cZdD4+2O+G6ulA==",
+ "dependencies": {
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/close-button": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-2.0.17.tgz",
+ "integrity": "sha512-05YPXk456t1Xa3KpqTrvm+7smx+95dmaPiwjiBN3p7LHUQVHJd8ZXSDB0V+WKi419k3cVQeJUdU/azDO2f40sw==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.0.16"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/color-mode": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-2.1.12.tgz",
+ "integrity": "sha512-sYyfJGDoJSLYO+V2hxV9r033qhte5Nw/wAn5yRGGZnEEN1dKPEdWQ3XZvglWSDTNd0w9zkoH2w6vP4FBBYb/iw==",
+ "dependencies": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/control-box": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-2.0.13.tgz",
+ "integrity": "sha512-FEyrU4crxati80KUF/+1Z1CU3eZK6Sa0Yv7Z/ydtz9/tvGblXW9NFanoomXAOvcIFLbaLQPPATm9Gmpr7VG05A==",
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/counter": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-2.0.14.tgz",
+ "integrity": "sha512-KxcSRfUbb94dP77xTip2myoE7P2HQQN4V5fRJmNAGbzcyLciJ+aDylUU/UxgNcEjawUp6Q242NbWb1TSbKoqog==",
+ "dependencies": {
+ "@chakra-ui/number-utils": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/css-reset": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-2.1.1.tgz",
+ "integrity": "sha512-jwEOfIAWmQsnChHQTW/eRE+dfE4MjmhvSvoUug5nkV1pI7veC/20noFlIZxzi82EbiQI8Fs0+Jnusgxr2yaOHA==",
+ "peerDependencies": {
+ "@emotion/react": ">=10.0.35",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/descendant": {
+ "version": "3.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-3.0.14.tgz",
+ "integrity": "sha512-+Ahvp9H4HMpfScIv9w1vaecGz7qWAaK1YFHHolz/SIsGLaLGlbdp+5UNabQC7L6TUnzzJDQDxzwif78rTD7ang==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/dom-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/dom-utils/-/dom-utils-2.0.6.tgz",
+ "integrity": "sha512-PVtDkPrDD5b8aoL6Atg7SLjkwhWb7BwMcLOF1L449L3nZN+DAO3nyAh6iUhZVJyunELj9d0r65CDlnMREyJZmA=="
+ },
+ "node_modules/@chakra-ui/editable": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-2.0.21.tgz",
+ "integrity": "sha512-oYuXbHnggxSYJN7P9Pn0Scs9tPC91no4z1y58Oe+ILoJKZ+bFAEHtL7FEISDNJxw++MEukeFu7GU1hVqmdLsKQ==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-focus-on-pointer-down": "2.0.6",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/event-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz",
+ "integrity": "sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw=="
+ },
+ "node_modules/@chakra-ui/focus-lock": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-2.0.16.tgz",
+ "integrity": "sha512-UuAdGCPVrCa1lecoAvpOQD7JFT7a9RdmhKWhFt5ioIcekSLJcerdLHuuL3w0qz//8kd1/SOt7oP0aJqdAJQrCw==",
+ "dependencies": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "react-focus-lock": "^2.9.2"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/form-control": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-2.0.18.tgz",
+ "integrity": "sha512-I0a0jG01IAtRPccOXSNugyRdUAe8Dy40ctqedZvznMweOXzbMCF1m+sHPLdWeWC/VI13VoAispdPY0/zHOdjsQ==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/hooks": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-2.1.6.tgz",
+ "integrity": "sha512-oMSOeoOF6/UpwTVlDFHSROAA4hPY8WgJ0erdHs1ZkuwAwHv7UzjDkvrb6xYzAAH9qHoFzc5RIBm6jVoh3LCc+Q==",
+ "dependencies": {
+ "@chakra-ui/react-utils": "2.0.12",
+ "@chakra-ui/utils": "2.0.15",
+ "compute-scroll-into-view": "1.0.20",
+ "copy-to-clipboard": "3.3.3"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/icon": {
+ "version": "3.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-3.0.16.tgz",
+ "integrity": "sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/image": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.0.15.tgz",
+ "integrity": "sha512-w2rElXtI3FHXuGpMCsSklus+pO1Pl2LWDwsCGdpBQUvGFbnHfl7MftQgTlaGHeD5OS95Pxva39hKrA2VklKHiQ==",
+ "dependencies": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/input": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-2.0.21.tgz",
+ "integrity": "sha512-AIWjjg6MgcOtlvKmVoZfPPfgF+sBSWL3Zq2HSCAMvS6h7jfxz/Xv0UTFGPk5F4Wt0YHT7qMySg0Jsm0b78HZJg==",
+ "dependencies": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/layout": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-2.1.18.tgz",
+ "integrity": "sha512-F4Gh2e+DGdaWdWT5NZduIFD9NM7Bnuh8sXARFHWPvIu7yvAwZ3ddqC9GK4F3qUngdmkJxDLWQqRSwSh96Lxbhw==",
+ "dependencies": {
+ "@chakra-ui/breakpoint-utils": "2.0.8",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/lazy-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz",
+ "integrity": "sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg=="
+ },
+ "node_modules/@chakra-ui/live-region": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-2.0.13.tgz",
+ "integrity": "sha512-Ja+Slk6ZkxSA5oJzU2VuGU7TpZpbMb/4P4OUhIf2D30ctmIeXkxTWw1Bs1nGJAVtAPcGS5sKA+zb89i8g+0cTQ==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/media-query": {
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-3.2.12.tgz",
+ "integrity": "sha512-8pSLDf3oxxhFrhd40rs7vSeIBfvOmIKHA7DJlGUC/y+9irD24ZwgmCtFnn+y3gI47hTJsopbSX+wb8nr7XPswA==",
+ "dependencies": {
+ "@chakra-ui/breakpoint-utils": "2.0.8",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/menu": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-2.1.12.tgz",
+ "integrity": "sha512-ylNK1VJlr/3/EGg9dLPZ87cBJJjeiYXeU/gOAphsKXMnByrXWhbp4YVnyyyha2KZ0zEw0aPU4nCZ+A69aT9wrg==",
+ "dependencies": {
+ "@chakra-ui/clickable": "2.0.14",
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-animation-state": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-focus-effect": "2.0.9",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-outside-click": "2.0.7",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/modal": {
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-2.2.11.tgz",
+ "integrity": "sha512-2J0ZUV5tEzkPiawdkgPz6bmex7NXAde1VXooMwdvK+vuT8PV3U61yorTJOZVLdw7TjjI1Yo94mzsp6UwBud43Q==",
+ "dependencies": {
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/focus-lock": "2.0.16",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16",
+ "aria-hidden": "^1.2.2",
+ "react-remove-scroll": "^2.5.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/number-input": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-2.0.19.tgz",
+ "integrity": "sha512-HDaITvtMEqOauOrCPsARDxKD9PSHmhWywpcyCSOX0lMe4xx2aaGhU0QQFhsJsykj8Er6pytMv6t0KZksdDv3YA==",
+ "dependencies": {
+ "@chakra-ui/counter": "2.0.14",
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-interval": "2.0.5",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/number-utils": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz",
+ "integrity": "sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg=="
+ },
+ "node_modules/@chakra-ui/object-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/object-utils/-/object-utils-2.0.8.tgz",
+ "integrity": "sha512-2upjT2JgRuiupdrtBWklKBS6tqeGMA77Nh6Q0JaoQuH/8yq+15CGckqn3IUWkWoGI0Fg3bK9LDlbbD+9DLw95Q=="
+ },
+ "node_modules/@chakra-ui/pin-input": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-2.0.20.tgz",
+ "integrity": "sha512-IHVmerrtHN8F+jRB3W1HnMir1S1TUCWhI7qDInxqPtoRffHt6mzZgLZ0izx8p1fD4HkW4c1d4/ZLEz9uH9bBRg==",
+ "dependencies": {
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/popover": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-2.1.9.tgz",
+ "integrity": "sha512-OMJ12VVs9N32tFaZSOqikkKPtwAVwXYsES/D1pff/amBrE3ngCrpxJSIp4uvTdORfIYDojJqrR52ZplDKS9hRQ==",
+ "dependencies": {
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-animation-state": "2.0.8",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-focus-effect": "2.0.9",
+ "@chakra-ui/react-use-focus-on-pointer-down": "2.0.6",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/popper": {
+ "version": "3.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-3.0.13.tgz",
+ "integrity": "sha512-FwtmYz80Ju8oK3Z1HQfisUE7JIMmDsCQsRBu6XuJ3TFQnBHit73yjZmxKjuRJ4JgyT4WBnZoTF3ATbRKSagBeg==",
+ "dependencies": {
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@popperjs/core": "^2.9.3"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/portal": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-2.0.16.tgz",
+ "integrity": "sha512-bVID0qbQ0l4xq38LdqAN4EKD4/uFkDnXzFwOlviC9sl0dNhzICDb1ltuH/Adl1d2HTMqyN60O3GO58eHy7plnQ==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/progress": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-2.1.6.tgz",
+ "integrity": "sha512-hHh5Ysv4z6bK+j2GJbi/FT9CVyto2PtNUNwBmr3oNMVsoOUMoRjczfXvvYqp0EHr9PCpxqrq7sRwgQXUzhbDSw==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/provider": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-2.2.2.tgz",
+ "integrity": "sha512-UVwnIDnAWq1aKroN5AF+OpNpUqLVeIUk7tKvX3z4CY9FsPFFi6LTEhRHdhpwaU1Tau3Tf9agEu5URegpY7S8BA==",
+ "dependencies": {
+ "@chakra-ui/css-reset": "2.1.1",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/system": "2.5.5",
+ "@chakra-ui/utils": "2.0.15"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0",
+ "@emotion/styled": "^11.0.0",
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/radio": {
+ "version": "2.0.22",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-2.0.22.tgz",
+ "integrity": "sha512-GsQ5WAnLwivWl6gPk8P1x+tCcpVakCt5R5T0HumF7DGPXKdJbjS+RaFySrbETmyTJsKY4QrfXn+g8CWVrMjPjw==",
+ "dependencies": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@zag-js/focus-visible": "0.2.2"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.5.5.tgz",
+ "integrity": "sha512-aBVMUtdWv2MrptD/tKSqICPsuJ+I+jvauegffO1qPUDlK3RrXIDeOHkLGWohgXNcjY5bGVWguFEzJm97//0ooQ==",
+ "dependencies": {
+ "@chakra-ui/accordion": "2.1.11",
+ "@chakra-ui/alert": "2.1.0",
+ "@chakra-ui/avatar": "2.2.8",
+ "@chakra-ui/breadcrumb": "2.1.5",
+ "@chakra-ui/button": "2.0.18",
+ "@chakra-ui/card": "2.1.6",
+ "@chakra-ui/checkbox": "2.2.14",
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/control-box": "2.0.13",
+ "@chakra-ui/counter": "2.0.14",
+ "@chakra-ui/css-reset": "2.1.1",
+ "@chakra-ui/editable": "2.0.21",
+ "@chakra-ui/focus-lock": "2.0.16",
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/hooks": "2.1.6",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/image": "2.0.15",
+ "@chakra-ui/input": "2.0.21",
+ "@chakra-ui/layout": "2.1.18",
+ "@chakra-ui/live-region": "2.0.13",
+ "@chakra-ui/media-query": "3.2.12",
+ "@chakra-ui/menu": "2.1.12",
+ "@chakra-ui/modal": "2.2.11",
+ "@chakra-ui/number-input": "2.0.19",
+ "@chakra-ui/pin-input": "2.0.20",
+ "@chakra-ui/popover": "2.1.9",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/progress": "2.1.6",
+ "@chakra-ui/provider": "2.2.2",
+ "@chakra-ui/radio": "2.0.22",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/select": "2.0.19",
+ "@chakra-ui/skeleton": "2.0.24",
+ "@chakra-ui/slider": "2.0.23",
+ "@chakra-ui/spinner": "2.0.13",
+ "@chakra-ui/stat": "2.0.18",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/switch": "2.0.26",
+ "@chakra-ui/system": "2.5.5",
+ "@chakra-ui/table": "2.0.17",
+ "@chakra-ui/tabs": "2.1.9",
+ "@chakra-ui/tag": "3.0.0",
+ "@chakra-ui/textarea": "2.0.19",
+ "@chakra-ui/theme": "3.0.1",
+ "@chakra-ui/theme-utils": "2.0.15",
+ "@chakra-ui/toast": "6.1.1",
+ "@chakra-ui/tooltip": "2.2.7",
+ "@chakra-ui/transition": "2.0.16",
+ "@chakra-ui/utils": "2.0.15",
+ "@chakra-ui/visually-hidden": "2.0.15"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0",
+ "@emotion/styled": "^11.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-children-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz",
+ "integrity": "sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-context": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-context/-/react-context-2.0.8.tgz",
+ "integrity": "sha512-tRTKdn6lCTXM6WPjSokAAKCw2ioih7Eg8cNgaYRSwKBck8nkz9YqxgIIEj3dJD7MGtpl24S/SNI98iRWkRwR/A==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-env": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-3.0.0.tgz",
+ "integrity": "sha512-tfMRO2v508HQWAqSADFrwZgR9oU10qC97oV6zGbjHh9ALP0/IcFR+Bi71KRTveDTm85fMeAzZYGj57P3Dsipkw==",
+ "dependencies": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-types/-/react-types-2.0.7.tgz",
+ "integrity": "sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-animation-state": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.0.8.tgz",
+ "integrity": "sha512-xv9zSF2Rd1mHWQ+m5DLBWeh4atF8qrNvsOs3MNrvxKYBS3f79N3pqcQGrWAEvirXWXfiCeje2VAkEggqFRIo+Q==",
+ "dependencies": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "@chakra-ui/react-use-event-listener": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-callback-ref": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.0.7.tgz",
+ "integrity": "sha512-YjT76nTpfHAK5NxplAlZsQwNju5KmQExnqsWNPFeOR6vvbC34+iPSTr+r91i1Hdy7gBSbevsOsd5Wm6RN3GuMw==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-controllable-state": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.0.8.tgz",
+ "integrity": "sha512-F7rdCbLEmRjwwODqWZ3y+mKgSSHPcLQxeUygwk1BkZPXbKkJJKymOIjIynil2cbH7ku3hcSIWRvuhpCcfQWJ7Q==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-disclosure": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.0.8.tgz",
+ "integrity": "sha512-2ir/mHe1YND40e+FyLHnDsnDsBQPwzKDLzfe9GZri7y31oU83JSbHdlAXAhp3bpjohslwavtRCp+S/zRxfO9aQ==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-event-listener": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.0.7.tgz",
+ "integrity": "sha512-4wvpx4yudIO3B31pOrXuTHDErawmwiXnvAN7gLEOVREi16+YGNcFnRJ5X5nRrmB7j2MDUtsEDpRBFfw5Z9xQ5g==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-focus-effect": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.0.9.tgz",
+ "integrity": "sha512-20nfNkpbVwyb41q9wxp8c4jmVp6TUGAPE3uFTDpiGcIOyPW5aecQtPmTXPMJH+2aa8Nu1wyoT1btxO+UYiQM3g==",
+ "dependencies": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-focus-on-pointer-down": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.0.6.tgz",
+ "integrity": "sha512-OigXiLRVySn3tyVqJ/rn57WGuukW8TQe8fJYiLwXbcNyAMuYYounvRxvCy2b53sQ7QIZamza0N0jhirbH5FNoQ==",
+ "dependencies": {
+ "@chakra-ui/react-use-event-listener": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-interval": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-interval/-/react-use-interval-2.0.5.tgz",
+ "integrity": "sha512-1nbdwMi2K87V6p5f5AseOKif2CkldLaJlq1TOqaPRwb7v3aU9rltBtYdf+fIyuHSToNJUV6wd9budCFdLCl3Fg==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-latest-ref": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.0.5.tgz",
+ "integrity": "sha512-3mIuFzMyIo3Ok/D8uhV9voVg7KkrYVO/pwVvNPJOHsDQqCA6DpYE4WDsrIx+fVcwad3Ta7SupexR5PoI+kq6QQ==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-merge-refs": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.0.7.tgz",
+ "integrity": "sha512-zds4Uhsc+AMzdH8JDDkLVet9baUBgtOjPbhC5r3A0ZXjZvGhCztFAVE3aExYiVoMPoHLKbLcqvCWE6ioFKz1lw==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-outside-click": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.0.7.tgz",
+ "integrity": "sha512-MsAuGLkwYNxNJ5rb8lYNvXApXxYMnJ3MzqBpQj1kh5qP/+JSla9XMjE/P94ub4fSEttmNSqs43SmPPrmPuihsQ==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-pan-event": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.0.9.tgz",
+ "integrity": "sha512-xu35QXkiyrgsHUOnctl+SwNcwf9Rl62uYE5y8soKOZdBm8E+FvZIt2hxUzK1EoekbJCMzEZ0Yv1ZQCssVkSLaQ==",
+ "dependencies": {
+ "@chakra-ui/event-utils": "2.0.8",
+ "@chakra-ui/react-use-latest-ref": "2.0.5",
+ "framesync": "6.1.2"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-previous": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-previous/-/react-use-previous-2.0.5.tgz",
+ "integrity": "sha512-BIZgjycPE4Xr+MkhKe0h67uHXzQQkBX/u5rYPd65iMGdX1bCkbE0oorZNfOHLKdTmnEb4oVsNvfN6Rfr+Mnbxw==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-safe-layout-effect": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.0.5.tgz",
+ "integrity": "sha512-MwAQBz3VxoeFLaesaSEN87reVNVbjcQBDex2WGexAg6hUB6n4gc1OWYH/iXp4tzp4kuggBNhEHkk9BMYXWfhJQ==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-size": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-size/-/react-use-size-2.0.10.tgz",
+ "integrity": "sha512-fdIkH14GDnKQrtQfxX8N3gxbXRPXEl67Y3zeD9z4bKKcQUAYIMqs0MsPZY+FMpGQw8QqafM44nXfL038aIrC5w==",
+ "dependencies": {
+ "@zag-js/element-size": "0.3.2"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-timeout": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-timeout/-/react-use-timeout-2.0.5.tgz",
+ "integrity": "sha512-QqmB+jVphh3h/CS60PieorpY7UqSPkrQCB7f7F+i9vwwIjtP8fxVHMmkb64K7VlzQiMPzv12nlID5dqkzlv0mw==",
+ "dependencies": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-use-update-effect": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.0.7.tgz",
+ "integrity": "sha512-vBM2bmmM83ZdDtasWv3PXPznpTUd+FvqBC8J8rxoRmvdMEfrxTiQRBJhiGHLpS9BPLLPQlosN6KdFU97csB6zg==",
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/react-utils": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz",
+ "integrity": "sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==",
+ "dependencies": {
+ "@chakra-ui/utils": "2.0.15"
+ },
+ "peerDependencies": {
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/select": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-2.0.19.tgz",
+ "integrity": "sha512-eAlFh+JhwtJ17OrB6fO6gEAGOMH18ERNrXLqWbYLrs674Le7xuREgtuAYDoxUzvYXYYTTdOJtVbcHGriI3o6rA==",
+ "dependencies": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/shared-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz",
+ "integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q=="
+ },
+ "node_modules/@chakra-ui/skeleton": {
+ "version": "2.0.24",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-2.0.24.tgz",
+ "integrity": "sha512-1jXtVKcl/jpbrJlc/TyMsFyI651GTXY5ma30kWyTXoby2E+cxbV6OR8GB/NMZdGxbQBax8/VdtYVjI0n+OBqWA==",
+ "dependencies": {
+ "@chakra-ui/media-query": "3.2.12",
+ "@chakra-ui/react-use-previous": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/slider": {
+ "version": "2.0.23",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-2.0.23.tgz",
+ "integrity": "sha512-/eyRUXLla+ZdBUPXpakE3SAS2JS8mIJR6qcUYiPVKSpRAi6tMyYeQijAXn2QC1AUVd2JrG8Pz+1Jy7Po3uA7cA==",
+ "dependencies": {
+ "@chakra-ui/number-utils": "2.0.7",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-latest-ref": "2.0.5",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-pan-event": "2.0.9",
+ "@chakra-ui/react-use-size": "2.0.10",
+ "@chakra-ui/react-use-update-effect": "2.0.7"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/spinner": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-2.0.13.tgz",
+ "integrity": "sha512-T1/aSkVpUIuiYyrjfn1+LsQEG7Onbi1UE9ccS/evgf61Dzy4GgTXQUnDuWFSgpV58owqirqOu6jn/9eCwDlzlg==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/stat": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-2.0.18.tgz",
+ "integrity": "sha512-wKyfBqhVlIs9bkSerUc6F9KJMw0yTIEKArW7dejWwzToCLPr47u+CtYO6jlJHV6lRvkhi4K4Qc6pyvtJxZ3VpA==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/styled-system": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.8.0.tgz",
+ "integrity": "sha512-bmRv/8ACJGGKGx84U1npiUddwdNifJ+/ETklGwooS5APM0ymwUtBYZpFxjYNJrqvVYpg3mVY6HhMyBVptLS7iA==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5",
+ "csstype": "^3.0.11",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "node_modules/@chakra-ui/switch": {
+ "version": "2.0.26",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-2.0.26.tgz",
+ "integrity": "sha512-x62lF6VazSZJQuVxosChVR6+0lIJe8Pxgkl/C9vxjhp2yVYb3mew5tcX/sDOu0dYZy8ro/9hMfGkdN4r9xEU8A==",
+ "dependencies": {
+ "@chakra-ui/checkbox": "2.2.14",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/system": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.5.5.tgz",
+ "integrity": "sha512-52BIp/Zyvefgxn5RTByfkTeG4J+y81LWEjWm8jCaRFsLVm8IFgqIrngtcq4I7gD5n/UKbneHlb4eLHo4uc5yDQ==",
+ "dependencies": {
+ "@chakra-ui/color-mode": "2.1.12",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-utils": "2.0.12",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme-utils": "2.0.15",
+ "@chakra-ui/utils": "2.0.15",
+ "react-fast-compare": "3.2.1"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0",
+ "@emotion/styled": "^11.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/table": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-2.0.17.tgz",
+ "integrity": "sha512-OScheTEp1LOYvTki2NFwnAYvac8siAhW9BI5RKm5f5ORL2gVJo4I72RUqE0aKe1oboxgm7CYt5afT5PS5cG61A==",
+ "dependencies": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/tabs": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-2.1.9.tgz",
+ "integrity": "sha512-Yf8e0kRvaGM6jfkJum0aInQ0U3ZlCafmrYYni2lqjcTtThqu+Yosmo3iYlnullXxCw5MVznfrkb9ySvgQowuYg==",
+ "dependencies": {
+ "@chakra-ui/clickable": "2.0.14",
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/tag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-3.0.0.tgz",
+ "integrity": "sha512-YWdMmw/1OWRwNkG9pX+wVtZio+B89odaPj6XeMn5nfNN8+jyhIEpouWv34+CO9G0m1lupJTxPSfgLAd7cqXZMA==",
+ "dependencies": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/textarea": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-2.0.19.tgz",
+ "integrity": "sha512-adJk+qVGsFeJDvfn56CcJKKse8k7oMGlODrmpnpTdF+xvlsiTM+1GfaJvgNSpHHuQFdz/A0z1uJtfGefk0G2ZA==",
+ "dependencies": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/theme": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-3.0.1.tgz",
+ "integrity": "sha512-92kDm/Ux/51uJqhRKevQo/O/rdwucDYcpHg2QuwzdAxISCeYvgtl2TtgOOl5EnqEP0j3IEAvZHZUlv8TTbawaw==",
+ "dependencies": {
+ "@chakra-ui/anatomy": "2.1.2",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/theme-tools": "2.0.17"
+ },
+ "peerDependencies": {
+ "@chakra-ui/styled-system": ">=2.0.0"
+ }
+ },
+ "node_modules/@chakra-ui/theme-tools": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-2.0.17.tgz",
+ "integrity": "sha512-Auu38hnihlJZQcPok6itRDBbwof3TpXGYtDPnOvrq4Xp7jnab36HLt7KEXSDPXbtOk3ZqU99pvI1en5LbDrdjg==",
+ "dependencies": {
+ "@chakra-ui/anatomy": "2.1.2",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "color2k": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@chakra-ui/styled-system": ">=2.0.0"
+ }
+ },
+ "node_modules/@chakra-ui/theme-utils": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme-utils/-/theme-utils-2.0.15.tgz",
+ "integrity": "sha512-UuxtEgE7gwMTGDXtUpTOI7F5X0iHB9ekEOG5PWPn2wWBL7rlk2JtPI7UP5Um5Yg6vvBfXYGK1ySahxqsgf+87g==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme": "3.0.1",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "node_modules/@chakra-ui/toast": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-6.1.1.tgz",
+ "integrity": "sha512-JtjIKkPVjEu8okGGCipCxNVgK/15h5AicTATZ6RbG2MsHmr4GfKG3fUCvpbuZseArqmLqGLQZQJjVE9vJzaSkQ==",
+ "dependencies": {
+ "@chakra-ui/alert": "2.1.0",
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-timeout": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme": "3.0.1"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": "2.5.5",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/tooltip": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-2.2.7.tgz",
+ "integrity": "sha512-ImUJ6NnVqARaYqpgtO+kzucDRmxo8AF3jMjARw0bx2LxUkKwgRCOEaaRK5p5dHc0Kr6t5/XqjDeUNa19/sLauA==",
+ "dependencies": {
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "framer-motion": ">=4.0.0",
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/transition": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-2.0.16.tgz",
+ "integrity": "sha512-E+RkwlPc3H7P1crEXmXwDXMB2lqY2LLia2P5siQ4IEnRWIgZXlIw+8Em+NtHNgusel2N+9yuB0wT9SeZZeZ3CQ==",
+ "dependencies": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ },
+ "peerDependencies": {
+ "framer-motion": ">=4.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@chakra-ui/utils": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.15.tgz",
+ "integrity": "sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==",
+ "dependencies": {
+ "@types/lodash.mergewith": "4.6.7",
+ "css-box-model": "1.2.1",
+ "framesync": "6.1.2",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "node_modules/@chakra-ui/visually-hidden": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-2.0.15.tgz",
+ "integrity": "sha512-WWULIiucYRBIewHKFA7BssQ2ABLHLVd9lrUo3N3SZgR0u4ZRDDVEUNOy+r+9ruDze8+36dGbN9wsN1IdELtdOw==",
+ "peerDependencies": {
+ "@chakra-ui/system": ">=2.0.0",
+ "react": ">=18"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",
+ "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/serialize": "^1.1.1",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.1.3"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.10.5",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
+ "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/sheet": "^1.2.1",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "stylis": "4.1.3"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
+ "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
+ "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
+ "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz",
+ "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/cache": "^11.10.5",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/unitless": "^0.8.0",
+ "@emotion/utils": "^1.2.0",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
+ "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz",
+ "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
+ "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
+ "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
+ "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
+ "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
+ "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
+ "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
+ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@heroicons/react": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.16.tgz",
+ "integrity": "sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==",
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "2.9.11",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz",
+ "integrity": "sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
+ "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.0",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+ "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "node_modules/@motionone/animation": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",
+ "integrity": "sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==",
+ "dependencies": {
+ "@motionone/easing": "^10.15.1",
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@motionone/dom": {
+ "version": "10.15.5",
+ "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.15.5.tgz",
+ "integrity": "sha512-Xc5avlgyh3xukU9tydh9+8mB8+2zAq+WlLsC3eEIp7Ax7DnXgY7Bj/iv0a4X2R9z9ZFZiaXK3BO0xMYHKbAAdA==",
+ "dependencies": {
+ "@motionone/animation": "^10.15.1",
+ "@motionone/generators": "^10.15.1",
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "hey-listen": "^1.0.8",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@motionone/easing": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.15.1.tgz",
+ "integrity": "sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==",
+ "dependencies": {
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@motionone/generators": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.15.1.tgz",
+ "integrity": "sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==",
+ "dependencies": {
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@motionone/types": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.15.1.tgz",
+ "integrity": "sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA=="
+ },
+ "node_modules/@motionone/utils": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.15.1.tgz",
+ "integrity": "sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==",
+ "dependencies": {
+ "@motionone/types": "^10.15.1",
+ "hey-listen": "^1.0.8",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.6",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+ "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz",
+ "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
+ "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sphinxxxx/color-conversion": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
+ "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
+ },
+ "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz",
+ "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
+ "integrity": "sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz",
+ "integrity": "sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz",
+ "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-dynamic-title": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz",
+ "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-em-dimensions": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz",
+ "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-react-native-svg": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz",
+ "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-svg-component": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz",
+ "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/babel-preset": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz",
+ "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==",
+ "dev": true,
+ "dependencies": {
+ "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1",
+ "@svgr/babel-plugin-remove-jsx-attribute": "*",
+ "@svgr/babel-plugin-remove-jsx-empty-expression": "*",
+ "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1",
+ "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1",
+ "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1",
+ "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1",
+ "@svgr/babel-plugin-transform-svg-component": "^6.5.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@svgr/core": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz",
+ "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.19.6",
+ "@svgr/babel-preset": "^6.5.1",
+ "@svgr/plugin-jsx": "^6.5.1",
+ "camelcase": "^6.2.0",
+ "cosmiconfig": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/hast-util-to-babel-ast": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz",
+ "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.0",
+ "entities": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/plugin-jsx": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz",
+ "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.19.6",
+ "@svgr/babel-preset": "^6.5.1",
+ "@svgr/hast-util-to-babel-ast": "^6.5.1",
+ "svg-parser": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ },
+ "peerDependencies": {
+ "@svgr/core": "^6.0.0"
+ }
+ },
+ "node_modules/@types/ace": {
+ "version": "0.0.48",
+ "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
+ "integrity": "sha512-esV6hOWiDOZ6d7w5S11iLu6LQsPGe/9RPzhri7gNNLdrK1LFpO9/m7IZhQL6dat0JHICJ7l51zvHAiCgnPLLHA==",
+ "dev": true
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
+ "dev": true
+ },
+ "node_modules/@types/jsoneditor": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@types/jsoneditor/-/jsoneditor-9.9.0.tgz",
+ "integrity": "sha512-OwDYzMuqCn+QZHQPMDu7miLVtnJueB2lQ42KmX1mZNZTaZxV8v55wiL1sit7cZGA5BzhNWVw8KAHhEoFWYyW3g==",
+ "dev": true,
+ "dependencies": {
+ "@types/ace": "*",
+ "ajv": "^6.12.0"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.14.191",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
+ "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
+ },
+ "node_modules/@types/lodash.debounce": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz",
+ "integrity": "sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/lodash.mergewith": {
+ "version": "4.6.7",
+ "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz",
+ "integrity": "sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz",
+ "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
+ "devOptional": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.0.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
+ "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-copy-to-clipboard": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
+ "integrity": "sha512-otTJsJpofYAeaIeOwV5xBUGpo6exXG2HX7X4nseToCB2VgPEBxGBHCm/FecZ676doNR7HCSTVtmohxfG2b3/yQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-datepicker": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.8.0.tgz",
+ "integrity": "sha512-20uzZsIf4moPAjjHDfPvH8UaOHZBxrkiQZoLS3wgKq8Xhp+95gdercLEdoA7/I8nR9R5Jz2qQkdMIM+Lq4AS1A==",
+ "dev": true,
+ "dependencies": {
+ "@popperjs/core": "^2.9.2",
+ "@types/react": "*",
+ "date-fns": "^2.0.1",
+ "react-popper": "^2.2.5"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.0.11",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz",
+ "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-json-editor-ajrm": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/@types/react-json-editor-ajrm/-/react-json-editor-ajrm-2.5.3.tgz",
+ "integrity": "sha512-hHf3FU1HjL6Lm3rK2iWUJtoYpZ1qDCSffnLAQXCf++TP1Ms18rnB06gBK3psQ8NEW6CfrfRNlirIL3D/P80QVg==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-slick": {
+ "version": "0.23.10",
+ "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.10.tgz",
+ "integrity": "sha512-ZiqdencANDZy6sWOWJ54LDvebuXFEhDlHtXU9FFipQR2BcYU2QJxZhvJPW6YK7cocibUiNn+YvDTbt1HtCIBVA==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
+ "devOptional": true
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz",
+ "integrity": "sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.19.6",
+ "@babel/plugin-transform-react-jsx": "^7.19.0",
+ "@babel/plugin-transform-react-jsx-development": "^7.18.6",
+ "@babel/plugin-transform-react-jsx-self": "^7.18.6",
+ "@babel/plugin-transform-react-jsx-source": "^7.19.6",
+ "magic-string": "^0.26.7",
+ "react-refresh": "^0.14.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^3.0.0"
+ }
+ },
+ "node_modules/@zag-js/element-size": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.3.2.tgz",
+ "integrity": "sha512-bVvvigUGvAuj7PCkE5AbzvTJDTw5f3bg9nQdv+ErhVN8SfPPppLJEmmWdxqsRzrHXgx8ypJt/+Ty0kjtISVDsQ=="
+ },
+ "node_modules/@zag-js/focus-visible": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-0.2.2.tgz",
+ "integrity": "sha512-0j2gZq8HiZ51z4zNnSkF1iSkqlwRDvdH+son3wHdoz+7IUdMN/5Exd4TxMJ+gq2Of1DiXReYLL9qqh2PdQ4wgA=="
+ },
+ "node_modules/ace-builds": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.20.0.tgz",
+ "integrity": "sha512-JUvWbRXz7tL3EkcM5CpVyyuu9mvVRo594ZRgwHzb9arb9s2o5gTg94Lrk3zQZzM1udq6fRSDnmU2RClJPt9aNw=="
+ },
+ "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/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": "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/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/apexcharts": {
+ "version": "3.40.0",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.40.0.tgz",
+ "integrity": "sha512-dSi3BUfCJkFd67uFp+xffrJVd3lDT7AAUUyRp0qPYiglJ76CeZLddVhM3FAk1P9GCzf8VewqGYUPCYQvXm+b9A==",
+ "dependencies": {
+ "svg.draggable.js": "^2.2.2",
+ "svg.easing.js": "^2.0.0",
+ "svg.filter.js": "^2.0.2",
+ "svg.pathmorphing.js": "^0.1.3",
+ "svg.resize.js": "^1.4.3",
+ "svg.select.js": "^3.0.1"
+ }
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.13",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
+ "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-lite": "^1.0.30001426",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/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/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/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=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.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/broadcast-channel": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+ "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+ "dependencies": {
+ "@babel/runtime": "^7.7.2",
+ "detect-node": "^2.1.0",
+ "js-sha3": "0.8.0",
+ "microseconds": "0.2.0",
+ "nano-time": "1.0.0",
+ "oblivious-set": "1.0.0",
+ "rimraf": "3.0.2",
+ "unload": "2.2.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
+ "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001449",
+ "electron-to-chromium": "^1.4.284",
+ "node-releases": "^2.0.8",
+ "update-browserslist-db": "^1.0.10"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "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/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001458",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
+ "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ]
+ },
+ "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/chalk/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/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "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"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+ "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+ },
+ "node_modules/clear-any-console": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/clear-any-console/-/clear-any-console-1.16.2.tgz",
+ "integrity": "sha512-OL/7wZpNy9x0GBSzz3poWja84Nr7iaH8aYNsJ5Uet2BVLj6Lm1zvWpZN/yH46Vv3ae7YfHmLLMmfHj911fshJg==",
+ "dev": true
+ },
+ "node_modules/cli-check-node": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/cli-check-node/-/cli-check-node-1.3.4.tgz",
+ "integrity": "sha512-iLGgQXm82iP8eH3R67qbOWs5qqUOLmNnMy5Lzl/RybcMh3y+H2zWU5POzuQ6oDUOdz4XWuxcFhP75szqd6frLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^3.0.0",
+ "log-symbols": "^3.0.0"
+ }
+ },
+ "node_modules/cli-check-node/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cli-check-node/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-check-node/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/cli-check-node/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/cli-check-node/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==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-check-node/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==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-handle-error": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/cli-handle-error/-/cli-handle-error-4.4.0.tgz",
+ "integrity": "sha512-RyBCnKlc7xVr79cKb9RfBq+4fjwQeX8HKeNzIPnI/W+DWWIUUKh2ur576DpwJ3kZt2UGHlIAOF7N9txy+mgZsA==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^3.0.0",
+ "log-symbols": "^3.0.0"
+ }
+ },
+ "node_modules/cli-handle-error/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cli-handle-error/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-handle-error/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/cli-handle-error/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/cli-handle-error/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==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-handle-error/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==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-handle-unhandled": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cli-handle-unhandled/-/cli-handle-unhandled-1.1.1.tgz",
+ "integrity": "sha512-Em91mJvU7VdgT2MxQpyY633vW1tDzRjPDbii6ZjEBHHLLh0xDoVkFt/wjvi9nSvJcz9rJmvtJSK8KL/hvF0Stg==",
+ "dev": true,
+ "dependencies": {
+ "cli-handle-error": "^4.1.0"
+ }
+ },
+ "node_modules/cli-welcome": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/cli-welcome/-/cli-welcome-2.2.2.tgz",
+ "integrity": "sha512-LgDGS0TW4nIf8v81wpuZzfOEDPcy68u0jKR0Fy5IaWftqdminI6FoDiMFt1mjPylqKGNv/wFsZ7fCs93IeDMIw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "clear-any-console": "^1.16.0",
+ "prettier": "^2.0.5"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/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/color2k": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.2.tgz",
+ "integrity": "sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w=="
+ },
+ "node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/compute-scroll-into-view": {
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+ "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
+ },
+ "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/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/copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "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-fetch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+ "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+ "dependencies": {
+ "node-fetch": "2.6.7"
+ }
+ },
+ "node_modules/css-box-model": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+ "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+ "dependencies": {
+ "tiny-invariant": "^1.0.6"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
+ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ },
+ "node_modules/date-fns": {
+ "version": "2.29.3",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+ "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+ "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "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/destr": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz",
+ "integrity": "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA=="
+ },
+ "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-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.311",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz",
+ "integrity": "sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==",
+ "dev": true
+ },
+ "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/enquire.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
+ "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw=="
+ },
+ "node_modules/entities": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
+ "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "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/esbuild": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
+ "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.15.18",
+ "@esbuild/linux-loong64": "0.15.18",
+ "esbuild-android-64": "0.15.18",
+ "esbuild-android-arm64": "0.15.18",
+ "esbuild-darwin-64": "0.15.18",
+ "esbuild-darwin-arm64": "0.15.18",
+ "esbuild-freebsd-64": "0.15.18",
+ "esbuild-freebsd-arm64": "0.15.18",
+ "esbuild-linux-32": "0.15.18",
+ "esbuild-linux-64": "0.15.18",
+ "esbuild-linux-arm": "0.15.18",
+ "esbuild-linux-arm64": "0.15.18",
+ "esbuild-linux-mips64le": "0.15.18",
+ "esbuild-linux-ppc64le": "0.15.18",
+ "esbuild-linux-riscv64": "0.15.18",
+ "esbuild-linux-s390x": "0.15.18",
+ "esbuild-netbsd-64": "0.15.18",
+ "esbuild-openbsd-64": "0.15.18",
+ "esbuild-sunos-64": "0.15.18",
+ "esbuild-windows-32": "0.15.18",
+ "esbuild-windows-64": "0.15.18",
+ "esbuild-windows-arm64": "0.15.18"
+ }
+ },
+ "node_modules/esbuild-android-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
+ "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-android-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
+ "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
+ "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-darwin-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
+ "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
+ "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-freebsd-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
+ "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
+ "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
+ "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
+ "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
+ "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-mips64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
+ "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-ppc64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
+ "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-riscv64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
+ "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-linux-s390x": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
+ "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-netbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
+ "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-openbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
+ "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-sunos-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
+ "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
+ "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
+ "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-windows-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
+ "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "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-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/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/focus-lock": {
+ "version": "0.11.6",
+ "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.6.tgz",
+ "integrity": "sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "7.10.3",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-7.10.3.tgz",
+ "integrity": "sha512-k2ccYeZNSpPg//HTaqrU+4pRq9f9ZpaaN7rr0+Rx5zA4wZLbk547wtDzge2db1sB+1mnJ6r59P4xb+aEIi/W+w==",
+ "dependencies": {
+ "@motionone/dom": "^10.15.3",
+ "hey-listen": "^1.0.8",
+ "tslib": "2.4.0"
+ },
+ "optionalDependencies": {
+ "@emotion/is-prop-valid": "^0.8.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "optional": true,
+ "dependencies": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "node_modules/framer-motion/node_modules/@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
+ "optional": true
+ },
+ "node_modules/framer-motion/node_modules/tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+ },
+ "node_modules/framesync": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
+ "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
+ "dependencies": {
+ "tslib": "2.4.0"
+ }
+ },
+ "node_modules/framesync/node_modules/tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "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==",
+ "dev": true,
+ "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-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/github-buttons": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/github-buttons/-/github-buttons-2.24.0.tgz",
+ "integrity": "sha512-dHpEno4S2JHjsEKPLSTGDTV50q1d08n3H7VDfNNA1m8cz5DMqLCZDdKWQxy4zyvd6M3tKvlVYLs+/H0BF3DggA=="
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "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/hey-listen": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
+ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
+ },
+ "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/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "22.4.15",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz",
+ "integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.20.6"
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz",
+ "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==",
+ "dependencies": {
+ "@babel/runtime": "^7.19.4"
+ }
+ },
+ "node_modules/i18next-http-backend": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.0.tgz",
+ "integrity": "sha512-Z4sM7R6tzdLknSPER9GisEBxKPg5FkI07UrQniuroZmS15PHQrcCPLyuGKj8SS68tf+O2aEDYSUnmy1TZqZSbw==",
+ "dependencies": {
+ "cross-fetch": "3.1.5"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
+ "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==",
+ "dev": true
+ },
+ "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/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "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/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/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-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==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "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/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
+ },
+ "node_modules/jmespath": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+ "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/jquery": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
+ "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
+ "peer": true
+ },
+ "node_modules/js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
+ "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/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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-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-source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz",
+ "integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="
+ },
+ "node_modules/json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+ "dependencies": {
+ "string-convert": "^0.2.0"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsoneditor": {
+ "version": "9.10.1",
+ "resolved": "https://registry.npmjs.org/jsoneditor/-/jsoneditor-9.10.1.tgz",
+ "integrity": "sha512-vequCPWX+dNndl25+wlniQAaEgJ2Vy+X44a8GL3kOfPrbF9E/ZFH5XEpwyltI1kr95YFTp0PISuwmgwJHpQx4g==",
+ "dependencies": {
+ "ace-builds": "^1.15.2",
+ "ajv": "^6.12.6",
+ "javascript-natural-sort": "^0.7.1",
+ "jmespath": "^0.16.0",
+ "json-source-map": "^0.6.1",
+ "jsonrepair": "^3.0.2",
+ "mobius1-selectr": "^2.4.13",
+ "picomodal": "^3.0.0",
+ "vanilla-picker": "^2.12.1"
+ }
+ },
+ "node_modules/jsonrepair": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.1.0.tgz",
+ "integrity": "sha512-idqReg23J0PVRAADmZMc5xQM3xeOX5bTB6OTyMnzq33IXJXmn9iJuWIEvGmrN80rQf4d7uLTMEDwpzujNcI0Rg==",
+ "bin": {
+ "jsonrepair": "bin/cli.js"
+ }
+ },
+ "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/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.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
+ },
+ "node_modules/log-symbols": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/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==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
+ "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/match-sorter": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
+ "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "remove-accents": "0.4.2"
+ }
+ },
+ "node_modules/microseconds": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+ "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+ },
+ "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/mobius1-selectr": {
+ "version": "2.4.13",
+ "resolved": "https://registry.npmjs.org/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz",
+ "integrity": "sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw=="
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/nano-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+ "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+ "dependencies": {
+ "big-integer": "^1.6.16"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "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/node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch-native": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-0.1.8.tgz",
+ "integrity": "sha512-ZNaury9r0NxaT2oL65GvdGDy+5PlSaHTovT6JV5tOW07k1TQmgC0olZETa4C9KZg0+6zBr99ctTYa3Utqj9P/Q=="
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
+ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
+ "dev": true
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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/oblivious-set": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+ "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+ },
+ "node_modules/ohmyfetch": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/ohmyfetch/-/ohmyfetch-0.4.21.tgz",
+ "integrity": "sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw==",
+ "dependencies": {
+ "destr": "^1.2.0",
+ "node-fetch-native": "^0.1.8",
+ "ufo": "^0.8.6",
+ "undici": "^5.12.0"
+ }
+ },
+ "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/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/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/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-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-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/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "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/picomodal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/picomodal/-/picomodal-3.0.0.tgz",
+ "integrity": "sha512-FoR3TDfuLlqUvcEeK5ifpKSVVns6B4BQvc8SDF6THVMuadya6LLtji0QgUDSStw0ZR2J7I6UGi5V2V23rnPWTw=="
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "dev": true,
+ "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.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "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==",
+ "dev": true
+ },
+ "node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "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/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode.react": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz",
+ "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-apexcharts": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz",
+ "integrity": "sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==",
+ "dependencies": {
+ "prop-types": "^15.5.7"
+ },
+ "peerDependencies": {
+ "apexcharts": "^3.18.0",
+ "react": ">=0.13"
+ }
+ },
+ "node_modules/react-clientside-effect": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
+ "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.13"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-copy-to-clipboard": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
+ "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
+ "dependencies": {
+ "copy-to-clipboard": "^3.3.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || 16 || 17 || 18"
+ }
+ },
+ "node_modules/react-datepicker": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.10.0.tgz",
+ "integrity": "sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag==",
+ "dependencies": {
+ "@popperjs/core": "^2.9.2",
+ "classnames": "^2.2.6",
+ "date-fns": "^2.24.0",
+ "prop-types": "^15.7.2",
+ "react-onclickoutside": "^6.12.2",
+ "react-popper": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17 || ^18",
+ "react-dom": "^16.9.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-fast-compare": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
+ "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
+ },
+ "node_modules/react-focus-lock": {
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz",
+ "integrity": "sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "focus-lock": "^0.11.6",
+ "prop-types": "^15.6.2",
+ "react-clientside-effect": "^1.2.6",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-github-btn": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz",
+ "integrity": "sha512-lV4FYClAfjWnBfv0iNlJUGhamDgIq6TayD0kPZED6VzHWdpcHmPfsYOZ/CFwLfPv4Zp+F4m8QKTj0oy2HjiGXg==",
+ "dependencies": {
+ "github-buttons": "^2.22.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.3.0"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.43.2",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.2.tgz",
+ "integrity": "sha512-NvD3Oe2Y9hhqo2R4I4iJigDzSLpdMnzUpNMxlnzTbdiT7NT3BW0GxWCzEtwPudZMUPbZhNcSy1EcGAygyhDORg==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-i18next": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz",
+ "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.20.6",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 19.0.0",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "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/react-json-editor-ajrm": {
+ "version": "2.5.14",
+ "resolved": "https://registry.npmjs.org/react-json-editor-ajrm/-/react-json-editor-ajrm-2.5.14.tgz",
+ "integrity": "sha512-z2035l5lnU0Wo73AUonj68d1Eh9vjVcLXEa8YogDEeFPcogzYkfT6R38fvYlLKDhP0oI+LyOO/IBWsFkXs4M0Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0-rc.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.2.0",
+ "react-dom": ">=16.2.0"
+ }
+ },
+ "node_modules/react-loading-skeleton": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.1.tgz",
+ "integrity": "sha512-gQeNzzKXlevvquRDOhN5kas5VE7toTKrpmE9BpUdf0iip9c5ae0LcVbulh5ebO2CFl4RrYsyLuDtYGyrh1h1iA==",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/react-onclickoutside": {
+ "version": "6.12.2",
+ "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
+ "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
+ },
+ "peerDependencies": {
+ "react": "^15.5.x || ^16.x || ^17.x || ^18.x",
+ "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
+ }
+ },
+ "node_modules/react-popper": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
+ "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
+ "dependencies": {
+ "react-fast-compare": "^3.0.1",
+ "warning": "^4.0.2"
+ },
+ "peerDependencies": {
+ "@popperjs/core": "^2.0.0",
+ "react": "^16.8.0 || ^17 || ^18",
+ "react-dom": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-query": {
+ "version": "3.39.3",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+ "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "broadcast-channel": "^3.4.1",
+ "match-sorter": "^6.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
+ "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.3",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
+ "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz",
+ "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==",
+ "dependencies": {
+ "@remix-run/router": "1.3.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz",
+ "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==",
+ "dependencies": {
+ "@remix-run/router": "1.3.2",
+ "react-router": "6.8.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-slick": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz",
+ "integrity": "sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA==",
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "enquire.js": "^2.1.6",
+ "json2mq": "^0.2.0",
+ "lodash.debounce": "^4.0.8",
+ "resize-observer-polyfill": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-websocket": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.3.1.tgz",
+ "integrity": "sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==",
+ "peerDependencies": {
+ "react": ">= 18.0.0",
+ "react-dom": ">= 18.0.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "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/remove-accents": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
+ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+ },
+ "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/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dependencies": {
+ "is-core-module": "^2.9.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-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/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.79.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
+ "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "devOptional": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-visualizer": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz",
+ "integrity": "sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==",
+ "dependencies": {
+ "open": "^8.4.0",
+ "picomatch": "^2.3.1",
+ "source-map": "^0.7.4",
+ "yargs": "^17.5.1"
+ },
+ "bin": {
+ "rollup-plugin-visualizer": "dist/bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "rollup": "2.x || 3.x"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/rollup-plugin-visualizer/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/sass": {
+ "version": "1.58.3",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz",
+ "integrity": "sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/slick-carousel": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
+ "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
+ "peerDependencies": {
+ "jquery": ">=1.8.0"
+ }
+ },
+ "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.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "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",
+ "dev": true
+ },
+ "node_modules/string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="
+ },
+ "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/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/stylis": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+ "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+ },
+ "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/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==",
+ "dev": true
+ },
+ "node_modules/svg.draggable.js": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
+ "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
+ "dependencies": {
+ "svg.js": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.easing.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
+ "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
+ "dependencies": {
+ "svg.js": ">=2.3.x"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.filter.js": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
+ "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
+ "dependencies": {
+ "svg.js": "^2.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.js": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
+ "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
+ },
+ "node_modules/svg.pathmorphing.js": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
+ "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
+ "dependencies": {
+ "svg.js": "^2.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.resize.js": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
+ "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
+ "dependencies": {
+ "svg.js": "^2.6.5",
+ "svg.select.js": "^2.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.resize.js/node_modules/svg.select.js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
+ "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
+ "dependencies": {
+ "svg.js": "^2.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/svg.select.js": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
+ "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
+ "dependencies": {
+ "svg.js": "^2.6.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
+ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/tsconfck": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.0.3.tgz",
+ "integrity": "sha512-o3DsPZO1+C98KqHMdAbWs30zpxD30kj8r9OLA4ML1yghx4khNDzaaShNalfluh8ZPPhzKe3qyVCP1HiZszSAsw==",
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^14.13.1 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "typescript": "^4.3.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "devOptional": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "0.8.6",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.8.6.tgz",
+ "integrity": "sha512-fk6CmUgwKCfX79EzcDQQpSCMxrHstvbLswFChHS0Vump+kFkw7nJBfTZoC1j0bOGoY9I7R3n2DGek5ajbcYnOw=="
+ },
+ "node_modules/undici": {
+ "version": "5.27.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
+ "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
+ "node_modules/unload": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+ "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+ "dependencies": {
+ "@babel/runtime": "^7.6.2",
+ "detect-node": "^2.0.4"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
+ "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "browserslist-lint": "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/use-callback-ref": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
+ "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/vanilla-picker": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.1.tgz",
+ "integrity": "sha512-2qrEP9VYylKXbyzXKsbu2dferBTvqnlsr29XjHwFE+/MEp0VNj6oEUESLDtKZ7DWzGdSv1x/+ujqFZF+KsO3cg==",
+ "dependencies": {
+ "@sphinxxxx/color-conversion": "^2.2.2"
+ }
+ },
+ "node_modules/vite": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
+ "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.15.9",
+ "postcss": "^8.4.18",
+ "resolve": "^1.22.1",
+ "rollup": "^2.79.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-svgr": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz",
+ "integrity": "sha512-q+mJJol6ThvqkkJvvVFEndI4EaKIjSI0I3jNFgSoC9fXAz1M7kYTVUin8fhUsFojFDKZ9VHKtX6NXNaOLpbsHA==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.2",
+ "@svgr/core": "^6.5.1"
+ },
+ "peerDependencies": {
+ "vite": "^2.6.0 || 3 || 4"
+ }
+ },
+ "node_modules/vite-tsconfig-paths": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.0.5.tgz",
+ "integrity": "sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^2.0.1"
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "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/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/wrap-ansi/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/wrap-ansi/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/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/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==",
+ "dev": true
+ },
+ "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": "17.7.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
+ "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz",
+ "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zustand-computed": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/zustand-computed/-/zustand-computed-1.3.3.tgz",
+ "integrity": "sha512-SXhjuH/scwA6T31xsqaqUzPVBoumgCK/PWi8YZXGmuU86HPrqVFxjI6TxLvYw8hGpScZkjFUKBAi1ThO0B6Tng==",
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "zustand": "^4.1.4"
+ }
+ }
+ },
+ "dependencies": {
+ "@ampproject/remapping": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
+ "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/gen-mapping": "^0.1.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@babel/code-frame": {
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "requires": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ }
+ },
+ "@babel/compat-data": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz",
+ "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==",
+ "dev": true
+ },
+ "@babel/core": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz",
+ "integrity": "sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==",
+ "dev": true,
+ "requires": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.21.0",
+ "@babel/helper-compilation-targets": "^7.20.7",
+ "@babel/helper-module-transforms": "^7.21.0",
+ "@babel/helpers": "^7.21.0",
+ "@babel/parser": "^7.21.0",
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.0",
+ "@babel/types": "^7.21.0",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.2",
+ "semver": "^6.3.0"
+ }
+ },
+ "@babel/generator": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz",
+ "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.23.3",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "dependencies": {
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ }
+ }
+ },
+ "@babel/helper-annotate-as-pure": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz",
+ "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-compilation-targets": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
+ "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+ "dev": true,
+ "requires": {
+ "@babel/compat-data": "^7.20.5",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.21.3",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.0"
+ }
+ },
+ "@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+ "dev": true
+ },
+ "@babel/helper-function-name": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ }
+ },
+ "@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.22.5"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-module-transforms": {
+ "version": "7.21.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz",
+ "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-environment-visitor": "^7.18.9",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.20.2",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.2",
+ "@babel/types": "^7.21.2"
+ }
+ },
+ "@babel/helper-plugin-utils": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
+ "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
+ "dev": true
+ },
+ "@babel/helper-simple-access": {
+ "version": "7.20.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
+ "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.20.2"
+ }
+ },
+ "@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.22.5"
+ }
+ },
+ "@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw=="
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
+ },
+ "@babel/helper-validator-option": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
+ "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==",
+ "dev": true
+ },
+ "@babel/helpers": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz",
+ "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==",
+ "dev": true,
+ "requires": {
+ "@babel/template": "^7.20.7",
+ "@babel/traverse": "^7.21.0",
+ "@babel/types": "^7.21.0"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@babel/parser": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
+ "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
+ "dev": true
+ },
+ "@babel/plugin-syntax-jsx": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz",
+ "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.18.6"
+ }
+ },
+ "@babel/plugin-transform-react-jsx": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz",
+ "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-jsx": "^7.18.6",
+ "@babel/types": "^7.21.0"
+ }
+ },
+ "@babel/plugin-transform-react-jsx-development": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz",
+ "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==",
+ "dev": true,
+ "requires": {
+ "@babel/plugin-transform-react-jsx": "^7.18.6"
+ }
+ },
+ "@babel/plugin-transform-react-jsx-self": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.21.0.tgz",
+ "integrity": "sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.20.2"
+ }
+ },
+ "@babel/plugin-transform-react-jsx-source": {
+ "version": "7.19.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz",
+ "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.19.0"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
+ "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.11"
+ }
+ },
+ "@babel/template": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz",
+ "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.3",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.3",
+ "@babel/types": "^7.23.3",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ }
+ },
+ "@babel/types": {
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz",
+ "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==",
+ "requires": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@chakra-ui/accordion": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.1.11.tgz",
+ "integrity": "sha512-mfVPmqETp9pyRDHJ33AdF19oHv/LyxVzQJtlxUByuvs8Cj9QQZ2LQLg5kejm+b3mj03A7A6yfbuo3RNaI4Bhsg==",
+ "requires": {
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16"
+ }
+ },
+ "@chakra-ui/alert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-2.1.0.tgz",
+ "integrity": "sha512-OcfHwoXI5VrmM+tHJTHT62Bx6TfyfCxSa0PWUOueJzSyhlUOKBND5we6UtrOB7D0jwX45qKKEDJOLG5yCG21jQ==",
+ "requires": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/spinner": "2.0.13"
+ }
+ },
+ "@chakra-ui/anatomy": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.1.2.tgz",
+ "integrity": "sha512-pKfOS/mztc4sUXHNc8ypJ1gPWSolWT770jrgVRfolVbYlki8y5Y+As996zMF6k5lewTu6j9DQequ7Cc9a69IVQ=="
+ },
+ "@chakra-ui/avatar": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-2.2.8.tgz",
+ "integrity": "sha512-uBs9PMrqyK111tPIYIKnOM4n3mwgKqGpvYmtwBnnbQLTNLg4gtiWWVbpTuNMpyu1av0xQYomjUt8Doed8w6p8g==",
+ "requires": {
+ "@chakra-ui/image": "2.0.15",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/breadcrumb": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-2.1.5.tgz",
+ "integrity": "sha512-p3eQQrHQBkRB69xOmNyBJqEdfCrMt+e0eOH+Pm/DjFWfIVIbnIaFbmDCeWClqlLa21Ypc6h1hR9jEmvg8kmOog==",
+ "requires": {
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/breakpoint-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz",
+ "integrity": "sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/button": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-2.0.18.tgz",
+ "integrity": "sha512-E3c99+lOm6ou4nQVOTLkG+IdOPMjsQK+Qe7VyP8A/xeAMFONuibrWPRPpprr4ZkB4kEoLMfNuyH2+aEza3ScUA==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/spinner": "2.0.13"
+ }
+ },
+ "@chakra-ui/card": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/card/-/card-2.1.6.tgz",
+ "integrity": "sha512-fFd/WAdRNVY/WOSQv4skpy0WeVhhI0f7dTY1Sm0jVl0KLmuP/GnpsWtKtqWjNcV00K963EXDyhlk6+9oxbP4gw==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/checkbox": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-2.2.14.tgz",
+ "integrity": "sha512-uqo6lFWLqYBujPglrvRhTAErtuIXpmdpc5w0W4bjK7kyvLhxOpUh1hlDb2WoqlNpfRn/OaNeF6VinPnf9BJL8w==",
+ "requires": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/visually-hidden": "2.0.15",
+ "@zag-js/focus-visible": "0.2.2"
+ }
+ },
+ "@chakra-ui/cli": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-2.4.1.tgz",
+ "integrity": "sha512-GZZuHUA1cXJWpmYNiVTLPihvY4VhIssRl+AXgw/0IbeodTMop3jWlIioPKLAQeXu5CwvRA6iESyGjnu1V8Zykg==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^3.5.3",
+ "cli-check-node": "^1.3.4",
+ "cli-handle-unhandled": "^1.1.1",
+ "cli-welcome": "^2.2.2",
+ "commander": "^9.3.0",
+ "esbuild": "^0.17.18",
+ "prettier": "^2.8.8"
+ },
+ "dependencies": {
+ "@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ }
+ }
+ },
+ "@chakra-ui/clickable": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-2.0.14.tgz",
+ "integrity": "sha512-jfsM1qaD74ZykLHmvmsKRhDyokLUxEfL8Il1VoZMNX5RBI0xW/56vKpLTFF/v/+vLPLS+Te2cZdD4+2O+G6ulA==",
+ "requires": {
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/close-button": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-2.0.17.tgz",
+ "integrity": "sha512-05YPXk456t1Xa3KpqTrvm+7smx+95dmaPiwjiBN3p7LHUQVHJd8ZXSDB0V+WKi419k3cVQeJUdU/azDO2f40sw==",
+ "requires": {
+ "@chakra-ui/icon": "3.0.16"
+ }
+ },
+ "@chakra-ui/color-mode": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-2.1.12.tgz",
+ "integrity": "sha512-sYyfJGDoJSLYO+V2hxV9r033qhte5Nw/wAn5yRGGZnEEN1dKPEdWQ3XZvglWSDTNd0w9zkoH2w6vP4FBBYb/iw==",
+ "requires": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ }
+ },
+ "@chakra-ui/control-box": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-2.0.13.tgz",
+ "integrity": "sha512-FEyrU4crxati80KUF/+1Z1CU3eZK6Sa0Yv7Z/ydtz9/tvGblXW9NFanoomXAOvcIFLbaLQPPATm9Gmpr7VG05A==",
+ "requires": {}
+ },
+ "@chakra-ui/counter": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-2.0.14.tgz",
+ "integrity": "sha512-KxcSRfUbb94dP77xTip2myoE7P2HQQN4V5fRJmNAGbzcyLciJ+aDylUU/UxgNcEjawUp6Q242NbWb1TSbKoqog==",
+ "requires": {
+ "@chakra-ui/number-utils": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/css-reset": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-2.1.1.tgz",
+ "integrity": "sha512-jwEOfIAWmQsnChHQTW/eRE+dfE4MjmhvSvoUug5nkV1pI7veC/20noFlIZxzi82EbiQI8Fs0+Jnusgxr2yaOHA==",
+ "requires": {}
+ },
+ "@chakra-ui/descendant": {
+ "version": "3.0.14",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-3.0.14.tgz",
+ "integrity": "sha512-+Ahvp9H4HMpfScIv9w1vaecGz7qWAaK1YFHHolz/SIsGLaLGlbdp+5UNabQC7L6TUnzzJDQDxzwif78rTD7ang==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7"
+ }
+ },
+ "@chakra-ui/dom-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/dom-utils/-/dom-utils-2.0.6.tgz",
+ "integrity": "sha512-PVtDkPrDD5b8aoL6Atg7SLjkwhWb7BwMcLOF1L449L3nZN+DAO3nyAh6iUhZVJyunELj9d0r65CDlnMREyJZmA=="
+ },
+ "@chakra-ui/editable": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-2.0.21.tgz",
+ "integrity": "sha512-oYuXbHnggxSYJN7P9Pn0Scs9tPC91no4z1y58Oe+ILoJKZ+bFAEHtL7FEISDNJxw++MEukeFu7GU1hVqmdLsKQ==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-focus-on-pointer-down": "2.0.6",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/event-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz",
+ "integrity": "sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw=="
+ },
+ "@chakra-ui/focus-lock": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-2.0.16.tgz",
+ "integrity": "sha512-UuAdGCPVrCa1lecoAvpOQD7JFT7a9RdmhKWhFt5ioIcekSLJcerdLHuuL3w0qz//8kd1/SOt7oP0aJqdAJQrCw==",
+ "requires": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "react-focus-lock": "^2.9.2"
+ }
+ },
+ "@chakra-ui/form-control": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-2.0.18.tgz",
+ "integrity": "sha512-I0a0jG01IAtRPccOXSNugyRdUAe8Dy40ctqedZvznMweOXzbMCF1m+sHPLdWeWC/VI13VoAispdPY0/zHOdjsQ==",
+ "requires": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/hooks": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-2.1.6.tgz",
+ "integrity": "sha512-oMSOeoOF6/UpwTVlDFHSROAA4hPY8WgJ0erdHs1ZkuwAwHv7UzjDkvrb6xYzAAH9qHoFzc5RIBm6jVoh3LCc+Q==",
+ "requires": {
+ "@chakra-ui/react-utils": "2.0.12",
+ "@chakra-ui/utils": "2.0.15",
+ "compute-scroll-into-view": "1.0.20",
+ "copy-to-clipboard": "3.3.3"
+ }
+ },
+ "@chakra-ui/icon": {
+ "version": "3.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-3.0.16.tgz",
+ "integrity": "sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/image": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.0.15.tgz",
+ "integrity": "sha512-w2rElXtI3FHXuGpMCsSklus+pO1Pl2LWDwsCGdpBQUvGFbnHfl7MftQgTlaGHeD5OS95Pxva39hKrA2VklKHiQ==",
+ "requires": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/input": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-2.0.21.tgz",
+ "integrity": "sha512-AIWjjg6MgcOtlvKmVoZfPPfgF+sBSWL3Zq2HSCAMvS6h7jfxz/Xv0UTFGPk5F4Wt0YHT7qMySg0Jsm0b78HZJg==",
+ "requires": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/layout": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-2.1.18.tgz",
+ "integrity": "sha512-F4Gh2e+DGdaWdWT5NZduIFD9NM7Bnuh8sXARFHWPvIu7yvAwZ3ddqC9GK4F3qUngdmkJxDLWQqRSwSh96Lxbhw==",
+ "requires": {
+ "@chakra-ui/breakpoint-utils": "2.0.8",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/lazy-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz",
+ "integrity": "sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg=="
+ },
+ "@chakra-ui/live-region": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-2.0.13.tgz",
+ "integrity": "sha512-Ja+Slk6ZkxSA5oJzU2VuGU7TpZpbMb/4P4OUhIf2D30ctmIeXkxTWw1Bs1nGJAVtAPcGS5sKA+zb89i8g+0cTQ==",
+ "requires": {}
+ },
+ "@chakra-ui/media-query": {
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-3.2.12.tgz",
+ "integrity": "sha512-8pSLDf3oxxhFrhd40rs7vSeIBfvOmIKHA7DJlGUC/y+9irD24ZwgmCtFnn+y3gI47hTJsopbSX+wb8nr7XPswA==",
+ "requires": {
+ "@chakra-ui/breakpoint-utils": "2.0.8",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/menu": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-2.1.12.tgz",
+ "integrity": "sha512-ylNK1VJlr/3/EGg9dLPZ87cBJJjeiYXeU/gOAphsKXMnByrXWhbp4YVnyyyha2KZ0zEw0aPU4nCZ+A69aT9wrg==",
+ "requires": {
+ "@chakra-ui/clickable": "2.0.14",
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-animation-state": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-focus-effect": "2.0.9",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-outside-click": "2.0.7",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16"
+ }
+ },
+ "@chakra-ui/modal": {
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-2.2.11.tgz",
+ "integrity": "sha512-2J0ZUV5tEzkPiawdkgPz6bmex7NXAde1VXooMwdvK+vuT8PV3U61yorTJOZVLdw7TjjI1Yo94mzsp6UwBud43Q==",
+ "requires": {
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/focus-lock": "2.0.16",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/transition": "2.0.16",
+ "aria-hidden": "^1.2.2",
+ "react-remove-scroll": "^2.5.5"
+ }
+ },
+ "@chakra-ui/number-input": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-2.0.19.tgz",
+ "integrity": "sha512-HDaITvtMEqOauOrCPsARDxKD9PSHmhWywpcyCSOX0lMe4xx2aaGhU0QQFhsJsykj8Er6pytMv6t0KZksdDv3YA==",
+ "requires": {
+ "@chakra-ui/counter": "2.0.14",
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-interval": "2.0.5",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/number-utils": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz",
+ "integrity": "sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg=="
+ },
+ "@chakra-ui/object-utils": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/object-utils/-/object-utils-2.0.8.tgz",
+ "integrity": "sha512-2upjT2JgRuiupdrtBWklKBS6tqeGMA77Nh6Q0JaoQuH/8yq+15CGckqn3IUWkWoGI0Fg3bK9LDlbbD+9DLw95Q=="
+ },
+ "@chakra-ui/pin-input": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-2.0.20.tgz",
+ "integrity": "sha512-IHVmerrtHN8F+jRB3W1HnMir1S1TUCWhI7qDInxqPtoRffHt6mzZgLZ0izx8p1fD4HkW4c1d4/ZLEz9uH9bBRg==",
+ "requires": {
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/popover": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-2.1.9.tgz",
+ "integrity": "sha512-OMJ12VVs9N32tFaZSOqikkKPtwAVwXYsES/D1pff/amBrE3ngCrpxJSIp4uvTdORfIYDojJqrR52ZplDKS9hRQ==",
+ "requires": {
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-animation-state": "2.0.8",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-focus-effect": "2.0.9",
+ "@chakra-ui/react-use-focus-on-pointer-down": "2.0.6",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/popper": {
+ "version": "3.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-3.0.13.tgz",
+ "integrity": "sha512-FwtmYz80Ju8oK3Z1HQfisUE7JIMmDsCQsRBu6XuJ3TFQnBHit73yjZmxKjuRJ4JgyT4WBnZoTF3ATbRKSagBeg==",
+ "requires": {
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@popperjs/core": "^2.9.3"
+ }
+ },
+ "@chakra-ui/portal": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-2.0.16.tgz",
+ "integrity": "sha512-bVID0qbQ0l4xq38LdqAN4EKD4/uFkDnXzFwOlviC9sl0dNhzICDb1ltuH/Adl1d2HTMqyN60O3GO58eHy7plnQ==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ }
+ },
+ "@chakra-ui/progress": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-2.1.6.tgz",
+ "integrity": "sha512-hHh5Ysv4z6bK+j2GJbi/FT9CVyto2PtNUNwBmr3oNMVsoOUMoRjczfXvvYqp0EHr9PCpxqrq7sRwgQXUzhbDSw==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8"
+ }
+ },
+ "@chakra-ui/provider": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-2.2.2.tgz",
+ "integrity": "sha512-UVwnIDnAWq1aKroN5AF+OpNpUqLVeIUk7tKvX3z4CY9FsPFFi6LTEhRHdhpwaU1Tau3Tf9agEu5URegpY7S8BA==",
+ "requires": {
+ "@chakra-ui/css-reset": "2.1.1",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/system": "2.5.5",
+ "@chakra-ui/utils": "2.0.15"
+ }
+ },
+ "@chakra-ui/radio": {
+ "version": "2.0.22",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-2.0.22.tgz",
+ "integrity": "sha512-GsQ5WAnLwivWl6gPk8P1x+tCcpVakCt5R5T0HumF7DGPXKdJbjS+RaFySrbETmyTJsKY4QrfXn+g8CWVrMjPjw==",
+ "requires": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@zag-js/focus-visible": "0.2.2"
+ }
+ },
+ "@chakra-ui/react": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.5.5.tgz",
+ "integrity": "sha512-aBVMUtdWv2MrptD/tKSqICPsuJ+I+jvauegffO1qPUDlK3RrXIDeOHkLGWohgXNcjY5bGVWguFEzJm97//0ooQ==",
+ "requires": {
+ "@chakra-ui/accordion": "2.1.11",
+ "@chakra-ui/alert": "2.1.0",
+ "@chakra-ui/avatar": "2.2.8",
+ "@chakra-ui/breadcrumb": "2.1.5",
+ "@chakra-ui/button": "2.0.18",
+ "@chakra-ui/card": "2.1.6",
+ "@chakra-ui/checkbox": "2.2.14",
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/control-box": "2.0.13",
+ "@chakra-ui/counter": "2.0.14",
+ "@chakra-ui/css-reset": "2.1.1",
+ "@chakra-ui/editable": "2.0.21",
+ "@chakra-ui/focus-lock": "2.0.16",
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/hooks": "2.1.6",
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/image": "2.0.15",
+ "@chakra-ui/input": "2.0.21",
+ "@chakra-ui/layout": "2.1.18",
+ "@chakra-ui/live-region": "2.0.13",
+ "@chakra-ui/media-query": "3.2.12",
+ "@chakra-ui/menu": "2.1.12",
+ "@chakra-ui/modal": "2.2.11",
+ "@chakra-ui/number-input": "2.0.19",
+ "@chakra-ui/pin-input": "2.0.20",
+ "@chakra-ui/popover": "2.1.9",
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/progress": "2.1.6",
+ "@chakra-ui/provider": "2.2.2",
+ "@chakra-ui/radio": "2.0.22",
+ "@chakra-ui/react-env": "3.0.0",
+ "@chakra-ui/select": "2.0.19",
+ "@chakra-ui/skeleton": "2.0.24",
+ "@chakra-ui/slider": "2.0.23",
+ "@chakra-ui/spinner": "2.0.13",
+ "@chakra-ui/stat": "2.0.18",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/switch": "2.0.26",
+ "@chakra-ui/system": "2.5.5",
+ "@chakra-ui/table": "2.0.17",
+ "@chakra-ui/tabs": "2.1.9",
+ "@chakra-ui/tag": "3.0.0",
+ "@chakra-ui/textarea": "2.0.19",
+ "@chakra-ui/theme": "3.0.1",
+ "@chakra-ui/theme-utils": "2.0.15",
+ "@chakra-ui/toast": "6.1.1",
+ "@chakra-ui/tooltip": "2.2.7",
+ "@chakra-ui/transition": "2.0.16",
+ "@chakra-ui/utils": "2.0.15",
+ "@chakra-ui/visually-hidden": "2.0.15"
+ }
+ },
+ "@chakra-ui/react-children-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz",
+ "integrity": "sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==",
+ "requires": {}
+ },
+ "@chakra-ui/react-context": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-context/-/react-context-2.0.8.tgz",
+ "integrity": "sha512-tRTKdn6lCTXM6WPjSokAAKCw2ioih7Eg8cNgaYRSwKBck8nkz9YqxgIIEj3dJD7MGtpl24S/SNI98iRWkRwR/A==",
+ "requires": {}
+ },
+ "@chakra-ui/react-env": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-3.0.0.tgz",
+ "integrity": "sha512-tfMRO2v508HQWAqSADFrwZgR9oU10qC97oV6zGbjHh9ALP0/IcFR+Bi71KRTveDTm85fMeAzZYGj57P3Dsipkw==",
+ "requires": {
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5"
+ }
+ },
+ "@chakra-ui/react-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-types/-/react-types-2.0.7.tgz",
+ "integrity": "sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-animation-state": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.0.8.tgz",
+ "integrity": "sha512-xv9zSF2Rd1mHWQ+m5DLBWeh4atF8qrNvsOs3MNrvxKYBS3f79N3pqcQGrWAEvirXWXfiCeje2VAkEggqFRIo+Q==",
+ "requires": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "@chakra-ui/react-use-event-listener": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-callback-ref": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.0.7.tgz",
+ "integrity": "sha512-YjT76nTpfHAK5NxplAlZsQwNju5KmQExnqsWNPFeOR6vvbC34+iPSTr+r91i1Hdy7gBSbevsOsd5Wm6RN3GuMw==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-controllable-state": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.0.8.tgz",
+ "integrity": "sha512-F7rdCbLEmRjwwODqWZ3y+mKgSSHPcLQxeUygwk1BkZPXbKkJJKymOIjIynil2cbH7ku3hcSIWRvuhpCcfQWJ7Q==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-disclosure": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.0.8.tgz",
+ "integrity": "sha512-2ir/mHe1YND40e+FyLHnDsnDsBQPwzKDLzfe9GZri7y31oU83JSbHdlAXAhp3bpjohslwavtRCp+S/zRxfO9aQ==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-event-listener": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.0.7.tgz",
+ "integrity": "sha512-4wvpx4yudIO3B31pOrXuTHDErawmwiXnvAN7gLEOVREi16+YGNcFnRJ5X5nRrmB7j2MDUtsEDpRBFfw5Z9xQ5g==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-focus-effect": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.0.9.tgz",
+ "integrity": "sha512-20nfNkpbVwyb41q9wxp8c4jmVp6TUGAPE3uFTDpiGcIOyPW5aecQtPmTXPMJH+2aa8Nu1wyoT1btxO+UYiQM3g==",
+ "requires": {
+ "@chakra-ui/dom-utils": "2.0.6",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-focus-on-pointer-down": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.0.6.tgz",
+ "integrity": "sha512-OigXiLRVySn3tyVqJ/rn57WGuukW8TQe8fJYiLwXbcNyAMuYYounvRxvCy2b53sQ7QIZamza0N0jhirbH5FNoQ==",
+ "requires": {
+ "@chakra-ui/react-use-event-listener": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-interval": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-interval/-/react-use-interval-2.0.5.tgz",
+ "integrity": "sha512-1nbdwMi2K87V6p5f5AseOKif2CkldLaJlq1TOqaPRwb7v3aU9rltBtYdf+fIyuHSToNJUV6wd9budCFdLCl3Fg==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-latest-ref": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.0.5.tgz",
+ "integrity": "sha512-3mIuFzMyIo3Ok/D8uhV9voVg7KkrYVO/pwVvNPJOHsDQqCA6DpYE4WDsrIx+fVcwad3Ta7SupexR5PoI+kq6QQ==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-merge-refs": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.0.7.tgz",
+ "integrity": "sha512-zds4Uhsc+AMzdH8JDDkLVet9baUBgtOjPbhC5r3A0ZXjZvGhCztFAVE3aExYiVoMPoHLKbLcqvCWE6ioFKz1lw==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-outside-click": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.0.7.tgz",
+ "integrity": "sha512-MsAuGLkwYNxNJ5rb8lYNvXApXxYMnJ3MzqBpQj1kh5qP/+JSla9XMjE/P94ub4fSEttmNSqs43SmPPrmPuihsQ==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-pan-event": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.0.9.tgz",
+ "integrity": "sha512-xu35QXkiyrgsHUOnctl+SwNcwf9Rl62uYE5y8soKOZdBm8E+FvZIt2hxUzK1EoekbJCMzEZ0Yv1ZQCssVkSLaQ==",
+ "requires": {
+ "@chakra-ui/event-utils": "2.0.8",
+ "@chakra-ui/react-use-latest-ref": "2.0.5",
+ "framesync": "6.1.2"
+ }
+ },
+ "@chakra-ui/react-use-previous": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-previous/-/react-use-previous-2.0.5.tgz",
+ "integrity": "sha512-BIZgjycPE4Xr+MkhKe0h67uHXzQQkBX/u5rYPd65iMGdX1bCkbE0oorZNfOHLKdTmnEb4oVsNvfN6Rfr+Mnbxw==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-safe-layout-effect": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.0.5.tgz",
+ "integrity": "sha512-MwAQBz3VxoeFLaesaSEN87reVNVbjcQBDex2WGexAg6hUB6n4gc1OWYH/iXp4tzp4kuggBNhEHkk9BMYXWfhJQ==",
+ "requires": {}
+ },
+ "@chakra-ui/react-use-size": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-size/-/react-use-size-2.0.10.tgz",
+ "integrity": "sha512-fdIkH14GDnKQrtQfxX8N3gxbXRPXEl67Y3zeD9z4bKKcQUAYIMqs0MsPZY+FMpGQw8QqafM44nXfL038aIrC5w==",
+ "requires": {
+ "@zag-js/element-size": "0.3.2"
+ }
+ },
+ "@chakra-ui/react-use-timeout": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-timeout/-/react-use-timeout-2.0.5.tgz",
+ "integrity": "sha512-QqmB+jVphh3h/CS60PieorpY7UqSPkrQCB7f7F+i9vwwIjtP8fxVHMmkb64K7VlzQiMPzv12nlID5dqkzlv0mw==",
+ "requires": {
+ "@chakra-ui/react-use-callback-ref": "2.0.7"
+ }
+ },
+ "@chakra-ui/react-use-update-effect": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.0.7.tgz",
+ "integrity": "sha512-vBM2bmmM83ZdDtasWv3PXPznpTUd+FvqBC8J8rxoRmvdMEfrxTiQRBJhiGHLpS9BPLLPQlosN6KdFU97csB6zg==",
+ "requires": {}
+ },
+ "@chakra-ui/react-utils": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz",
+ "integrity": "sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==",
+ "requires": {
+ "@chakra-ui/utils": "2.0.15"
+ }
+ },
+ "@chakra-ui/select": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-2.0.19.tgz",
+ "integrity": "sha512-eAlFh+JhwtJ17OrB6fO6gEAGOMH18ERNrXLqWbYLrs674Le7xuREgtuAYDoxUzvYXYYTTdOJtVbcHGriI3o6rA==",
+ "requires": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/shared-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz",
+ "integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q=="
+ },
+ "@chakra-ui/skeleton": {
+ "version": "2.0.24",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-2.0.24.tgz",
+ "integrity": "sha512-1jXtVKcl/jpbrJlc/TyMsFyI651GTXY5ma30kWyTXoby2E+cxbV6OR8GB/NMZdGxbQBax8/VdtYVjI0n+OBqWA==",
+ "requires": {
+ "@chakra-ui/media-query": "3.2.12",
+ "@chakra-ui/react-use-previous": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/slider": {
+ "version": "2.0.23",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-2.0.23.tgz",
+ "integrity": "sha512-/eyRUXLla+ZdBUPXpakE3SAS2JS8mIJR6qcUYiPVKSpRAi6tMyYeQijAXn2QC1AUVd2JrG8Pz+1Jy7Po3uA7cA==",
+ "requires": {
+ "@chakra-ui/number-utils": "2.0.7",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-callback-ref": "2.0.7",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-latest-ref": "2.0.5",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-pan-event": "2.0.9",
+ "@chakra-ui/react-use-size": "2.0.10",
+ "@chakra-ui/react-use-update-effect": "2.0.7"
+ }
+ },
+ "@chakra-ui/spinner": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-2.0.13.tgz",
+ "integrity": "sha512-T1/aSkVpUIuiYyrjfn1+LsQEG7Onbi1UE9ccS/evgf61Dzy4GgTXQUnDuWFSgpV58owqirqOu6jn/9eCwDlzlg==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/stat": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-2.0.18.tgz",
+ "integrity": "sha512-wKyfBqhVlIs9bkSerUc6F9KJMw0yTIEKArW7dejWwzToCLPr47u+CtYO6jlJHV6lRvkhi4K4Qc6pyvtJxZ3VpA==",
+ "requires": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/styled-system": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.8.0.tgz",
+ "integrity": "sha512-bmRv/8ACJGGKGx84U1npiUddwdNifJ+/ETklGwooS5APM0ymwUtBYZpFxjYNJrqvVYpg3mVY6HhMyBVptLS7iA==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5",
+ "csstype": "^3.0.11",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "@chakra-ui/switch": {
+ "version": "2.0.26",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-2.0.26.tgz",
+ "integrity": "sha512-x62lF6VazSZJQuVxosChVR6+0lIJe8Pxgkl/C9vxjhp2yVYb3mew5tcX/sDOu0dYZy8ro/9hMfGkdN4r9xEU8A==",
+ "requires": {
+ "@chakra-ui/checkbox": "2.2.14",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/system": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.5.5.tgz",
+ "integrity": "sha512-52BIp/Zyvefgxn5RTByfkTeG4J+y81LWEjWm8jCaRFsLVm8IFgqIrngtcq4I7gD5n/UKbneHlb4eLHo4uc5yDQ==",
+ "requires": {
+ "@chakra-ui/color-mode": "2.1.12",
+ "@chakra-ui/object-utils": "2.0.8",
+ "@chakra-ui/react-utils": "2.0.12",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme-utils": "2.0.15",
+ "@chakra-ui/utils": "2.0.15",
+ "react-fast-compare": "3.2.1"
+ }
+ },
+ "@chakra-ui/table": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-2.0.17.tgz",
+ "integrity": "sha512-OScheTEp1LOYvTki2NFwnAYvac8siAhW9BI5RKm5f5ORL2gVJo4I72RUqE0aKe1oboxgm7CYt5afT5PS5cG61A==",
+ "requires": {
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/tabs": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-2.1.9.tgz",
+ "integrity": "sha512-Yf8e0kRvaGM6jfkJum0aInQ0U3ZlCafmrYYni2lqjcTtThqu+Yosmo3iYlnullXxCw5MVznfrkb9ySvgQowuYg==",
+ "requires": {
+ "@chakra-ui/clickable": "2.0.14",
+ "@chakra-ui/descendant": "3.0.14",
+ "@chakra-ui/lazy-utils": "2.0.5",
+ "@chakra-ui/react-children-utils": "2.0.6",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-controllable-state": "2.0.8",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/react-use-safe-layout-effect": "2.0.5",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/tag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-3.0.0.tgz",
+ "integrity": "sha512-YWdMmw/1OWRwNkG9pX+wVtZio+B89odaPj6XeMn5nfNN8+jyhIEpouWv34+CO9G0m1lupJTxPSfgLAd7cqXZMA==",
+ "requires": {
+ "@chakra-ui/icon": "3.0.16",
+ "@chakra-ui/react-context": "2.0.8"
+ }
+ },
+ "@chakra-ui/textarea": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-2.0.19.tgz",
+ "integrity": "sha512-adJk+qVGsFeJDvfn56CcJKKse8k7oMGlODrmpnpTdF+xvlsiTM+1GfaJvgNSpHHuQFdz/A0z1uJtfGefk0G2ZA==",
+ "requires": {
+ "@chakra-ui/form-control": "2.0.18",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/theme": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-3.0.1.tgz",
+ "integrity": "sha512-92kDm/Ux/51uJqhRKevQo/O/rdwucDYcpHg2QuwzdAxISCeYvgtl2TtgOOl5EnqEP0j3IEAvZHZUlv8TTbawaw==",
+ "requires": {
+ "@chakra-ui/anatomy": "2.1.2",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/theme-tools": "2.0.17"
+ }
+ },
+ "@chakra-ui/theme-tools": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-2.0.17.tgz",
+ "integrity": "sha512-Auu38hnihlJZQcPok6itRDBbwof3TpXGYtDPnOvrq4Xp7jnab36HLt7KEXSDPXbtOk3ZqU99pvI1en5LbDrdjg==",
+ "requires": {
+ "@chakra-ui/anatomy": "2.1.2",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "color2k": "^2.0.0"
+ }
+ },
+ "@chakra-ui/theme-utils": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/theme-utils/-/theme-utils-2.0.15.tgz",
+ "integrity": "sha512-UuxtEgE7gwMTGDXtUpTOI7F5X0iHB9ekEOG5PWPn2wWBL7rlk2JtPI7UP5Um5Yg6vvBfXYGK1ySahxqsgf+87g==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme": "3.0.1",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "@chakra-ui/toast": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-6.1.1.tgz",
+ "integrity": "sha512-JtjIKkPVjEu8okGGCipCxNVgK/15h5AicTATZ6RbG2MsHmr4GfKG3fUCvpbuZseArqmLqGLQZQJjVE9vJzaSkQ==",
+ "requires": {
+ "@chakra-ui/alert": "2.1.0",
+ "@chakra-ui/close-button": "2.0.17",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-context": "2.0.8",
+ "@chakra-ui/react-use-timeout": "2.0.5",
+ "@chakra-ui/react-use-update-effect": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5",
+ "@chakra-ui/styled-system": "2.8.0",
+ "@chakra-ui/theme": "3.0.1"
+ }
+ },
+ "@chakra-ui/tooltip": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-2.2.7.tgz",
+ "integrity": "sha512-ImUJ6NnVqARaYqpgtO+kzucDRmxo8AF3jMjARw0bx2LxUkKwgRCOEaaRK5p5dHc0Kr6t5/XqjDeUNa19/sLauA==",
+ "requires": {
+ "@chakra-ui/popper": "3.0.13",
+ "@chakra-ui/portal": "2.0.16",
+ "@chakra-ui/react-types": "2.0.7",
+ "@chakra-ui/react-use-disclosure": "2.0.8",
+ "@chakra-ui/react-use-event-listener": "2.0.7",
+ "@chakra-ui/react-use-merge-refs": "2.0.7",
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/transition": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-2.0.16.tgz",
+ "integrity": "sha512-E+RkwlPc3H7P1crEXmXwDXMB2lqY2LLia2P5siQ4IEnRWIgZXlIw+8Em+NtHNgusel2N+9yuB0wT9SeZZeZ3CQ==",
+ "requires": {
+ "@chakra-ui/shared-utils": "2.0.5"
+ }
+ },
+ "@chakra-ui/utils": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.15.tgz",
+ "integrity": "sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==",
+ "requires": {
+ "@types/lodash.mergewith": "4.6.7",
+ "css-box-model": "1.2.1",
+ "framesync": "6.1.2",
+ "lodash.mergewith": "4.6.2"
+ }
+ },
+ "@chakra-ui/visually-hidden": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-2.0.15.tgz",
+ "integrity": "sha512-WWULIiucYRBIewHKFA7BssQ2ABLHLVd9lrUo3N3SZgR0u4ZRDDVEUNOy+r+9ruDze8+36dGbN9wsN1IdELtdOw==",
+ "requires": {}
+ },
+ "@emotion/babel-plugin": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",
+ "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/serialize": "^1.1.1",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.1.3"
+ }
+ },
+ "@emotion/cache": {
+ "version": "11.10.5",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
+ "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
+ "requires": {
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/sheet": "^1.2.1",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "stylis": "4.1.3"
+ }
+ },
+ "@emotion/hash": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
+ "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
+ },
+ "@emotion/is-prop-valid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz",
+ "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==",
+ "requires": {
+ "@emotion/memoize": "^0.8.0"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
+ "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
+ },
+ "@emotion/react": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz",
+ "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==",
+ "requires": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/cache": "^11.10.5",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "hoist-non-react-statics": "^3.3.1"
+ }
+ },
+ "@emotion/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
+ "requires": {
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/unitless": "^0.8.0",
+ "@emotion/utils": "^1.2.0",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@emotion/sheet": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
+ "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
+ },
+ "@emotion/styled": {
+ "version": "11.10.6",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz",
+ "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==",
+ "requires": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0"
+ }
+ },
+ "@emotion/unitless": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
+ "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
+ },
+ "@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
+ "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
+ "requires": {}
+ },
+ "@emotion/utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
+ "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
+ },
+ "@emotion/weak-memoize": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
+ "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
+ },
+ "@esbuild/android-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
+ "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
+ "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "dev": true,
+ "optional": true
+ },
+ "@fastify/busboy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
+ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA=="
+ },
+ "@heroicons/react": {
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.16.tgz",
+ "integrity": "sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==",
+ "requires": {}
+ },
+ "@hookform/resolvers": {
+ "version": "2.9.11",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz",
+ "integrity": "sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==",
+ "requires": {}
+ },
+ "@jridgewell/gen-mapping": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
+ "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.0",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true
+ },
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+ "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "@motionone/animation": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",
+ "integrity": "sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==",
+ "requires": {
+ "@motionone/easing": "^10.15.1",
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "@motionone/dom": {
+ "version": "10.15.5",
+ "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.15.5.tgz",
+ "integrity": "sha512-Xc5avlgyh3xukU9tydh9+8mB8+2zAq+WlLsC3eEIp7Ax7DnXgY7Bj/iv0a4X2R9z9ZFZiaXK3BO0xMYHKbAAdA==",
+ "requires": {
+ "@motionone/animation": "^10.15.1",
+ "@motionone/generators": "^10.15.1",
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "hey-listen": "^1.0.8",
+ "tslib": "^2.3.1"
+ }
+ },
+ "@motionone/easing": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.15.1.tgz",
+ "integrity": "sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==",
+ "requires": {
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "@motionone/generators": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.15.1.tgz",
+ "integrity": "sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==",
+ "requires": {
+ "@motionone/types": "^10.15.1",
+ "@motionone/utils": "^10.15.1",
+ "tslib": "^2.3.1"
+ }
+ },
+ "@motionone/types": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.15.1.tgz",
+ "integrity": "sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA=="
+ },
+ "@motionone/utils": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.15.1.tgz",
+ "integrity": "sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==",
+ "requires": {
+ "@motionone/types": "^10.15.1",
+ "hey-listen": "^1.0.8",
+ "tslib": "^2.3.1"
+ }
+ },
+ "@popperjs/core": {
+ "version": "2.11.6",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+ "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
+ },
+ "@remix-run/router": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz",
+ "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA=="
+ },
+ "@rollup/pluginutils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
+ "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "dev": true,
+ "requires": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "@sphinxxxx/color-conversion": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
+ "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
+ },
+ "@svgr/babel-plugin-add-jsx-attribute": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz",
+ "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-remove-jsx-attribute": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz",
+ "integrity": "sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-remove-jsx-empty-expression": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz",
+ "integrity": "sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-replace-jsx-attribute-value": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz",
+ "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-svg-dynamic-title": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz",
+ "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-svg-em-dimensions": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz",
+ "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-transform-react-native-svg": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz",
+ "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-plugin-transform-svg-component": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz",
+ "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "@svgr/babel-preset": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz",
+ "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==",
+ "dev": true,
+ "requires": {
+ "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1",
+ "@svgr/babel-plugin-remove-jsx-attribute": "*",
+ "@svgr/babel-plugin-remove-jsx-empty-expression": "*",
+ "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1",
+ "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1",
+ "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1",
+ "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1",
+ "@svgr/babel-plugin-transform-svg-component": "^6.5.1"
+ }
+ },
+ "@svgr/core": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz",
+ "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==",
+ "dev": true,
+ "requires": {
+ "@babel/core": "^7.19.6",
+ "@svgr/babel-preset": "^6.5.1",
+ "@svgr/plugin-jsx": "^6.5.1",
+ "camelcase": "^6.2.0",
+ "cosmiconfig": "^7.0.1"
+ }
+ },
+ "@svgr/hast-util-to-babel-ast": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz",
+ "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.20.0",
+ "entities": "^4.4.0"
+ }
+ },
+ "@svgr/plugin-jsx": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz",
+ "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==",
+ "dev": true,
+ "requires": {
+ "@babel/core": "^7.19.6",
+ "@svgr/babel-preset": "^6.5.1",
+ "@svgr/hast-util-to-babel-ast": "^6.5.1",
+ "svg-parser": "^2.0.4"
+ }
+ },
+ "@types/ace": {
+ "version": "0.0.48",
+ "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
+ "integrity": "sha512-esV6hOWiDOZ6d7w5S11iLu6LQsPGe/9RPzhri7gNNLdrK1LFpO9/m7IZhQL6dat0JHICJ7l51zvHAiCgnPLLHA==",
+ "dev": true
+ },
+ "@types/estree": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
+ "dev": true
+ },
+ "@types/jsoneditor": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@types/jsoneditor/-/jsoneditor-9.9.0.tgz",
+ "integrity": "sha512-OwDYzMuqCn+QZHQPMDu7miLVtnJueB2lQ42KmX1mZNZTaZxV8v55wiL1sit7cZGA5BzhNWVw8KAHhEoFWYyW3g==",
+ "dev": true,
+ "requires": {
+ "@types/ace": "*",
+ "ajv": "^6.12.0"
+ }
+ },
+ "@types/lodash": {
+ "version": "4.14.191",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
+ "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
+ },
+ "@types/lodash.debounce": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz",
+ "integrity": "sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
+ "@types/lodash.mergewith": {
+ "version": "4.6.7",
+ "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz",
+ "integrity": "sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==",
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
+ "@types/node": {
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz",
+ "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
+ "@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
+ "devOptional": true
+ },
+ "@types/react": {
+ "version": "18.0.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
+ "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
+ "devOptional": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-copy-to-clipboard": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
+ "integrity": "sha512-otTJsJpofYAeaIeOwV5xBUGpo6exXG2HX7X4nseToCB2VgPEBxGBHCm/FecZ676doNR7HCSTVtmohxfG2b3/yQ==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-datepicker": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.8.0.tgz",
+ "integrity": "sha512-20uzZsIf4moPAjjHDfPvH8UaOHZBxrkiQZoLS3wgKq8Xhp+95gdercLEdoA7/I8nR9R5Jz2qQkdMIM+Lq4AS1A==",
+ "dev": true,
+ "requires": {
+ "@popperjs/core": "^2.9.2",
+ "@types/react": "*",
+ "date-fns": "^2.0.1",
+ "react-popper": "^2.2.5"
+ }
+ },
+ "@types/react-dom": {
+ "version": "18.0.11",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz",
+ "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-json-editor-ajrm": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/@types/react-json-editor-ajrm/-/react-json-editor-ajrm-2.5.3.tgz",
+ "integrity": "sha512-hHf3FU1HjL6Lm3rK2iWUJtoYpZ1qDCSffnLAQXCf++TP1Ms18rnB06gBK3psQ8NEW6CfrfRNlirIL3D/P80QVg==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-slick": {
+ "version": "0.23.10",
+ "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.10.tgz",
+ "integrity": "sha512-ZiqdencANDZy6sWOWJ54LDvebuXFEhDlHtXU9FFipQR2BcYU2QJxZhvJPW6YK7cocibUiNn+YvDTbt1HtCIBVA==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
+ "devOptional": true
+ },
+ "@vitejs/plugin-react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz",
+ "integrity": "sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==",
+ "dev": true,
+ "requires": {
+ "@babel/core": "^7.19.6",
+ "@babel/plugin-transform-react-jsx": "^7.19.0",
+ "@babel/plugin-transform-react-jsx-development": "^7.18.6",
+ "@babel/plugin-transform-react-jsx-self": "^7.18.6",
+ "@babel/plugin-transform-react-jsx-source": "^7.19.6",
+ "magic-string": "^0.26.7",
+ "react-refresh": "^0.14.0"
+ }
+ },
+ "@zag-js/element-size": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.3.2.tgz",
+ "integrity": "sha512-bVvvigUGvAuj7PCkE5AbzvTJDTw5f3bg9nQdv+ErhVN8SfPPppLJEmmWdxqsRzrHXgx8ypJt/+Ty0kjtISVDsQ=="
+ },
+ "@zag-js/focus-visible": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-0.2.2.tgz",
+ "integrity": "sha512-0j2gZq8HiZ51z4zNnSkF1iSkqlwRDvdH+son3wHdoz+7IUdMN/5Exd4TxMJ+gq2Of1DiXReYLL9qqh2PdQ4wgA=="
+ },
+ "ace-builds": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.20.0.tgz",
+ "integrity": "sha512-JUvWbRXz7tL3EkcM5CpVyyuu9mvVRo594ZRgwHzb9arb9s2o5gTg94Lrk3zQZzM1udq6fRSDnmU2RClJPt9aNw=="
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "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==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "apexcharts": {
+ "version": "3.40.0",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.40.0.tgz",
+ "integrity": "sha512-dSi3BUfCJkFd67uFp+xffrJVd3lDT7AAUUyRp0qPYiglJ76CeZLddVhM3FAk1P9GCzf8VewqGYUPCYQvXm+b9A==",
+ "requires": {
+ "svg.draggable.js": "^2.2.2",
+ "svg.easing.js": "^2.0.0",
+ "svg.filter.js": "^2.0.2",
+ "svg.pathmorphing.js": "^0.1.3",
+ "svg.resize.js": "^1.4.3",
+ "svg.select.js": "^3.0.1"
+ }
+ },
+ "aria-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz",
+ "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "autoprefixer": {
+ "version": "10.4.13",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
+ "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.21.4",
+ "caniuse-lite": "^1.0.30001426",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "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==",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "broadcast-channel": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+ "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "detect-node": "^2.1.0",
+ "js-sha3": "0.8.0",
+ "microseconds": "0.2.0",
+ "nano-time": "1.0.0",
+ "oblivious-set": "1.0.0",
+ "rimraf": "3.0.2",
+ "unload": "2.2.0"
+ }
+ },
+ "browserslist": {
+ "version": "4.21.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
+ "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001449",
+ "electron-to-chromium": "^1.4.284",
+ "node-releases": "^2.0.8",
+ "update-browserslist-db": "^1.0.10"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+ },
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001458",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
+ "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "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=="
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.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"
+ }
+ },
+ "classnames": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+ "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+ },
+ "clear-any-console": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/clear-any-console/-/clear-any-console-1.16.2.tgz",
+ "integrity": "sha512-OL/7wZpNy9x0GBSzz3poWja84Nr7iaH8aYNsJ5Uet2BVLj6Lm1zvWpZN/yH46Vv3ae7YfHmLLMmfHj911fshJg==",
+ "dev": true
+ },
+ "cli-check-node": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/cli-check-node/-/cli-check-node-1.3.4.tgz",
+ "integrity": "sha512-iLGgQXm82iP8eH3R67qbOWs5qqUOLmNnMy5Lzl/RybcMh3y+H2zWU5POzuQ6oDUOdz4XWuxcFhP75szqd6frLg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^3.0.0",
+ "log-symbols": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "cli-handle-error": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/cli-handle-error/-/cli-handle-error-4.4.0.tgz",
+ "integrity": "sha512-RyBCnKlc7xVr79cKb9RfBq+4fjwQeX8HKeNzIPnI/W+DWWIUUKh2ur576DpwJ3kZt2UGHlIAOF7N9txy+mgZsA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^3.0.0",
+ "log-symbols": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "cli-handle-unhandled": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cli-handle-unhandled/-/cli-handle-unhandled-1.1.1.tgz",
+ "integrity": "sha512-Em91mJvU7VdgT2MxQpyY633vW1tDzRjPDbii6ZjEBHHLLh0xDoVkFt/wjvi9nSvJcz9rJmvtJSK8KL/hvF0Stg==",
+ "dev": true,
+ "requires": {
+ "cli-handle-error": "^4.1.0"
+ }
+ },
+ "cli-welcome": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/cli-welcome/-/cli-welcome-2.2.2.tgz",
+ "integrity": "sha512-LgDGS0TW4nIf8v81wpuZzfOEDPcy68u0jKR0Fy5IaWftqdminI6FoDiMFt1mjPylqKGNv/wFsZ7fCs93IeDMIw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "clear-any-console": "^1.16.0",
+ "prettier": "^2.0.5"
+ }
+ },
+ "cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "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==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "color2k": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.2.tgz",
+ "integrity": "sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w=="
+ },
+ "commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true
+ },
+ "compute-scroll-into-view": {
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+ "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "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=="
+ },
+ "copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "requires": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "requires": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ }
+ },
+ "cross-fetch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
+ "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+ "requires": {
+ "node-fetch": "2.6.7"
+ }
+ },
+ "css-box-model": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+ "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+ "requires": {
+ "tiny-invariant": "^1.0.6"
+ }
+ },
+ "csstype": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
+ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ },
+ "date-fns": {
+ "version": "2.29.3",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+ "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
+ },
+ "dayjs": {
+ "version": "1.11.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+ "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "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=="
+ },
+ "destr": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz",
+ "integrity": "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA=="
+ },
+ "detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
+ },
+ "detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
+ "electron-to-chromium": {
+ "version": "1.4.311",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz",
+ "integrity": "sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==",
+ "dev": true
+ },
+ "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=="
+ },
+ "enquire.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
+ "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw=="
+ },
+ "entities": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
+ "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
+ "dev": true
+ },
+ "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==",
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "esbuild": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
+ "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.15.18",
+ "@esbuild/linux-loong64": "0.15.18",
+ "esbuild-android-64": "0.15.18",
+ "esbuild-android-arm64": "0.15.18",
+ "esbuild-darwin-64": "0.15.18",
+ "esbuild-darwin-arm64": "0.15.18",
+ "esbuild-freebsd-64": "0.15.18",
+ "esbuild-freebsd-arm64": "0.15.18",
+ "esbuild-linux-32": "0.15.18",
+ "esbuild-linux-64": "0.15.18",
+ "esbuild-linux-arm": "0.15.18",
+ "esbuild-linux-arm64": "0.15.18",
+ "esbuild-linux-mips64le": "0.15.18",
+ "esbuild-linux-ppc64le": "0.15.18",
+ "esbuild-linux-riscv64": "0.15.18",
+ "esbuild-linux-s390x": "0.15.18",
+ "esbuild-netbsd-64": "0.15.18",
+ "esbuild-openbsd-64": "0.15.18",
+ "esbuild-sunos-64": "0.15.18",
+ "esbuild-windows-32": "0.15.18",
+ "esbuild-windows-64": "0.15.18",
+ "esbuild-windows-arm64": "0.15.18"
+ }
+ },
+ "esbuild-android-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
+ "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-android-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
+ "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
+ "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
+ "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
+ "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
+ "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
+ "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
+ "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
+ "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
+ "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-mips64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
+ "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-ppc64le": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
+ "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-riscv64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
+ "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-s390x": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
+ "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-netbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
+ "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-openbsd-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
+ "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-sunos-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
+ "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-32": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
+ "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
+ "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-arm64": {
+ "version": "0.15.18",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
+ "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
+ "dev": true,
+ "optional": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
+ },
+ "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=="
+ },
+ "estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
+ "focus-lock": {
+ "version": "0.11.6",
+ "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.6.tgz",
+ "integrity": "sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==",
+ "requires": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "dev": true
+ },
+ "framer-motion": {
+ "version": "7.10.3",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-7.10.3.tgz",
+ "integrity": "sha512-k2ccYeZNSpPg//HTaqrU+4pRq9f9ZpaaN7rr0+Rx5zA4wZLbk547wtDzge2db1sB+1mnJ6r59P4xb+aEIi/W+w==",
+ "requires": {
+ "@emotion/is-prop-valid": "^0.8.2",
+ "@motionone/dom": "^10.15.3",
+ "hey-listen": "^1.0.8",
+ "tslib": "2.4.0"
+ },
+ "dependencies": {
+ "@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "optional": true,
+ "requires": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
+ "optional": true
+ },
+ "tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+ }
+ }
+ },
+ "framesync": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
+ "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
+ "requires": {
+ "tslib": "2.4.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+ }
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "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==",
+ "dev": true
+ },
+ "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=="
+ },
+ "get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
+ },
+ "github-buttons": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/github-buttons/-/github-buttons-2.24.0.tgz",
+ "integrity": "sha512-dHpEno4S2JHjsEKPLSTGDTV50q1d08n3H7VDfNNA1m8cz5DMqLCZDdKWQxy4zyvd6M3tKvlVYLs+/H0BF3DggA=="
+ },
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "requires": {
+ "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"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
+ },
+ "globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "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=="
+ },
+ "hey-listen": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
+ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
+ },
+ "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==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "requires": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "i18next": {
+ "version": "22.4.15",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz",
+ "integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==",
+ "requires": {
+ "@babel/runtime": "^7.20.6"
+ }
+ },
+ "i18next-browser-languagedetector": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz",
+ "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==",
+ "requires": {
+ "@babel/runtime": "^7.19.4"
+ }
+ },
+ "i18next-http-backend": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.0.tgz",
+ "integrity": "sha512-Z4sM7R6tzdLknSPER9GisEBxKPg5FkI07UrQniuroZmS15PHQrcCPLyuGKj8SS68tf+O2aEDYSUnmy1TZqZSbw==",
+ "requires": {
+ "cross-fetch": "3.1.5"
+ }
+ },
+ "immutable": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
+ "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
+ },
+ "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=="
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "requires": {
+ "is-docker": "^2.0.0"
+ }
+ },
+ "javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
+ },
+ "jmespath": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+ "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
+ },
+ "jquery": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
+ "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
+ "peer": true
+ },
+ "js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "json-source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.6.1.tgz",
+ "integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="
+ },
+ "json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+ "requires": {
+ "string-convert": "^0.2.0"
+ }
+ },
+ "json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true
+ },
+ "jsoneditor": {
+ "version": "9.10.1",
+ "resolved": "https://registry.npmjs.org/jsoneditor/-/jsoneditor-9.10.1.tgz",
+ "integrity": "sha512-vequCPWX+dNndl25+wlniQAaEgJ2Vy+X44a8GL3kOfPrbF9E/ZFH5XEpwyltI1kr95YFTp0PISuwmgwJHpQx4g==",
+ "requires": {
+ "ace-builds": "^1.15.2",
+ "ajv": "^6.12.6",
+ "javascript-natural-sort": "^0.7.1",
+ "jmespath": "^0.16.0",
+ "json-source-map": "^0.6.1",
+ "jsonrepair": "^3.0.2",
+ "mobius1-selectr": "^2.4.13",
+ "picomodal": "^3.0.0",
+ "vanilla-picker": "^2.12.1"
+ }
+ },
+ "jsonrepair": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.1.0.tgz",
+ "integrity": "sha512-idqReg23J0PVRAADmZMc5xQM3xeOX5bTB6OTyMnzq33IXJXmn9iJuWIEvGmrN80rQf4d7uLTMEDwpzujNcI0Rg=="
+ },
+ "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=="
+ },
+ "lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
+ },
+ "lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
+ },
+ "log-symbols": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+ "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2"
+ }
+ },
+ "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==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "magic-string": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
+ "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "match-sorter": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
+ "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "remove-accents": "0.4.2"
+ }
+ },
+ "microseconds": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+ "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "mobius1-selectr": {
+ "version": "2.4.13",
+ "resolved": "https://registry.npmjs.org/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz",
+ "integrity": "sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw=="
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "nano-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+ "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+ "requires": {
+ "big-integer": "^1.6.16"
+ }
+ },
+ "nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true
+ },
+ "node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "node-fetch-native": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-0.1.8.tgz",
+ "integrity": "sha512-ZNaury9r0NxaT2oL65GvdGDy+5PlSaHTovT6JV5tOW07k1TQmgC0olZETa4C9KZg0+6zBr99ctTYa3Utqj9P/Q=="
+ },
+ "node-releases": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
+ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
+ "dev": true
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "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==",
+ "dev": true
+ },
+ "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=="
+ },
+ "oblivious-set": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+ "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+ },
+ "ohmyfetch": {
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/ohmyfetch/-/ohmyfetch-0.4.21.tgz",
+ "integrity": "sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw==",
+ "requires": {
+ "destr": "^1.2.0",
+ "node-fetch-native": "^0.1.8",
+ "ufo": "^0.8.6",
+ "undici": "^5.12.0"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "requires": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ }
+ },
+ "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==",
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "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==",
+ "requires": {
+ "@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"
+ }
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "picomodal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/picomodal/-/picomodal-3.0.0.tgz",
+ "integrity": "sha512-FoR3TDfuLlqUvcEeK5ifpKSVVns6B4BQvc8SDF6THVMuadya6LLtji0QgUDSStw0ZR2J7I6UGi5V2V23rnPWTw=="
+ },
+ "postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "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==",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true
+ },
+ "prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
+ },
+ "qrcode.react": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz",
+ "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==",
+ "requires": {}
+ },
+ "react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "react-apexcharts": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz",
+ "integrity": "sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==",
+ "requires": {
+ "prop-types": "^15.5.7"
+ }
+ },
+ "react-clientside-effect": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
+ "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
+ "requires": {
+ "@babel/runtime": "^7.12.13"
+ }
+ },
+ "react-copy-to-clipboard": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
+ "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
+ "requires": {
+ "copy-to-clipboard": "^3.3.1",
+ "prop-types": "^15.8.1"
+ }
+ },
+ "react-datepicker": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.10.0.tgz",
+ "integrity": "sha512-6IfBCZyWj54ZZGLmEZJ9c4Yph0s9MVfEGDC2evOvf9AmVz+RRcfP2Czqad88Ff9wREbcbqa4dk7IFYeXF1d3Ag==",
+ "requires": {
+ "@popperjs/core": "^2.9.2",
+ "classnames": "^2.2.6",
+ "date-fns": "^2.24.0",
+ "prop-types": "^15.7.2",
+ "react-onclickoutside": "^6.12.2",
+ "react-popper": "^2.3.0"
+ }
+ },
+ "react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-fast-compare": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
+ "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
+ },
+ "react-focus-lock": {
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz",
+ "integrity": "sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "focus-lock": "^0.11.6",
+ "prop-types": "^15.6.2",
+ "react-clientside-effect": "^1.2.6",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ }
+ },
+ "react-github-btn": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz",
+ "integrity": "sha512-lV4FYClAfjWnBfv0iNlJUGhamDgIq6TayD0kPZED6VzHWdpcHmPfsYOZ/CFwLfPv4Zp+F4m8QKTj0oy2HjiGXg==",
+ "requires": {
+ "github-buttons": "^2.22.0"
+ }
+ },
+ "react-hook-form": {
+ "version": "7.43.2",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.2.tgz",
+ "integrity": "sha512-NvD3Oe2Y9hhqo2R4I4iJigDzSLpdMnzUpNMxlnzTbdiT7NT3BW0GxWCzEtwPudZMUPbZhNcSy1EcGAygyhDORg==",
+ "requires": {}
+ },
+ "react-i18next": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz",
+ "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==",
+ "requires": {
+ "@babel/runtime": "^7.20.6",
+ "html-parse-stringify": "^3.0.1"
+ }
+ },
+ "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=="
+ },
+ "react-json-editor-ajrm": {
+ "version": "2.5.14",
+ "resolved": "https://registry.npmjs.org/react-json-editor-ajrm/-/react-json-editor-ajrm-2.5.14.tgz",
+ "integrity": "sha512-z2035l5lnU0Wo73AUonj68d1Eh9vjVcLXEa8YogDEeFPcogzYkfT6R38fvYlLKDhP0oI+LyOO/IBWsFkXs4M0Q==",
+ "requires": {
+ "@babel/runtime": "^7.0.0-rc.0"
+ }
+ },
+ "react-loading-skeleton": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.1.tgz",
+ "integrity": "sha512-gQeNzzKXlevvquRDOhN5kas5VE7toTKrpmE9BpUdf0iip9c5ae0LcVbulh5ebO2CFl4RrYsyLuDtYGyrh1h1iA==",
+ "requires": {}
+ },
+ "react-onclickoutside": {
+ "version": "6.12.2",
+ "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
+ "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+ "requires": {}
+ },
+ "react-popper": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
+ "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
+ "requires": {
+ "react-fast-compare": "^3.0.1",
+ "warning": "^4.0.2"
+ }
+ },
+ "react-query": {
+ "version": "3.39.3",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+ "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "broadcast-channel": "^3.4.1",
+ "match-sorter": "^6.0.2"
+ }
+ },
+ "react-refresh": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
+ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+ "dev": true
+ },
+ "react-remove-scroll": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
+ "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
+ "requires": {
+ "react-remove-scroll-bar": "^2.3.3",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ }
+ },
+ "react-remove-scroll-bar": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz",
+ "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==",
+ "requires": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ }
+ },
+ "react-router": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz",
+ "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==",
+ "requires": {
+ "@remix-run/router": "1.3.2"
+ }
+ },
+ "react-router-dom": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz",
+ "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==",
+ "requires": {
+ "@remix-run/router": "1.3.2",
+ "react-router": "6.8.1"
+ }
+ },
+ "react-slick": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.29.0.tgz",
+ "integrity": "sha512-TGdOKE+ZkJHHeC4aaoH85m8RnFyWqdqRfAGkhd6dirmATXMZWAxOpTLmw2Ll/jPTQ3eEG7ercFr/sbzdeYCJXA==",
+ "requires": {
+ "classnames": "^2.2.5",
+ "enquire.js": "^2.1.6",
+ "json2mq": "^0.2.0",
+ "lodash.debounce": "^4.0.8",
+ "resize-observer-polyfill": "^1.5.0"
+ }
+ },
+ "react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "requires": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ }
+ },
+ "react-use-websocket": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.3.1.tgz",
+ "integrity": "sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==",
+ "requires": {}
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "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=="
+ },
+ "remove-accents": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
+ "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+ },
+ "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=="
+ },
+ "resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+ },
+ "resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "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=="
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "rollup": {
+ "version": "2.79.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
+ "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "devOptional": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "rollup-plugin-visualizer": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz",
+ "integrity": "sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==",
+ "requires": {
+ "open": "^8.4.0",
+ "picomatch": "^2.3.1",
+ "source-map": "^0.7.4",
+ "yargs": "^17.5.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
+ }
+ }
+ },
+ "sass": {
+ "version": "1.58.3",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz",
+ "integrity": "sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==",
+ "dev": true,
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ }
+ },
+ "scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true
+ },
+ "slick-carousel": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
+ "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
+ "requires": {}
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true
+ },
+ "sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "dev": true
+ },
+ "string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="
+ },
+ "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==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "stylis": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+ "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "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=="
+ },
+ "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==",
+ "dev": true
+ },
+ "svg.draggable.js": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
+ "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
+ "requires": {
+ "svg.js": "^2.0.1"
+ }
+ },
+ "svg.easing.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
+ "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
+ "requires": {
+ "svg.js": ">=2.3.x"
+ }
+ },
+ "svg.filter.js": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
+ "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
+ "requires": {
+ "svg.js": "^2.2.5"
+ }
+ },
+ "svg.js": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
+ "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
+ },
+ "svg.pathmorphing.js": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
+ "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
+ "requires": {
+ "svg.js": "^2.4.0"
+ }
+ },
+ "svg.resize.js": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
+ "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
+ "requires": {
+ "svg.js": "^2.6.5",
+ "svg.select.js": "^2.1.2"
+ },
+ "dependencies": {
+ "svg.select.js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
+ "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
+ "requires": {
+ "svg.js": "^2.2.5"
+ }
+ }
+ }
+ },
+ "svg.select.js": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
+ "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
+ "requires": {
+ "svg.js": "^2.6.5"
+ }
+ },
+ "tiny-invariant": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
+ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
+ },
+ "to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "tsconfck": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.0.3.tgz",
+ "integrity": "sha512-o3DsPZO1+C98KqHMdAbWs30zpxD30kj8r9OLA4ML1yghx4khNDzaaShNalfluh8ZPPhzKe3qyVCP1HiZszSAsw==",
+ "requires": {}
+ },
+ "tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
+ "typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "devOptional": true
+ },
+ "ufo": {
+ "version": "0.8.6",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-0.8.6.tgz",
+ "integrity": "sha512-fk6CmUgwKCfX79EzcDQQpSCMxrHstvbLswFChHS0Vump+kFkw7nJBfTZoC1j0bOGoY9I7R3n2DGek5ajbcYnOw=="
+ },
+ "undici": {
+ "version": "5.27.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
+ "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
+ "requires": {
+ "@fastify/busboy": "^2.0.0"
+ }
+ },
+ "unload": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+ "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+ "requires": {
+ "@babel/runtime": "^7.6.2",
+ "detect-node": "^2.0.4"
+ }
+ },
+ "update-browserslist-db": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
+ "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "dev": true,
+ "requires": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "use-callback-ref": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz",
+ "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "requires": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ }
+ },
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
+ },
+ "vanilla-picker": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.1.tgz",
+ "integrity": "sha512-2qrEP9VYylKXbyzXKsbu2dferBTvqnlsr29XjHwFE+/MEp0VNj6oEUESLDtKZ7DWzGdSv1x/+ujqFZF+KsO3cg==",
+ "requires": {
+ "@sphinxxxx/color-conversion": "^2.2.2"
+ }
+ },
+ "vite": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
+ "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.15.9",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.4.18",
+ "resolve": "^1.22.1",
+ "rollup": "^2.79.1"
+ }
+ },
+ "vite-plugin-svgr": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz",
+ "integrity": "sha512-q+mJJol6ThvqkkJvvVFEndI4EaKIjSI0I3jNFgSoC9fXAz1M7kYTVUin8fhUsFojFDKZ9VHKtX6NXNaOLpbsHA==",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^5.0.2",
+ "@svgr/core": "^6.5.1"
+ }
+ },
+ "vite-tsconfig-paths": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.0.5.tgz",
+ "integrity": "sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==",
+ "requires": {
+ "debug": "^4.1.1",
+ "globrex": "^0.1.2",
+ "tsconfck": "^2.0.1"
+ }
+ },
+ "void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
+ },
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "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==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "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==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "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=="
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
+ },
+ "yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
+ },
+ "yargs": {
+ "version": "17.7.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
+ "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
+ "requires": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
+ },
+ "zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="
+ },
+ "zustand": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz",
+ "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==",
+ "requires": {
+ "use-sync-external-store": "1.2.0"
+ }
+ },
+ "zustand-computed": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/zustand-computed/-/zustand-computed-1.3.3.tgz",
+ "integrity": "sha512-SXhjuH/scwA6T31xsqaqUzPVBoumgCK/PWi8YZXGmuU86HPrqVFxjI6TxLvYw8hGpScZkjFUKBAi1ThO0B6Tng==",
+ "requires": {}
+ }
+ }
+}
diff --git a/app/dashboard/package.json b/app/dashboard/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "marzban-dashboard",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "build": "tsc && vite build",
+ "preview": "vite preview --port 3000",
+ "gen:theme-typings": "chakra-cli tokens ./chakra.config.ts",
+ "postinstall": "npm run gen:theme-typings"
+ },
+ "dependencies": {
+ "@chakra-ui/icons": "^2.1.1",
+ "@chakra-ui/react": "^2.5.5",
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@heroicons/react": "^2.0.12",
+ "@hookform/devtools": "^4.3.1",
+ "@hookform/resolvers": "^2.9.10",
+ "apexcharts": "^3.40.0",
+ "classnames": "^2.3.2",
+ "dayjs": "^1.11.6",
+ "framer-motion": "^7.6.6",
+ "i18next": "^22.4.14",
+ "i18next-browser-languagedetector": "^7.0.1",
+ "i18next-http-backend": "^2.2.0",
+ "jsoneditor": "^9.10.1",
+ "lodash.debounce": "^4.0.8",
+ "ohmyfetch": "^0.4.21",
+ "qrcode.react": "^3.1.0",
+ "react": "^18.2.0",
+ "react-apexcharts": "^1.4.0",
+ "react-copy-to-clipboard": "^5.1.0",
+ "react-datepicker": "^4.8.0",
+ "react-dom": "^18.2.0",
+ "react-github-btn": "^1.4.0",
+ "react-hook-form": "^7.39.1",
+ "react-i18next": "^12.2.0",
+ "react-json-editor-ajrm": "^2.5.14",
+ "react-loading-skeleton": "^3.1.0",
+ "react-query": "^3.39.3",
+ "react-router-dom": "^6.4.3",
+ "react-slick": "^0.29.0",
+ "react-use-websocket": "^4.3.1",
+ "rollup-plugin-visualizer": "^5.9.0",
+ "slick-carousel": "^1.8.1",
+ "vite-tsconfig-paths": "^4.0.3",
+ "zod": "^3.19.1",
+ "zustand": "^4.4.6",
+ "zustand-computed": "^1.3.3"
+ },
+ "devDependencies": {
+ "@chakra-ui/cli": "^2.3.0",
+ "@types/jsoneditor": "^9.9.0",
+ "@types/lodash.debounce": "^4.0.7",
+ "@types/react": "^18.0.17",
+ "@types/react-copy-to-clipboard": "^5.0.4",
+ "@types/react-datepicker": "^4.8.0",
+ "@types/react-dom": "^18.0.6",
+ "@types/react-json-editor-ajrm": "^2.5.3",
+ "@types/react-slick": "^0.23.10",
+ "@vitejs/plugin-react": "^2.1.0",
+ "autoprefixer": "^10.4.12",
+ "sass": "^1.57.1",
+ "typescript": "^4.9.5",
+ "vite": "^3.1.0",
+ "vite-plugin-svgr": "^2.2.2"
+ }
+}
diff --git a/app/dashboard/public/favicon/android-chrome-192x192.png b/app/dashboard/public/favicon/android-chrome-192x192.png
Binary files /dev/null and b/app/dashboard/public/favicon/android-chrome-192x192.png differ
diff --git a/app/dashboard/public/favicon/android-chrome-512x512.png b/app/dashboard/public/favicon/android-chrome-512x512.png
Binary files /dev/null and b/app/dashboard/public/favicon/android-chrome-512x512.png differ
diff --git a/app/dashboard/public/favicon/apple-touch-icon.png b/app/dashboard/public/favicon/apple-touch-icon.png
Binary files /dev/null and b/app/dashboard/public/favicon/apple-touch-icon.png differ
diff --git a/app/dashboard/public/favicon/browserconfig.xml b/app/dashboard/public/favicon/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+ <msapplication>
+ <tile>
+ <square150x150logo src="/favicon/mstile-150x150.png"/>
+ <TileColor>#2b5797</TileColor>
+ </tile>
+ </msapplication>
+</browserconfig>
diff --git a/app/dashboard/public/favicon/favicon-16x16.png b/app/dashboard/public/favicon/favicon-16x16.png
Binary files /dev/null and b/app/dashboard/public/favicon/favicon-16x16.png differ
diff --git a/app/dashboard/public/favicon/favicon-32x32.png b/app/dashboard/public/favicon/favicon-32x32.png
Binary files /dev/null and b/app/dashboard/public/favicon/favicon-32x32.png differ
diff --git a/app/dashboard/public/favicon/favicon.ico b/app/dashboard/public/favicon/favicon.ico
Binary files /dev/null and b/app/dashboard/public/favicon/favicon.ico differ
diff --git a/app/dashboard/public/favicon/mstile-150x150.png b/app/dashboard/public/favicon/mstile-150x150.png
Binary files /dev/null and b/app/dashboard/public/favicon/mstile-150x150.png differ
diff --git a/app/dashboard/public/favicon/safari-pinned-tab.svg b/app/dashboard/public/favicon/safari-pinned-tab.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="747.000000pt" height="747.000000pt" viewBox="0 0 747.000000 747.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+</metadata>
+<g transform="translate(0.000000,747.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M58 7454 c-60 -32 -58 -8 -58 -771 l0 -698 957 -957 958 -958 -947
+-948 c-520 -521 -951 -957 -957 -969 -8 -15 -11 -229 -11 -713 0 -634 1 -694
+17 -721 29 -49 99 -72 146 -48 12 6 356 345 764 752 468 467 744 736 746 727
+3 -8 10 -35 17 -60 50 -186 223 -354 416 -405 75 -20 210 -19 288 1 145 37
+171 58 562 447 l362 359 17 -58 c60 -210 217 -366 420 -419 76 -19 210 -19
+289 1 136 35 182 69 441 326 l240 237 1285 -1284 c880 -879 1294 -1286 1313
+-1291 38 -9 72 2 110 35 l32 29 3 689 c1 378 0 699 -3 713 -4 16 -366 385
+-961 980 l-954 955 946 945 c520 520 952 957 960 972 12 23 14 135 14 708 0
+611 -2 686 -16 716 -28 60 -111 83 -164 48 -14 -9 -356 -347 -761 -752 l-736
+-734 -7 46 c-4 25 -26 83 -48 129 -127 254 -434 387 -691 298 -143 -50 -161
+-65 -543 -444 l-361 -358 -7 43 c-4 24 -25 81 -48 128 -70 145 -205 259 -363
+306 -81 24 -229 24 -310 0 -139 -41 -184 -74 -438 -325 l-238 -235 -1287 1287
+c-1284 1284 -1287 1287 -1327 1287 -22 -1 -52 -7 -67 -16z m2269 -2971 l-247
+-248 -920 920 -920 920 0 495 0 495 1167 -1167 1168 -1168 -248 -247z m3341
+-620 c-1721 -1721 -1603 -1612 -1744 -1621 -173 -11 -313 87 -360 252 -20 72
+-14 153 17 229 20 46 227 257 1833 1865 l1811 1812 3 -487 2 -488 -1562 -1562z
+m-315 1677 c102 -38 188 -148 207 -264 10 -66 -4 -155 -35 -215 -17 -33 -441
+-463 -1557 -1577 -1492 -1491 -1535 -1532 -1593 -1554 -111 -40 -239 -24 -332
+43 -91 65 -151 198 -139 309 14 136 -94 20 1570 1686 875 876 1540 1534 1565
+1550 95 56 203 63 314 22z m-1623 -345 c165 -83 232 -296 145 -463 -17 -34
+-508 -532 -1829 -1852 l-1806 -1805 0 490 0 490 1563 1561 c1676 1675 1594
+1598 1712 1613 71 9 157 -4 215 -34z m3500 -4295 l0 -495 -1170 1170 -1170
+1170 247 247 248 248 922 -922 923 -923 0 -495z"/>
+</g>
+</svg>
diff --git a/app/dashboard/public/favicon/site.webmanifest b/app/dashboard/public/favicon/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "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"
+}
diff --git a/app/dashboard/public/locales/en.json b/app/dashboard/public/locales/en.json
@@ -0,0 +1,163 @@
+{
+ "password": "Password",
+ "login": "Login",
+ "cancel": "Cancel",
+ "apply": "Apply",
+ "delete": "Delete",
+ "reset": "Reset",
+ "createUser": "Create User",
+ "username": "Username",
+ "expires": "Expires in {{time}}",
+ "expired": "Expired {{time}} ago",
+ "dateFormat": "MMMM d, yyy",
+ "inbound": "Inbound",
+ "login.loginYourAccount": "Login to your account",
+ "login.welcomeBack": "Welcome back, please enter your details",
+ "login.fieldRequired": "This field is required",
+ "header.hostSettings": "Host Settings",
+ "header.nodeSettings": "Node Settings",
+ "header.nodesUsage": "Nodes Usage",
+ "header.donation": "Donation",
+ "header.logout": "Log out",
+ "deleteUser.title": "Delete User",
+ "deleteUser.prompt": "Are you sure you want to delete <b>{{username}}</b>?",
+ "deleteUser.deleteSuccess": "{{username}} deleted successfully.",
+ "usersTable.status": "status",
+ "usersTable.dataUsage": "data usage",
+ "usersTable.noUserMatched": "It seems there is no user matched with what you are looking for",
+ "usersTable.noUser": "There is no user added to the system",
+ "usersTable.copyLink": "Copy Subscription Link",
+ "usersTable.copied": "Copied",
+ "usersTable.copyConfigs": "Copy Configs",
+ "usersTable.total": "Total",
+ "userDialog.dataLimit": "Data Limit",
+ "userDialog.periodicUsageReset": "Periodic Usage Reset",
+ "userDialog.warningNoProtocol": "Please select at least one protocol",
+ "userDialog.expiryDate": "Expiry Date",
+ "userDialog.note": "Note",
+ "userDialog.resetUsage": "Reset Usage",
+ "userDialog.usage": "Usage",
+ "userDialog.protocols": "Protocols",
+ "userDialog.services": "Services",
+ "userDialog.editUserTitle": "Edit user",
+ "userDialog.editUser": "Edit user",
+ "userDialog.userEdited": "User {{username}} edited.",
+ "userDialog.userCreated": "User {{username}} created.",
+ "userDialog.userAlreadyExists": "User already exists",
+ "userDialog.vmessDesc": "Fast and secure",
+ "userDialog.vlessDesc": "Lightweight, fast and secure",
+ "userDialog.trojanDesc": "Lightweight, secure and lightening fast",
+ "userDialog.shadowsocksDesc": "Fast and secure, but not efficient as others",
+ "userDialog.resetStrategyNo": "No",
+ "userDialog.resetStrategyDaily": "Daily",
+ "userDialog.resetStrategyWeekly": "Weekly",
+ "userDialog.resetStrategyMonthly": "Monthly",
+ "userDialog.resetStrategyAnnually": "Annually",
+ "userDialog.selectOneProtocol": "Please select at least one protocol",
+ "userDialog.optional": "optional",
+ "userDialog.method": "Method",
+ "userDialog.generatedByDefault": "Automatically generated by default",
+ "userDialog.hours": "Hours",
+ "userDialog.days": "Days",
+ "userDialog.weeks": "Weeks",
+ "userDialog.months": "Months",
+ "userDialog.relative": "Relative",
+ "userDialog.absolute": "Absolute",
+ "userDialog.custom": "Custom",
+ "userDialog.startDate": "Start date",
+ "userDialog.endDate": "End date",
+ "userDialog.revokeSubscription": "Revoke Subscription",
+ "revoke": "Revoke",
+ "userDialog.total": "Total: ",
+ "hostsDialog.title": "Using this setting, you are able to assign specific address for each inbound.",
+ "hostsDialog.desc": "Use these variables to make it dynamic",
+ "hostsDialog.username": "The username of the user",
+ "hostsDialog.dataUsage": "The current usage of the user",
+ "hostsDialog.remainingData": "Remaining data of the user",
+ "hostsDialog.dataLimit": "The usage limit of the user",
+ "hostsDialog.remainingDays": "Remaining days of the user",
+ "hostsDialog.expireDate": "Expiry date of the user",
+ "hostsDialog.jalaliExpireDate": "Expiry date of the user in solar calendar",
+ "hostsDialog.remainingTime": "Remaining time of the user",
+ "hostsDialog.statusEmoji": "User status as an emoji (✅,⌛️,🪫,❌,🔌)",
+ "hostsDialog.proxyProtocol": "Proxy protocol (e.g. VMess)",
+ "hostsDialog.proxyMethod": "Proxy transport method (e.g. ws)",
+ "hostsDialog.currentServer": "Current server ip address",
+ "hostsDialog.security": "Security Layer",
+ "hostsDialog.host": "Request Host",
+ "hostsDialog.port": "Port",
+ "hostsDialog.path": "Path",
+ "hostsDialog.sni": "SNI",
+ "hostsDialog.advancedOptions": "Advanced options",
+ "hostsDialog.addHost": "Add host",
+ "hostsDialog.savedSuccess": "Hosts saved successfully",
+ "hostsDialog.loading": "loading...",
+ "hostsDialog.apply": "Apply",
+ "hostsDialog.port.info": "By default, a host uses the default port of the inbound. You can set a custom port in case this host is a server that forwards traffic from a port that differs from your server's port. For example, the server may forward traffic from port 8443 to the default port of your inbound server.",
+ "hostsDialog.path.info": "Set a path for host users, useful behind a reverse proxy.",
+ "hostsDialog.sni.info": "By default, a host uses the default SNI of the inbound. You can set a custom SNI in case this host is a server that has a different SNI. For example, the server may receive traffic with a different SSL certificate, perform SSL termination, and forward it to your inbound server.",
+ "hostsDialog.host.info": "By default, if a request host is set in the Xray config, this host is used. However, you can set a custom request host here if needed.",
+ "hostsDialog.security.info": "If the middleware server of this host uses a different security layer than the inbound's default, you can set a custom security layer here.",
+ "hostsDialog.alpn": "ALPN",
+ "hostsDialog.fingerprint": "Fingerprint",
+ "hostsDialog.host.multiHost": "To set multiple addresses, separate them with <badge>,</badge> Each time an address is chosen randomly.",
+ "hostsDialog.host.wildcard": "Use <badge>*</badge> to generate a random string (works for wildcard domain names)",
+ "nodes.title": "Using Marzban-Node, you are able to scale up your connection quality by adding different nodes on different servers.",
+ "nodes.addNewMarzbanNode": "Add New Marzban Node",
+ "nodes.certificate": "Certificate",
+ "nodes.connection-hint": "To setup a Marzban Node, you need to set this certificate on the node to initialize a secure connection between main server and the node",
+ "nodes.download-certificate": "Download certificate",
+ "nodes.show-certificate": "Show certificate",
+ "nodes.hide-certificate": "Hide certificate",
+ "nodes.addHostForEveryInbound": "Add this node as a new host for every inbound",
+ "nodes.addNode": "Add Node",
+ "nodes.addNodeSuccess": "Node {{name}} added successfully",
+ "nodes.apply": "editNode",
+ "nodes.nodeName": "Name",
+ "nodes.nodeAddress": "Address",
+ "nodes.nodePort": "Port",
+ "nodes.nodeAPIPort": "API Port",
+ "nodes.editNode": "Update Node",
+ "nodes.reconnect": "Reconnect",
+ "nodes.reconnecting": "Reconnecting...",
+ "deleteNode.title": "Delete Node",
+ "deleteNode.prompt": "Are you sure you want to delete the <b>{{name}}</b> node?",
+ "deleteNode.deleteSuccess": "Node {{name}} removed successfully",
+ "users": "Users",
+ "active": "active",
+ "disabled": "disabled",
+ "activeUsers": "active users",
+ "dataUsage": "data usage",
+ "memoryUsage": "memory usage",
+ "itemsPerPage": "Items per page",
+ "previous": "Previous",
+ "next": "Next",
+ "createNewUser": "Create new user",
+ "search": "Search",
+ "resetAllUsage": "Reset All Usages",
+ "qrcodeDialog.sublink": "Subscribe Link",
+ "resetUserUsage.prompt": "Are you sure you want to reset <b>{{username}}</b>'s usage?",
+ "resetUserUsage.title": "Reset User Usage",
+ "resetUserUsage.success": "{{username}}'s usage has reset successfully.",
+ "resetUserUsage.error": "Usage reset failed, please try again.",
+ "revokeUserSub.prompt": "Are you sure you want to revoke <b>{{username}}</b>'s subscription?",
+ "revokeUserSub.title": "Revoke User Subscription",
+ "revokeUserSub.success": "{{username}}'s subscription has revoked successfully.",
+ "revokeUserSub.error": "Subscription revoke failed, please try again.",
+ "resetAllUsage.title": "Reset data usage for all users",
+ "resetAllUsage.prompt": "This action clears all of the users' data usage completely. Are you sure you want to reset all usage? THIS CANNOT BE UNDONE!",
+ "resetAllUsage.success": "All usage has reset successfully.",
+ "resetAllUsage.error": "Usage reset failed, please try again.",
+ "core.title": "Core Settings",
+ "core.socket.connecting": "Connecting...",
+ "core.socket.connected": "Connected",
+ "core.socket.not_connected": "Not Connected",
+ "core.socket.closed": "Closed",
+ "core.restarting": "Restarting...",
+ "core.restartCore": "Restart Core",
+ "core.save": "Save",
+ "core.logs": "Logs",
+ "core.configuration": "Configuration",
+ "core.generalErrorMessage": "Something went wrong, please check the configuration",
+ "core.successMessage": "Core settings updated successfully"
+}
diff --git a/app/dashboard/public/locales/fa.json b/app/dashboard/public/locales/fa.json
@@ -0,0 +1,160 @@
+{
+ "password": "گذرواژه",
+ "login": "ورود",
+ "cancel": "لغو",
+ "apply": "تایید",
+ "delete": "حذف",
+ "reset": "بازنشانی",
+ "createUser": "افزودن کاربر",
+ "username": "نام کاربری",
+ "dateInfo.year": " سال",
+ "dateInfo.month": " ماه",
+ "dateInfo.day": " روز",
+ "dateInfo.hour": " ساعت",
+ "dateInfo.min": " دقیقه",
+ "expires": "پایان در {{time}}",
+ "expired": "{{time}} پیش به پایان رسیده",
+ "dateFormat": "MM/dd/yyyy",
+ "inbound": "ورودی",
+ "login.loginYourAccount": "وارد حساب خود شوید",
+ "login.welcomeBack": "خوش آمدید, لطفا اطلاعات خود را وارد کنید",
+ "login.fieldRequired": "This field is required",
+ "header.hostSettings": "تنظیمات هاست",
+ "header.nodeSettings": "تنظیمات گرهها",
+ "header.nodesUsage": "نمودار گرهها",
+ "header.donation": "کمک مالی",
+ "header.logout": "خروج",
+ "deleteUser.title": "حذف کاربر",
+ "deleteUser.prompt": "از حذف کاربر <b>{{username}}</b> مطمئن هستید?",
+ "deleteUser.deleteSuccess": "{{username}} با موفقیت حذف شد.",
+ "usersTable.status": "وضعیت",
+ "usersTable.dataUsage": "مصرف داده",
+ "usersTable.noUserMatched": "بهنظر میرسه کاربری که جستجو کردید، وجود ندارد",
+ "usersTable.noUser": "کاربری افزوده نشده است",
+ "usersTable.copyLink": "کپی لینک اشتراک",
+ "usersTable.copied": "کپی شد",
+ "usersTable.copyConfigs": "تنظیمات کپی",
+ "usersTable.total": "مجموع",
+ "userDialog.dataLimit": "حد مصرف داده",
+ "userDialog.periodicUsageReset": "بازنشانی دورهای مصرف",
+ "userDialog.warningNoProtocol": "لطفا حداقل یک پروتکل انتخاب کنید",
+ "userDialog.note": "توضیحات",
+ "userDialog.expiryDate": "تاریخ پایان",
+ "userDialog.resetUsage": "بازنشانی مصرف",
+ "userDialog.usage": "مصرف",
+ "userDialog.protocols": "پروتکلها",
+ "userDialog.services": "سرویس ها",
+ "userDialog.editUserTitle": "ویرایش کاربر",
+ "userDialog.editUser": "ویرایش کاربر",
+ "userDialog.userEdited": "کاربر {{username}} ویرایش شد.",
+ "userDialog.userCreated": "کاربر {{username}} ساخته شد.",
+ "userDialog.userAlreadyExists": "کاربر وجود دارد",
+ "userDialog.vmessDesc": "سریع و امن",
+ "userDialog.vlessDesc": "سبک، سریع و امن",
+ "userDialog.trojanDesc": "سبک، امن و فوقالعاده سریع",
+ "userDialog.shadowsocksDesc": "سریع و امن، اما کارآمد کمتر از بقیه",
+ "userDialog.resetStrategyNo": "خیر",
+ "userDialog.resetStrategyDaily": "روزانه",
+ "userDialog.resetStrategyWeekly": "هفتگی",
+ "userDialog.resetStrategyMonthly": "ماهانه",
+ "userDialog.resetStrategyAnnually": "سالانه",
+ "userDialog.selectOneProtocol": "لطفا حداقل یک پروتکل انتخاب کنید",
+ "userDialog.optional": "اختیاری",
+ "userDialog.method": "روش",
+ "userDialog.generatedByDefault": "پیشفرض ساخته میشود",
+ "userDialog.hours": "ساعات",
+ "userDialog.days": "روزها",
+ "userDialog.weeks": "هفتهها",
+ "userDialog.months": "ماهها",
+ "userDialog.relative": "نسبی",
+ "userDialog.absolute": "مطلق",
+ "userDialog.custom": "انتخابی",
+ "userDialog.startDate": "تاریخ شروع",
+ "userDialog.endDate": "تاریخ پایان",
+ "userDialog.total": "مجموع: ",
+ "hostsDialog.title": "با انتخاب این تنظیمات، میتوانید برای هر ورودی یک آدرس منحصر به فرد انتخاب کنید.",
+ "hostsDialog.desc": "برای ساخت نوشته پویا، از این متغیرها استفاده کنید",
+ "hostsDialog.username": "نام کاربری کاربر",
+ "hostsDialog.dataUsage": "مصرف کنونی کاربر",
+ "hostsDialog.remainingData": "حجم باقیمانده کاربر",
+ "hostsDialog.dataLimit": "حد مصرف کاربر",
+ "hostsDialog.remainingDays": "روزهای باقی مانده کاربر",
+ "hostsDialog.expireDate": "تاریخ انقضای کاربر به میلادی",
+ "hostsDialog.jalaliExpireDate": "تاریخ انقضای کاربر به شمسی",
+ "hostsDialog.remainingTime": "زمان باقی مانده کاربر",
+ "hostsDialog.statusEmoji": "وضعیت کاربر در قالب اموجی (✅,⌛️,🪫,❌,🔌)",
+ "hostsDialog.proxyProtocol": "پروتکل پروکسی (مانند VMess)",
+ "hostsDialog.proxyMethod": "روش انتقال پروکسی (مانند ws)",
+ "hostsDialog.currentServer": "IP کنونی سرور",
+ "hostsDialog.security": "لایه امنیت",
+ "hostsDialog.host": "هاست درخواست",
+ "hostsDialog.port": "پورت",
+ "hostsDialog.sni": "SNI",
+ "hostsDialog.advancedOptions": "تنظیمات پیشرفته",
+ "hostsDialog.addHost": "افزودن هاست",
+ "hostsDialog.savedSuccess": "هاستها با موفقیت ذخیره شدند",
+ "hostsDialog.loading": "درحال بارگزاری...",
+ "hostsDialog.apply": "اعمال",
+ "hostsDialog.port.info": "به طور پیشفرض، هاست، از پورت پیشفرض ورودی استفاده میکند، در صورتی که این هاست ترافیک را از پورتی، متفاوت از پورت سرور، منتقل میکند، میتوانید آن را تغییر دهید. برای مثال، ممکن از سرور ترافیک را از پورت 8443 به پورت پیشفرض سرور ورودی شما منتقل کند.",
+ "hostsDialog.sni.info": "به طور پیشفرض، هاست، از SNI پیشفرض ورودی استفاده میکند، در صورتی که این هاست یک SNI دیگری دارد، میتوانید آن را تغییر دهید. برای مثال، ممکن است سرور ترافیک با SSL متفاوت را دریافت و منتقل کند.",
+ "hostsDialog.host.info": "بهطور پیشفرض، اگر هاست درخواستی در پیکربندی *** تنظیم شده باشد، این هاست استفاده میشود. اما میتوانید یک هاست درخواستی متفاوت در اینجا قرار دهید.",
+ "hostsDialog.security.info": "اگر میانافزار سرور این هاست، از لایه امنیتی متفاوتی از ورودی استفاده میکند، میتوانید اینجا انتخاب کنید",
+ "hostsDialog.alpn": "ALPN",
+ "hostsDialog.fingerprint": "اثرانگشت",
+ "hostsDialog.host.multiHost": "برای تنظیم چند آدرس، با <badge>,</badge> از هم جدا کنید. هر دفعه آدرسی به صورت تصادفی قرار داده میشود.",
+ "hostsDialog.host.wildcard": "از <badge>*</badge> برای ساخت عبارت تصادفی استفاده کنید (برای نامهای wildcard کار میکند)",
+ "nodes.title": "با استفاده از Marzban-Node, شما میتوانید با افزودن گرههای متفاوت در سرورهای متفاوت، کیفیت اتصال خود را بالاتر ببرید.",
+ "nodes.addNewMarzbanNode": "افزودن گره مرزبان",
+ "nodes.certificate": "گواهی",
+ "nodes.addHostForEveryInbound": "استفاده از این گره به عنوان میزبان تمام ورودیها",
+ "nodes.addNode": "افزودن گره",
+ "nodes.addNodeSuccess": "گره {{name}} با موفقیت افزوده شد",
+ "nodes.apply": "ویرایش گره",
+ "nodes.nodeName": "نام",
+ "nodes.nodeAddress": "آدرس",
+ "nodes.nodePort": "پورت",
+ "nodes.nodeAPIPort": "پورت API",
+ "nodes.editNode": "ویرایش گره",
+ "nodes.reconnect": "اتصال مجدد",
+ "nodes.connection-hint": "برای راه اندازی یک گره جدید، لازم است گواهی زیر را روی گره تنظیم کنید تا یک اتصال امن بین سرور اصلی و گره ایجاد شود.",
+ "nodes.download-certificate": "دانلود گواهی",
+ "nodes.show-certificate": "نمایش دادن گواهی",
+ "nodes.hide-certificate": "مخفی کردن گواهی",
+ "nodes.reconnecting": "درحال اتصال مجدد...",
+ "deleteNode.title": "حذف گره",
+ "deleteNode.prompt": "از حذف گره <b>{{name}}</b> مطمئن هستید؟",
+ "deleteNode.deleteSuccess": "گره {{name}} با موفقیت حذف شد",
+ "active": "فعال",
+ "disabled": "غیر فعال",
+ "users": "کاربران",
+ "activeUsers": "کاربران فعال",
+ "dataUsage": "مصرف داده",
+ "memoryUsage": "مصرف حافظه",
+ "itemsPerPage": "تعداد در صفحه",
+ "previous": "قبلی",
+ "next": "بعدی",
+ "createNewUser": "ساخت کاربر",
+ "search": "جستجو",
+ "resetAllUsage": "بازنشانی کل مصرف",
+ "qrcodeDialog.sublink": "لینک اشتراک",
+ "resetUserUsage.prompt": "از بازنشانی مصرف <b>{{username}}</b> مطئمن هستید؟",
+ "resetUserUsage.title": "بازنشانی مصرف کاربر",
+ "resetUserUsage.success": "مصرف {{username}} با موفقیت بازنشانی شد.",
+ "resetUserUsage.error": "بازنشانی مصرف انجام نشد، دوباره تلاش کنید.",
+ "resetAllUsage.title": "بازنشانی مصرف تمام کاربران",
+ "resetAllUsage.prompt": "این عمل مصرف تمام کاربران را بازنشانی میکند. از این بازنشانی مطمئن هستید؟ این عمل قابل بازگشت نیست!",
+ "resetAllUsage.success": "مصرف کل با موفقیت بازنشانی شد.",
+ "resetAllUsage.error": "بازنشانی مصرف انجام نشد، دوباره تلاش کنید.",
+ "core.title": "تنظیمات هسته",
+ "core.socket.connecting": "در حال اتصال...",
+ "core.socket.connected": "متصل",
+ "core.socket.not_connected": "عدم اتصال",
+ "core.socket.closed": "بسته شده",
+ "core.restarting": "درحال اجرا مجدد...",
+ "core.restartCore": "اجرا مجدد هسته",
+ "core.save": "ذخیره",
+ "core.logs": "گزارش",
+ "core.configuration": "پیکربندی",
+ "core.generalErrorMessage": "مشکلی پیش آمده، لطفا پیکربندی را بررسی کنید",
+ "core.successMessage": "تنظیمات هسته با موفقیت ثبت شد"
+}
diff --git a/app/dashboard/public/locales/ru.json b/app/dashboard/public/locales/ru.json
@@ -0,0 +1,159 @@
+{
+ "password": "Пароль",
+ "login": "Вход",
+ "cancel": "Отмена",
+ "apply": "Применить",
+ "delete": "Удалить",
+ "reset": "Сбросить",
+ "createUser": "Создать",
+ "username": "Имя пользователя",
+ "expires": "Истекает через {{time}}",
+ "expired": "Истекло {{time}} назад",
+ "dateFormat": "MMMM d, yyy",
+ "inbound": "inbound",
+ "login.loginYourAccount": "Войдите в свой аккаунт",
+ "login.welcomeBack": "Пожалуйста, введите свои данные",
+ "login.fieldRequired": "Это поле обязательно для заполнения",
+ "header.hostSettings": "Настройки хоста",
+ "header.nodeSettings": "Настройки узлов",
+ "header.nodesUsage": "Использование узлов",
+ "header.donation": "Пожертвование",
+ "header.logout": "Выйти",
+ "deleteUser.title": "Удалить пользователя",
+ "deleteUser.prompt": "Вы уверены, что хотите удалить пользователя <b>{{username}}</b>?",
+ "deleteUser.deleteSuccess": "{{username}} успешно удален.",
+ "usersTable.status": "Статус",
+ "usersTable.dataUsage": "Расход трафика",
+ "usersTable.noUserMatched": "Похоже, нет пользователя, соответствующего вашему запросу",
+ "usersTable.noUser": "В системе нет созданных пользователей",
+ "usersTable.copyLink": "Скопировать ссылку на подписку",
+ "usersTable.copied": "Скопировано",
+ "usersTable.copyConfigs": "Скопировать конфигурации",
+ "usersTable.total": "Всего",
+ "userDialog.dataLimit": "Лимит трафика",
+ "userDialog.periodicUsageReset": "Период сброса трафика",
+ "userDialog.warningNoProtocol": "Пожалуйста, выберите хотя бы один протокол",
+ "userDialog.expiryDate": "Дата истечения срока",
+ "userDialog.note": "Примечание",
+ "userDialog.resetUsage": "Сбросить трафик",
+ "userDialog.usage": "Потребление",
+ "userDialog.protocols": "Протоколы",
+ "userDialog.services": "Услуги",
+ "userDialog.editUserTitle": "Редактировать пользователя",
+ "userDialog.editUser": "Редактировать",
+ "userDialog.userEdited": "Пользователь {{username}} изменен.",
+ "userDialog.userCreated": "Пользователь {{username}} создан.",
+ "userDialog.userAlreadyExists": "Пользователь уже существует",
+ "userDialog.vmessDesc": "Быстрый и безопасный",
+ "userDialog.vlessDesc": "Легковесный, быстрый и безопасный",
+ "userDialog.trojanDesc": "Легковесный, безопасный и быстрый",
+ "userDialog.shadowsocksDesc": "Быстрый и безопасный, но не такой эффективный, как другие",
+ "userDialog.resetStrategyNo": "Нет",
+ "userDialog.resetStrategyDaily": "Ежедневно",
+ "userDialog.resetStrategyWeekly": "Еженедельно",
+ "userDialog.resetStrategyMonthly": "Ежемесячно",
+ "userDialog.resetStrategyAnnually": "Ежегодно",
+ "userDialog.selectOneProtocol": "Пожалуйста, выберите хотя бы один протокол",
+ "userDialog.optional": "необязательно",
+ "userDialog.method": "Метод",
+ "userDialog.generatedByDefault": "по умолчанию",
+ "userDialog.hours": "Часы",
+ "userDialog.days": "Дни",
+ "userDialog.weeks": "Недели",
+ "userDialog.months": "Месяцы",
+ "userDialog.relative": "Относительно",
+ "userDialog.absolute": "Абсолютно",
+ "userDialog.custom": "Пользовательский",
+ "userDialog.startDate": "Дата начала",
+ "userDialog.endDate": "Дата окончания",
+ "userDialog.revokeSubscription": "Отозвать подписку",
+ "revoke": "Отозвать",
+ "userDialog.total": "Всего: ",
+ "hostsDialog.title": "Используя эту настройку, Вы можете настроить свои inbound.",
+ "hostsDialog.desc": "Используйте эти переменные, чтобы сделать его динамическим",
+ "hostsDialog.username": "Имя пользователя",
+ "hostsDialog.dataUsage": "Использованный трафик пользователя",
+ "hostsDialog.remainingData": "Оставшийся трафик пользователя",
+ "hostsDialog.dataLimit": "Лимит трафика пользователя",
+ "hostsDialog.remainingDays": "Оставшиеся дни пользователя",
+ "hostsDialog.expireDate": "Дата истечения срока пользователя",
+ "hostsDialog.jalaliExpireDate": "Дата истечения срока по солнечному календарю",
+ "hostsDialog.remainingTime": "Оставшееся время пользователя",
+ "hostsDialog.statusEmoji": "Статус пользователя в виде смайлика (✅,⌛️,🪫,❌,🔌)",
+ "hostsDialog.proxyProtocol": "Протокол прокси (например, VMess)",
+ "hostsDialog.proxyMethod": "Метод транспорта прокси (например, ws)",
+ "hostsDialog.currentServer": "Текущий IP-адрес сервера",
+ "hostsDialog.security": "Security Layer",
+ "hostsDialog.host": "Host",
+ "hostsDialog.port": "Port",
+ "hostsDialog.sni": "SNI",
+ "hostsDialog.advancedOptions": "Дополнительные опции",
+ "hostsDialog.addHost": "Добавить хост",
+ "hostsDialog.savedSuccess": "Хосты успешно сохранены",
+ "hostsDialog.loading": "загрузка...",
+ "hostsDialog.apply": "Применить",
+ "hostsDialog.port.info": "По умолчанию хост использует порт, значение которого обьявленно в inbound. Вы можете установить свое значение порта, если с него трафик перенаправляется на другой порт. Например, сервер может перенаправлять трафик с порта 443 на порт по умолчанию Вашего inbound",
+ "hostsDialog.sni.info": "По умолчанию хост использует SNI, значение которого обьявленно в inbound. Вы можете установить свое значение SNI, если этот хост содержит другие SNI. Например, сервер может принимать трафик с другим SSL-сертификатом, выполнять SSL-терминацию и перенаправлять его на ваш inbound.",
+ "hostsDialog.host.info": "По умолчанию, если в конфигурации XRAY задан запрашиваемый хост, то он и будет использоваться. Однако, если необходимо, вы можете установить здесь пользовательский запрашиваемый хост.",
+ "hostsDialog.security.info": "Если промежуточный сервер этого хоста использует другой security layer, отличный от security layer Вашего inbound по умолчанию, Вы можете установить его здесь",
+ "hostsDialog.alpn": "ALPN",
+ "hostsDialog.fingerprint": "Fingerprint",
+ "hostsDialog.host.multiHost": "Чтобы установить несколько адресов, разделяйте их с помощью <badge>,</badge>. Каждый раз будет выбран случайный адрес.",
+ "hostsDialog.host.wildcard": "Используйте <badge>*</badge>, чтобы сгенерировать случайную строку (работает для wildcard доменов)",
+ "nodes.title": "Используя Marzban-Node, Вы можете масштабировать инфраструктуру, добавляя узлы на разных серверах.",
+ "nodes.addNewMarzbanNode": "Добавить новый узел Marzban",
+ "nodes.certificate": "Сертификат",
+ "nodes.addHostForEveryInbound": "Добавить этот узел как новый хост для каждого inbound",
+ "nodes.addNode": "Добавить узел",
+ "nodes.addNodeSuccess": "Узел {{name}} успешно добавлен",
+ "nodes.apply": "Настройки узла успешно обновлены",
+ "nodes.nodeName": "Имя",
+ "nodes.nodeAddress": "Адрес",
+ "nodes.nodePort": "Порт",
+ "nodes.nodeAPIPort": "API порт",
+ "nodes.editNode": "Редактировать узел",
+ "nodes.reconnect": "Переподключиться",
+ "nodes.connection-hint": "Для настройки узла Marzban, необходимо установить на нем данный сертификат, для инициализации безопасного соединения между главным сервером и узлом",
+ "nodes.download-certificate": "Скачать сертификат",
+ "nodes.show-certificate": "Показать сертификат",
+ "nodes.hide-certificate": "Скрыть сертификат",
+ "nodes.reconnecting": "Переподключение...",
+ "deleteNode.title": "Удалить узел",
+ "deleteNode.prompt": "Вы уверены, что хотите удалить узел <b>{{name}}</b>?",
+ "deleteNode.deleteSuccess": "Узел {{name}} успешно удален",
+ "users": "Пользователи",
+ "activeUsers": "Пользователи",
+ "dataUsage": "Трафик",
+ "memoryUsage": "Память",
+ "itemsPerPage": "Элементов на страницу",
+ "previous": "Назад",
+ "next": "Вперед",
+ "createNewUser": "Создать нового пользователя",
+ "search": "Поиск",
+ "resetAllUsage": "Сбросить расход трафика",
+ "qrcodeDialog.sublink": "Ссылка на подписку",
+ "resetUserUsage.prompt": "Вы уверены, что хотите сбросить расход трафика для пользователя <b>{{username}}</b>?",
+ "resetUserUsage.title": "Сбросить расход трафика пользователя",
+ "resetUserUsage.success": "Расход трафика пользователя {{username}} успешно сброшен.",
+ "resetUserUsage.error": "Сброс расхода не удался, пожалуйста, попробуйте еще раз.",
+ "revokeUserSub.prompt": "Вы уверены, что хотите отозвать подписку для пользователя <b>{{username}}</b>?",
+ "revokeUserSub.title": "Отозвать подписку пользователя",
+ "revokeUserSub.success": "Подписка пользователя {{username}} успешно отозвана.",
+ "revokeUserSub.error": "Отзыв подписки не удался, пожалуйста, попробуйте еще раз.",
+ "resetAllUsage.title": "Сбросить расход трафика для всех пользователей",
+ "resetAllUsage.prompt": "Это действие полностью очищает весь расход трафика пользователей. Вы уверены? ЭТО ДЕЙСТВИЕ НЕОБРАТИМО!",
+ "resetAllUsage.success": "Расход трафика успешно сброшен.",
+ "resetAllUsage.error": "Сброс расхода трафика не удался, пожалуйста, попробуйте еще раз.",
+ "core.title": "Основные настройки",
+ "core.socket.connecting": "Соединение...",
+ "core.socket.connected": "Подключено",
+ "core.socket.not_connected": "Не подключено",
+ "core.socket.closed": "Закрыто",
+ "core.restarting": "Перезагрузка...",
+ "core.restartCore": "Перезагрузить ядро",
+ "core.save": "Сохранить",
+ "core.logs": "Логи",
+ "core.configuration": "Конфигурация",
+ "core.generalErrorMessage": "Что-то пошло не так, пожалуйста, проверьте конфигурацию",
+ "core.successMessage": "Основные настройки успешно обновлены"
+}
diff --git a/app/dashboard/public/locales/zh.json b/app/dashboard/public/locales/zh.json
@@ -0,0 +1,136 @@
+{
+ "password": "密码",
+ "login": "登录",
+ "cancel": "取消",
+ "apply": "应用",
+ "delete": "删除",
+ "reset": "重置",
+ "createUser": "创建用户",
+ "username": "用户名",
+ "expires": "{{time}}有效",
+ "expired": "{{time}}失效",
+ "dateFormat": "MM/dd/yyyy",
+ "inbound": "入站",
+ "login.loginYourAccount": "登录您的帐号",
+ "login.welcomeBack": "欢迎回来,请输入您的详细信息",
+ "login.fieldRequired": "此项必填",
+ "header.hostSettings": "设置",
+ "header.nodeSettings": "节点设置",
+ "header.nodesUsage": "节点统计",
+ "header.donation": "捐赠",
+ "header.logout": "退出",
+ "deleteUser.title": "删除用户",
+ "deleteUser.prompt": "您确定你要删除 <b>{{username}}</b>?",
+ "deleteUser.deleteSuccess": "{{username}} 删除成功。",
+ "usersTable.status": "状态",
+ "usersTable.dataUsage": "流量统计",
+ "usersTable.noUserMatched": "没有找到您搜索的用户",
+ "usersTable.noUser": "还没有添加任何用户",
+ "usersTable.copyLink": "复制订阅链接",
+ "usersTable.copied": "已复制",
+ "usersTable.copyConfigs": "复制配置",
+ "usersTable.total": "总共",
+ "userDialog.dataLimit": "流量限制",
+ "userDialog.periodicUsageReset": "定期重置流量",
+ "userDialog.expiryDate": "过期日期",
+ "userDialog.resetUsage": "重置流量",
+ "userDialog.usage": "流量详情",
+ "userDialog.protocols": "协议",
+ "userDialog.services": "服务",
+ "userDialog.editUserTitle": "用户编辑",
+ "userDialog.editUser": "修改",
+ "userDialog.userEdited": "已更新用户 {{username}}。",
+ "userDialog.userCreated": "成功创建用户 {{username}}。",
+ "userDialog.userAlreadyExists": "用户已存在",
+ "userDialog.vmessDesc": "快速且安全",
+ "userDialog.vlessDesc": "轻量、快速且安全",
+ "userDialog.trojanDesc": "轻量、安全且非常快",
+ "userDialog.shadowsocksDesc": "快速且安全, 但效率不如其它",
+ "userDialog.resetStrategyNo": "无",
+ "userDialog.resetStrategyDaily": "每天",
+ "userDialog.resetStrategyWeekly": "每周",
+ "userDialog.resetStrategyMonthly": "每月",
+ "userDialog.resetStrategyAnnually": "每年",
+ "userDialog.selectOneProtocol": "请至少选择一个协议",
+ "userDialog.optional": "可以为空",
+ "userDialog.method": "加密方式",
+ "userDialog.generatedByDefault": "默认自动生成",
+ "userDialog.hours": "小时",
+ "userDialog.days": "天",
+ "userDialog.weeks": "周",
+ "userDialog.months": "月",
+ "userDialog.relative": "相对时间",
+ "userDialog.absolute": "选择范围",
+ "userDialog.custom": "自定义",
+ "userDialog.startDate": "开始日期",
+ "userDialog.endDate": "结束日期",
+ "userDialog.total": "总共:",
+ "hostsDialog.title": "使用此设置,您可以为每个入站分配特定的地址。",
+ "hostsDialog.desc": "使用这些变量使其可以动态替换",
+ "hostsDialog.username": "用户的用户名",
+ "hostsDialog.dataUsage": "用户当前流量情况",
+ "hostsDialog.remainingData": "用户剩余流量情况",
+ "hostsDialog.dataLimit": "用户的流量限制",
+ "hostsDialog.remainingDays": "用户的剩余天数",
+ "hostsDialog.expireDate": "用户的有效期",
+ "hostsDialog.jalaliExpireDate": "用户阳历有效日期",
+ "hostsDialog.remainingTime": "用户剩余时间",
+ "hostsDialog.statusEmoji": "用户状态作为表情符号 (✅,⌛️,🪫,❌,🔌)",
+ "hostsDialog.proxyProtocol": "代理协议(例如 VMess)",
+ "hostsDialog.proxyMethod": "代理传输方法(例如 ws)",
+ "hostsDialog.currentServer": "当前服务器的 IP 地址",
+ "hostsDialog.security": "安全层",
+ "hostsDialog.host": "请求主机",
+ "hostsDialog.port": "端口",
+ "hostsDialog.sni": "SNI",
+ "hostsDialog.advancedOptions": "高级选项",
+ "hostsDialog.addHost": "添加主机",
+ "hostsDialog.savedSuccess": "设置保存成功",
+ "hostsDialog.loading": "加载中...",
+ "hostsDialog.apply": "保存",
+ "hostsDialog.port.info": "默认情况下,主机使用入站端口的默认端口。如果此主机是一个服务器,用于从与您的服务器端口不同的端口转发流量,则可以设置自定义端口。例如,服务器可能会将来自端口 8443 的流量转发到您入站服务器的默认端口。",
+ "hostsDialog.sni.info": "默认情况下,主机使用入站 SNI 的默认值。如果此主机是一个服务器且 SNI 不同,则可以设置自定义 SNI。例如,服务器可能会接收带有不同 SSL 证书的流量,执行 SSL 处理并将其转发到您的入站服务器。",
+ "hostsDialog.host.info": "默认情况下,如果在 Xray 配置中设置了请求主机,则使用该主机。但是,如果需要,您可以在此处设置自定义请求主机。",
+ "hostsDialog.security.info": "如果此主机的中间件服务器使用的安全层与入站默认值不同,则可以在此处设置自定义安全层。",
+ "hostsDialog.alpn": "ALPN",
+ "hostsDialog.fingerprint": "指纹",
+ "nodes.title": "使用 Marzban-Node,您可以通过在不同的服务器上添加不同的节点来提供多节点负载。",
+ "nodes.addNewMarzbanNode": "添加新的 Marzban 节点",
+ "nodes.certificate": "证书",
+ "nodes.addHostForEveryInbound": "为每个入站请求添加此节点作为新主机",
+ "nodes.addNode": "添加节点",
+ "nodes.addNodeSuccess": "节点 {{name}} 添加成功",
+ "nodes.editNode": "保存",
+ "nodes.nodeName": "名称",
+ "nodes.nodeAddress": "地址",
+ "nodes.nodePort": "端口",
+ "nodes.nodeAPIPort": "API 端口",
+ "nodes.reconnect": "重新连接",
+ "nodes.connection-hint": "要设置Marzban节点,您需要在节点上设置此证书以初始化主服务器和节点之间的安全连接",
+ "nodes.download-certificate": "下载证书",
+ "nodes.show-certificate": "如何证书",
+ "nodes.hide-certificate": "隐藏证书",
+ "nodes.reconnecting": "重连中...",
+ "deleteNode.title": "删除节点",
+ "deleteNode.prompt": "您确实要删除 <b>{{name}}</b> 节点吗?",
+ "deleteNode.deleteSuccess": "节点 {{name}} 删除成功",
+ "users": "用户",
+ "activeUsers": "活跃用户",
+ "dataUsage": "总流量",
+ "memoryUsage": "内存状态",
+ "itemsPerPage": "每页条数",
+ "previous": "上一页",
+ "next": "下一页",
+ "createNewUser": "创建新用户",
+ "search": "搜索",
+ "resetAllUsage": "重置所有统计",
+ "qrcodeDialog.sublink": "订阅链接",
+ "resetUserUsage.prompt": "您确定要重置 <b>{{username}}</b> 的流量统计吗?",
+ "resetUserUsage.title": "重置用户流量统计",
+ "resetUserUsage.success": "{{username}} 的流量统计重置完成。",
+ "resetUserUsage.error": "重置失败,请稍候再试",
+ "resetAllUsage.title": "重置所有用户的数据使用情况",
+ "resetAllUsage.prompt": "此操作将清除所有用户统计,您确定要执行此操作吗? 这不能被撤消!",
+ "resetAllUsage.success": "所有统计重置完成。",
+ "resetAllUsage.error": "重置失败,请稍候再试!"
+}
diff --git a/app/dashboard/src/App.tsx b/app/dashboard/src/App.tsx
@@ -0,0 +1,14 @@
+import "react-datepicker/dist/react-datepicker.css";
+import "react-loading-skeleton/dist/skeleton.css";
+import { RouterProvider } from "react-router-dom";
+import { router } from "./components/modules/Router";
+
+function App() {
+ return (
+ <main className="p-8">
+ <RouterProvider router={router} />
+ </main>
+ );
+}
+
+export default App;
diff --git a/app/dashboard/src/assets/add_file.svg b/app/dashboard/src/assets/add_file.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="782.04441" height="701.88002" viewBox="0 0 782.04441 701.88002" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M609.48783,100.59015l-25.44631,6.56209L270.53735,187.9987,245.091,194.56079A48.17927,48.17927,0,0,0,210.508,253.17865L320.849,681.05606a48.17924,48.17924,0,0,0,58.61776,34.58317l.06572-.01695,364.26536-93.93675.06572-.01695a48.17923,48.17923,0,0,0,34.58309-58.6178l-110.341-427.87741A48.17928,48.17928,0,0,0,609.48783,100.59015Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M612.94784,114.00532l-30.13945,7.77236L278.68955,200.20385l-30.139,7.77223a34.30949,34.30949,0,0,0-24.6275,41.74308l110.341,427.87741a34.30946,34.30946,0,0,0,41.7431,24.62736l.06572-.01695,364.26536-93.93674.06619-.01707a34.30935,34.30935,0,0,0,24.627-41.7429l-110.341-427.87741A34.30938,34.30938,0,0,0,612.94784,114.00532Z" transform="translate(-208.9778 -99.05999)" fill="#fff"/><path d="M590.19,252.56327,405.917,300.08359a8.01411,8.01411,0,0,1-4.00241-15.52046l184.273-47.52033A8.01412,8.01412,0,0,1,590.19,252.56327Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M628.955,270.49906,412.671,326.27437a8.01411,8.01411,0,1,1-4.00241-15.52046l216.284-55.77531a8.01411,8.01411,0,0,1,4.00242,15.52046Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M620.45825,369.93676l-184.273,47.52032a8.01411,8.01411,0,1,1-4.00242-15.52046l184.273-47.52032a8.01411,8.01411,0,1,1,4.00241,15.52046Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M659.22329,387.87255l-216.284,55.77531a8.01411,8.01411,0,1,1-4.00242-15.52046l216.284-55.77531a8.01411,8.01411,0,0,1,4.00242,15.52046Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M650.72653,487.31025l-184.273,47.52033a8.01412,8.01412,0,0,1-4.00242-15.52047l184.273-47.52032a8.01411,8.01411,0,0,1,4.00242,15.52046Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M689.49156,505.246l-216.284,55.77532a8.01412,8.01412,0,1,1-4.00241-15.52047l216.284-55.77531a8.01411,8.01411,0,0,1,4.00242,15.52046Z" transform="translate(-208.9778 -99.05999)" fill="#f2f2f2"/><path d="M374.45884,348.80871l-65.21246,16.817a3.847,3.847,0,0,1-4.68062-2.76146L289.5963,304.81607a3.847,3.847,0,0,1,2.76145-4.68061l65.21247-16.817a3.847,3.847,0,0,1,4.68061,2.76145l14.96947,58.04817A3.847,3.847,0,0,1,374.45884,348.80871Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M404.72712,466.1822l-65.21247,16.817a3.847,3.847,0,0,1-4.68062-2.76146l-14.96946-58.04816A3.847,3.847,0,0,1,322.626,417.509l65.21246-16.817a3.847,3.847,0,0,1,4.68062,2.76145l14.96946,58.04817A3.847,3.847,0,0,1,404.72712,466.1822Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M434.99539,583.55569l-65.21246,16.817a3.847,3.847,0,0,1-4.68062-2.76145l-14.96946-58.04817a3.847,3.847,0,0,1,2.76145-4.68062l65.21247-16.817a3.847,3.847,0,0,1,4.68061,2.76146l14.96947,58.04816A3.847,3.847,0,0,1,434.99539,583.55569Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M863.63647,209.0517H487.31811a48.17928,48.17928,0,0,0-48.125,48.12512V699.05261a48.17924,48.17924,0,0,0,48.125,48.12507H863.63647a48.17924,48.17924,0,0,0,48.125-48.12507V257.17682A48.17928,48.17928,0,0,0,863.63647,209.0517Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M863.637,222.90589H487.31811a34.30948,34.30948,0,0,0-34.271,34.27093V699.05261a34.30947,34.30947,0,0,0,34.271,34.27088H863.637a34.30936,34.30936,0,0,0,34.27051-34.27088V257.17682A34.30937,34.30937,0,0,0,863.637,222.90589Z" transform="translate(-208.9778 -99.05999)" fill="#fff"/><circle cx="694.19401" cy="614.02963" r="87.85039" fill="#3182CE"/><path d="M945.18722,701.63087H914.63056V671.07421a11.45875,11.45875,0,0,0-22.9175,0v30.55666H861.1564a11.45875,11.45875,0,0,0,0,22.9175h30.55666V755.105a11.45875,11.45875,0,1,0,22.9175,0V724.54837h30.55666a11.45875,11.45875,0,0,0,0-22.9175Z" transform="translate(-208.9778 -99.05999)" fill="#fff"/><path d="M807.00068,465.71551H616.699a8.01412,8.01412,0,1,1,0-16.02823H807.00068a8.01412,8.01412,0,0,1,0,16.02823Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M840.05889,492.76314H616.699a8.01412,8.01412,0,1,1,0-16.02823H840.05889a8.01411,8.01411,0,1,1,0,16.02823Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M807.00068,586.929H616.699a8.01412,8.01412,0,1,1,0-16.02823H807.00068a8.01411,8.01411,0,0,1,0,16.02823Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M840.05889,613.97661H616.699a8.01412,8.01412,0,1,1,0-16.02823H840.05889a8.01412,8.01412,0,1,1,0,16.02823Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M574.07028,505.04162H506.72434a3.847,3.847,0,0,1-3.84278-3.84278V441.25158a3.847,3.847,0,0,1,3.84278-3.84278h67.34594a3.847,3.847,0,0,1,3.84278,3.84278v59.94726A3.847,3.847,0,0,1,574.07028,505.04162Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M574.07028,626.25509H506.72434a3.847,3.847,0,0,1-3.84278-3.84278V562.46505a3.847,3.847,0,0,1,3.84278-3.84278h67.34594a3.847,3.847,0,0,1,3.84278,3.84278v59.94726A3.847,3.847,0,0,1,574.07028,626.25509Z" transform="translate(-208.9778 -99.05999)" fill="#e6e6e6"/><path d="M807.21185,330.781H666.91017a8.01411,8.01411,0,0,1,0-16.02823H807.21185a8.01411,8.01411,0,0,1,0,16.02823Z" transform="translate(-208.9778 -99.05999)" fill="#ccc"/><path d="M840.27007,357.82862H666.91017a8.01411,8.01411,0,1,1,0-16.02822h173.3599a8.01411,8.01411,0,0,1,0,16.02822Z" transform="translate(-208.9778 -99.05999)" fill="#ccc"/><path d="M635.85911,390.6071H506.51316a3.847,3.847,0,0,1-3.84277-3.84277V285.81706a3.847,3.847,0,0,1,3.84277-3.84277H635.85911a3.847,3.847,0,0,1,3.84277,3.84277V386.76433A3.847,3.847,0,0,1,635.85911,390.6071Z" transform="translate(-208.9778 -99.05999)" fill="#ccc"/></svg>
diff --git a/app/dashboard/src/assets/logo.svg b/app/dashboard/src/assets/logo.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 747 747" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M746.671 209.652V77.6572C746.671 72.9384 743.827 68.6832 739.468 66.8811C735.108 65.0738 730.093 66.0739 726.754 69.4072L579.354 216.807C577.453 202.386 570.203 189.203 559.037 179.87C547.875 170.542 533.615 165.744 519.083 166.432C504.551 167.12 490.807 173.24 480.573 183.583L414.375 249.776C412.417 235.391 405.141 222.25 393.985 212.959C382.823 203.662 368.594 198.88 354.083 199.552C339.578 200.224 325.849 206.302 315.594 216.588L274.343 257.828L19.9167 3.41515C16.5781 0.0818157 11.5625 -0.918184 7.20306 0.889149C2.84373 2.69648 0 6.94648 0 11.6651V143.649C0 146.743 1.22917 149.712 3.41667 151.899L191.843 340.339L3.41667 528.753C1.2292 530.94 0 533.909 0 537.003V668.997C0 673.716 2.84373 677.971 7.20306 679.773C11.5624 681.581 16.578 680.58 19.9167 677.247L167.317 529.847C169.285 544.233 176.561 557.358 187.718 566.65C198.869 575.947 213.098 580.728 227.603 580.066C242.103 579.404 255.838 573.342 266.098 563.071L332.295 496.874V496.879C334.259 511.264 341.535 524.395 352.691 533.686C363.853 542.977 378.082 547.759 392.587 547.092C407.093 546.421 420.822 540.348 431.077 530.066L472.327 488.826L726.754 743.239C728.941 745.427 731.91 746.656 735.004 746.656C736.535 746.661 738.056 746.359 739.473 745.77C743.832 743.963 746.671 739.708 746.671 734.989V602.994C746.671 599.901 745.442 596.932 743.254 594.744L554.827 406.318L743.254 217.904H743.259C745.447 215.717 746.676 212.748 746.676 209.654L746.671 209.652ZM23.3373 39.8118L257.844 274.318L208.328 323.818L23.3413 138.818L23.3373 39.8118ZM723.337 706.825L488.831 472.318L538.347 422.818L723.333 607.818L723.337 706.825ZM23.3373 541.825L332.097 233.078C338.764 226.719 347.624 223.172 356.843 223.172C366.056 223.172 374.916 226.719 381.588 233.078C388.151 239.641 391.838 248.548 391.838 257.828C391.838 267.114 388.151 276.016 381.588 282.577L23.3347 640.831L23.3373 541.825ZM249.604 546.575C240.755 555.419 227.864 558.872 215.781 555.638C203.703 552.398 194.265 542.961 191.027 530.883C187.792 518.8 191.245 505.91 200.089 497.06L497.076 200.074C505.92 191.23 518.811 187.772 530.893 191.011C542.971 194.246 552.409 203.678 555.648 215.76C558.888 227.839 555.434 240.73 546.591 249.572L249.604 546.575ZM723.337 204.815L414.577 513.562C407.911 519.926 399.051 523.473 389.832 523.473C380.618 523.473 371.753 519.926 365.087 513.562C358.529 506.999 354.842 498.098 354.842 488.816C354.842 479.535 358.529 470.634 365.087 464.071L723.34 105.818L723.337 204.815Z" fill="currentColor"/>
+</svg>
diff --git a/app/dashboard/src/components/CoreSettingsModal.tsx b/app/dashboard/src/components/CoreSettingsModal.tsx
@@ -0,0 +1,340 @@
+import {
+ Badge,
+ Box,
+ Button,
+ chakra,
+ CircularProgress,
+ FormControl,
+ FormLabel,
+ HStack,
+ IconButton,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ Tooltip,
+ useToast,
+} from "@chakra-ui/react";
+import {
+ ArrowPathIcon,
+ ArrowsPointingInIcon,
+ ArrowsPointingOutIcon,
+ Cog6ToothIcon,
+} from "@heroicons/react/24/outline";
+import { joinPaths } from "@remix-run/router";
+import classNames from "classnames";
+import { useCoreSettings } from "contexts/CoreSettingsContext";
+import { useDashboard } from "contexts/DashboardContext";
+import debounce from "lodash.debounce";
+import { FC, useCallback, useEffect, useRef, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import { useMutation } from "react-query";
+import { ReadyState } from "react-use-websocket";
+import { useWebSocket } from "react-use-websocket/dist/lib/use-websocket";
+import { getAuthToken } from "utils/authStorage";
+import { Icon } from "./Icon";
+import { JsonEditor } from "./JsonEditor";
+import "./JsonEditor/themes.js";
+
+export const MAX_NUMBER_OF_LOGS = 500;
+
+const UsageIcon = chakra(Cog6ToothIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+export const ReloadIcon = chakra(ArrowPathIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export const FullScreenIcon = chakra(ArrowsPointingOutIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+export const ExitFullScreenIcon = chakra(ArrowsPointingInIcon, {
+ baseStyle: {
+ w: 3,
+ h: 3,
+ },
+});
+
+const getStatus = (status: string) => {
+ return {
+ [ReadyState.CONNECTING]: "connecting",
+ [ReadyState.OPEN]: "connected",
+ [ReadyState.CLOSING]: "closed",
+ [ReadyState.CLOSED]: "closed",
+ [ReadyState.UNINSTANTIATED]: "closed",
+ }[status];
+};
+
+const getWebsocketUrl = () => {
+ try {
+ let baseURL = new URL(
+ import.meta.env.VITE_BASE_API.startsWith("/")
+ ? window.location.origin + import.meta.env.VITE_BASE_API
+ : import.meta.env.VITE_BASE_API
+ );
+
+ return (
+ (baseURL.protocol === "https:" ? "wss://" : "ws://") +
+ joinPaths([baseURL.host + baseURL.pathname, "/core/logs"]) +
+ "?interval=1&token=" +
+ getAuthToken()
+ );
+ } catch (e) {
+ console.error("Unable to generate websocket url");
+ console.error(e);
+ return null;
+ }
+};
+
+let logsTmp: string[] = [];
+const CoreSettingModalContent: FC = () => {
+ const { isEditingCore } = useDashboard();
+ const {
+ fetchCoreSettings,
+ updateConfig,
+ isLoading,
+ config,
+ isPostLoading,
+ version,
+ restartCore,
+ } = useCoreSettings();
+ const logsDiv = useRef<HTMLDivElement | null>(null);
+ const [logs, setLogs] = useState<string[]>([]);
+ const { t } = useTranslation();
+ const toast = useToast();
+ const form = useForm({
+ defaultValues: { config: config || {} },
+ });
+
+ useEffect(() => {
+ if (config) form.setValue("config", config);
+ }, [config]);
+
+ useEffect(() => {
+ if (isEditingCore) fetchCoreSettings();
+ }, [isEditingCore]);
+ "".startsWith;
+ const scrollShouldStayOnEnd = useRef(true);
+ const updateLogs = useCallback(
+ debounce((logs: string[]) => {
+ const isScrollOnEnd =
+ Math.abs(
+ (logsDiv.current?.scrollTop || 0) -
+ (logsDiv.current?.scrollHeight || 0) +
+ (logsDiv.current?.offsetHeight || 0)
+ ) < 10;
+ if (logsDiv.current && isScrollOnEnd)
+ scrollShouldStayOnEnd.current = true;
+ else scrollShouldStayOnEnd.current = false;
+ if (logs.length < 40) setLogs(logs);
+ }, 300),
+ []
+ );
+
+ const { readyState } = useWebSocket(getWebsocketUrl(), {
+ onMessage: (e: any) => {
+ logsTmp.push(e.data);
+ if (logsTmp.length > MAX_NUMBER_OF_LOGS)
+ logsTmp = logsTmp.splice(0, logsTmp.length - MAX_NUMBER_OF_LOGS);
+ updateLogs([...logsTmp]);
+ },
+ shouldReconnect: () => true,
+ reconnectAttempts: 10,
+ reconnectInterval: 1000,
+ });
+
+ useEffect(() => {
+ if (logsDiv.current && scrollShouldStayOnEnd.current)
+ logsDiv.current.scrollTop = logsDiv.current?.scrollHeight;
+ }, [logs]);
+
+ useEffect(() => {
+ return () => {
+ logsTmp = [];
+ };
+ }, []);
+
+ const status = getStatus(readyState.toString());
+
+ const { mutate: handleRestartCore, isLoading: isRestarting } =
+ useMutation(restartCore);
+
+ const handleOnSave = ({ config }: any) => {
+ updateConfig(config)
+ .then(() => {
+ toast({
+ title: t("core.successMessage"),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .catch((e) => {
+ let message = t("core.generalErrorMessage");
+ if (typeof e.response._data.detail === "object")
+ message =
+ e.response._data.detail[Object.keys(e.response._data.detail)[0]];
+ if (typeof e.response._data.detail === "string")
+ message = e.response._data.detail;
+
+ toast({
+ title: message,
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ });
+ };
+ const editorRef = useRef<HTMLDivElement>(null);
+ const [isFullScreen, setFullScreen] = useState(false);
+ const handleFullScreen = () => {
+ if (document.fullscreenElement) {
+ document.exitFullscreen();
+ setFullScreen(false);
+ } else {
+ editorRef.current?.requestFullscreen();
+ setFullScreen(true);
+ }
+ };
+ return (
+ <form onSubmit={form.handleSubmit(handleOnSave)}>
+ <ModalBody>
+ <FormControl>
+ <HStack justifyContent="space-between" alignItems="flex-start">
+ <FormLabel>
+ {t("core.configuration")}{" "}
+ {isLoading && <CircularProgress isIndeterminate size="15px" />}
+ </FormLabel>
+ <HStack gap={0}>
+ <Tooltip label="Xray Version" placement="top">
+ <Badge height="100%" textTransform="lowercase">
+ {version && `v${version}`}
+ </Badge>
+ </Tooltip>
+ </HStack>
+ </HStack>
+ <Box position="relative" ref={editorRef} minHeight="300px">
+ <Controller
+ control={form.control}
+ name="config"
+ render={({ field }) => (
+ <JsonEditor json={config} onChange={field.onChange} />
+ )}
+ />
+ <IconButton
+ size="xs"
+ aria-label="full screen"
+ variant="ghost"
+ position="absolute"
+ top="2"
+ right="4"
+ onClick={handleFullScreen}
+ >
+ {!isFullScreen ? <FullScreenIcon /> : <ExitFullScreenIcon />}
+ </IconButton>
+ </Box>
+ </FormControl>
+ <FormControl mt="4">
+ <HStack justifyContent="space-between">
+ <FormLabel>{t("core.logs")}</FormLabel>
+ <Text as={FormLabel}>{t(`core.socket.${status}`)}</Text>
+ </HStack>
+ <Box
+ border="1px solid"
+ borderColor="gray.300"
+ bg="#F9F9F9"
+ _dark={{
+ borderColor: "gray.500",
+ bg: "#2e3440",
+ }}
+ borderRadius={5}
+ minHeight="200px"
+ maxHeight={"250px"}
+ p={2}
+ overflowY="auto"
+ ref={logsDiv}
+ >
+ {logs.map((message, i) => (
+ <Text fontSize="xs" opacity={0.8} key={i} whiteSpace="pre-line">
+ {message}
+ </Text>
+ ))}
+ </Box>
+ </FormControl>
+ </ModalBody>
+ <ModalFooter>
+ <HStack w="full" justifyContent="space-between">
+ <Box>
+ <Button
+ size="sm"
+ leftIcon={
+ <ReloadIcon
+ className={classNames({
+ "animate-spin": isRestarting,
+ })}
+ />
+ }
+ onClick={() => handleRestartCore()}
+ >
+ {t(isRestarting ? "core.restarting" : "core.restartCore")}
+ </Button>
+ </Box>
+ <HStack>
+ <Button
+ size="sm"
+ variant="solid"
+ colorScheme="primary"
+ px="5"
+ type="submit"
+ isDisabled={isLoading || isPostLoading}
+ isLoading={isPostLoading}
+ >
+ {t("core.save")}
+ </Button>
+ </HStack>
+ </HStack>
+ </ModalFooter>
+ </form>
+ );
+};
+export const CoreSettingsModal: FC = () => {
+ const { isEditingCore } = useDashboard();
+ const onClose = useDashboard.setState.bind(null, { isEditingCore: false });
+ const { t } = useTranslation();
+
+ return (
+ <Modal isOpen={isEditingCore} onClose={onClose} size="3xl">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="full">
+ <ModalHeader pt={6}>
+ <HStack gap={2}>
+ <Icon color="primary">
+ <UsageIcon color="white" />
+ </Icon>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("core.title")}
+ </Text>
+ </HStack>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <CoreSettingModalContent />
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/DeleteNodeModal.tsx b/app/dashboard/src/components/DeleteNodeModal.tsx
@@ -0,0 +1,95 @@
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
+import { FetchNodesQueryKey, useNodes } from "contexts/NodesContext";
+import { FC } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { useMutation, useQueryClient } from "react-query";
+import {
+ generateErrorMessage,
+ generateSuccessMessage,
+} from "utils/toastHandler";
+import { DeleteIcon, DeleteUserModalProps } from "./DeleteUserModal";
+import { Icon } from "./Icon";
+
+export const DeleteNodeModal: FC<DeleteUserModalProps> = ({
+ deleteCallback,
+}) => {
+ const { deleteNode, deletingNode, setDeletingNode } = useNodes();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const queryClient = useQueryClient();
+ const onClose = () => {
+ setDeletingNode(null);
+ };
+
+ const { isLoading, mutate: onDelete } = useMutation(deleteNode, {
+ onSuccess: () => {
+ generateSuccessMessage(
+ t("deleteNode.deleteSuccess", {name: deletingNode && deletingNode.name}),
+ toast
+ );
+ setDeletingNode(null);
+ queryClient.invalidateQueries(FetchNodesQueryKey);
+ deleteCallback && deleteCallback();
+ },
+ onError: (e) => {
+ generateErrorMessage(e, toast);
+ },
+ });
+
+ return (
+ <Modal isCentered isOpen={!!deletingNode} onClose={onClose} size="sm">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3">
+ <ModalHeader pt={6}>
+ <Icon color="red">
+ <DeleteIcon />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("deleteNode.title")}
+ </Text>
+ {deletingNode && (
+ <Text
+ mt={1}
+ fontSize="sm"
+ _dark={{ color: "gray.400" }}
+ color="gray.600"
+ >
+ <Trans components={{ b: <b /> }}>
+ {t("deleteNode.prompt", {name: deletingNode.name})}
+ </Trans>
+ </Text>
+ )}
+ </ModalBody>
+ <ModalFooter display="flex">
+ <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
+ {t("cancel")}
+ </Button>
+ <Button
+ size="sm"
+ w="full"
+ colorScheme="red"
+ onClick={() => onDelete()}
+ leftIcon={isLoading ? <Spinner size="xs" /> : undefined}
+ >
+ {t("delete")}
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/DeleteUserModal.tsx b/app/dashboard/src/components/DeleteUserModal.tsx
@@ -0,0 +1,101 @@
+import {
+ Button,
+ chakra,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { useDashboard } from "contexts/DashboardContext";
+import { FC, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { Icon } from "./Icon";
+
+export const DeleteIcon = chakra(TrashIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type DeleteUserModalProps = {
+ deleteCallback?: () => void;
+};
+
+export const DeleteUserModal: FC<DeleteUserModalProps> = () => {
+ const [loading, setLoading] = useState(false);
+ const { deletingUser: user, onDeletingUser, deleteUser } = useDashboard();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const onClose = () => {
+ onDeletingUser(null);
+ };
+ const onDelete = () => {
+ if (user) {
+ setLoading(true);
+ deleteUser(user)
+ .then(() => {
+ toast({
+ title: t("deleteUser.deleteSuccess", { username: user.username }),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .then(onClose)
+ .finally(setLoading.bind(null, false));
+ }
+ };
+ return (
+ <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3">
+ <ModalHeader pt={6}>
+ <Icon color="red">
+ <DeleteIcon />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("deleteUser.title")}
+ </Text>
+ {user && (
+ <Text
+ mt={1}
+ fontSize="sm"
+ _dark={{ color: "gray.400" }}
+ color="gray.600"
+ >
+ <Trans components={{ b: <b /> }}>
+ {t("deleteUser.prompt", { username: user.username })}
+ </Trans>
+ </Text>
+ )}
+ </ModalBody>
+ <ModalFooter display="flex">
+ <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
+ {t("cancel")}
+ </Button>
+ <Button
+ size="sm"
+ w="full"
+ colorScheme="red"
+ onClick={onDelete}
+ leftIcon={loading ? <Spinner size="xs" /> : undefined}
+ >
+ {t("delete")}
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/Filters.tsx b/app/dashboard/src/components/Filters.tsx
@@ -0,0 +1,137 @@
+import {
+ BoxProps,
+ Button,
+ chakra,
+ Grid,
+ GridItem,
+ HStack,
+ IconButton,
+ Input,
+ InputGroup,
+ InputLeftElement,
+ InputRightElement,
+ Spinner,
+} from "@chakra-ui/react";
+import {
+ ArrowPathIcon,
+ MagnifyingGlassIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+import classNames from "classnames";
+import { useDashboard } from "contexts/DashboardContext";
+import debounce from "lodash.debounce";
+import React, { FC, useState } from "react";
+import { useTranslation } from "react-i18next";
+
+const iconProps = {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+};
+
+const SearchIcon = chakra(MagnifyingGlassIcon, iconProps);
+const ClearIcon = chakra(XMarkIcon, iconProps);
+export const ReloadIcon = chakra(ArrowPathIcon, iconProps);
+
+export type FilterProps = {} & BoxProps;
+const setSearchField = debounce((username: string) => {
+ useDashboard.getState().onFilterChange({
+ ...useDashboard.getState().filters,
+ offset: 0,
+ username,
+ });
+}, 300);
+
+export const Filters: FC<FilterProps> = ({ ...props }) => {
+ const { loading, filters, onFilterChange, refetchUsers, onCreateUser } =
+ useDashboard();
+ const { t } = useTranslation();
+ const [search, setSearch] = useState("");
+ const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setSearch(e.target.value);
+ setSearchField(e.target.value);
+ };
+ const clear = () => {
+ setSearch("");
+ onFilterChange({
+ ...filters,
+ offset: 0,
+ username: "",
+ });
+ };
+ return (
+ <Grid
+ id="filters"
+ templateColumns={{
+ lg: "repeat(3, 1fr)",
+ md: "repeat(4, 1fr)",
+ base: "repeat(1, 1fr)",
+ }}
+ position="sticky"
+ top={0}
+ mx="-6"
+ px="6"
+ rowGap={4}
+ gap={{
+ lg: 4,
+ base: 0,
+ }}
+ bg="var(--chakra-colors-chakra-body-bg)"
+ py={4}
+ zIndex="docked"
+ {...props}
+ >
+ <GridItem colSpan={{ base: 1, md: 2, lg: 1 }} order={{ base: 2, md: 1 }}>
+ <InputGroup>
+ <InputLeftElement pointerEvents="none" children={<SearchIcon />} />
+ <Input
+ placeholder={t("search")}
+ value={search}
+ borderColor="light-border"
+ onChange={onChange}
+ />
+
+ <InputRightElement>
+ {loading && <Spinner size="xs" />}
+ {filters.username && filters.username.length > 0 && (
+ <IconButton
+ onClick={clear}
+ aria-label="clear"
+ size="xs"
+ variant="ghost"
+ >
+ <ClearIcon />
+ </IconButton>
+ )}
+ </InputRightElement>
+ </InputGroup>
+ </GridItem>
+ <GridItem colSpan={2} order={{ base: 1, md: 2 }}>
+ <HStack justifyContent="flex-end" alignItems="center" h="full">
+ <IconButton
+ aria-label="refresh users"
+ disabled={loading}
+ onClick={refetchUsers}
+ size="sm"
+ variant="outline"
+ >
+ <ReloadIcon
+ className={classNames({
+ "animate-spin": loading,
+ })}
+ />
+ </IconButton>
+ <Button
+ colorScheme="primary"
+ size="sm"
+ onClick={() => onCreateUser(true)}
+ px={5}
+ >
+ {t("createUser")}
+ </Button>
+ </HStack>
+ </GridItem>
+ </Grid>
+ );
+};
diff --git a/app/dashboard/src/components/Footer.tsx b/app/dashboard/src/components/Footer.tsx
@@ -0,0 +1,28 @@
+import { BoxProps, HStack, Link, Text } from "@chakra-ui/react";
+import { ORGANIZATION_URL, REPO_URL } from "constants/Project";
+import { useDashboard } from "contexts/DashboardContext";
+import { FC } from "react";
+
+export const Footer: FC<BoxProps> = (props) => {
+ const { version } = useDashboard();
+ return (
+ <HStack w="full" py="0" position="relative" {...props}>
+ <Text
+ display="inline-block"
+ flexGrow={1}
+ textAlign="center"
+ color="gray.500"
+ fontSize="xs"
+ >
+ <Link color="blue.400" href={REPO_URL}>
+ Marzban
+ </Link>
+ {version ? ` (v${version}), ` : ", "}
+ Made with ❤️ in{" "}
+ <Link color="blue.400" href={ORGANIZATION_URL}>
+ Gozargah
+ </Link>
+ </Text>
+ </HStack>
+ );
+};
diff --git a/app/dashboard/src/components/Header.tsx b/app/dashboard/src/components/Header.tsx
@@ -0,0 +1,238 @@
+import {
+ Box,
+ chakra,
+ HStack,
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ Text,
+ useColorMode,
+} from "@chakra-ui/react";
+import {
+ ArrowLeftOnRectangleIcon,
+ Bars3Icon,
+ ChartPieIcon,
+ Cog6ToothIcon,
+ CurrencyDollarIcon,
+ DocumentMinusIcon,
+ LinkIcon,
+ MoonIcon,
+ SquaresPlusIcon,
+ SunIcon,
+} from "@heroicons/react/24/outline";
+import { DONATION_URL, REPO_URL } from "constants/Project";
+import { useDashboard } from "contexts/DashboardContext";
+import differenceInDays from "date-fns/differenceInDays";
+import isValid from "date-fns/isValid";
+import { FC, ReactNode, useState } from "react";
+import GitHubButton from "react-github-btn";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+import { updateThemeColor } from "utils/themeColor";
+import { Language } from "./Language";
+
+type HeaderProps = {
+ actions?: ReactNode;
+};
+const iconProps = {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+};
+
+const DarkIcon = chakra(MoonIcon, iconProps);
+const LightIcon = chakra(SunIcon, iconProps);
+const CoreSettingsIcon = chakra(Cog6ToothIcon, iconProps);
+const SettingsIcon = chakra(Bars3Icon, iconProps);
+const LogoutIcon = chakra(ArrowLeftOnRectangleIcon, iconProps);
+const DonationIcon = chakra(CurrencyDollarIcon, iconProps);
+const HostsIcon = chakra(LinkIcon, iconProps);
+const NodesIcon = chakra(SquaresPlusIcon, iconProps);
+const NodesUsageIcon = chakra(ChartPieIcon, iconProps);
+const ResetUsageIcon = chakra(DocumentMinusIcon, iconProps);
+const NotificationCircle = chakra(Box, {
+ baseStyle: {
+ bg: "yellow.500",
+ w: "2",
+ h: "2",
+ rounded: "full",
+ position: "absolute",
+ },
+});
+
+const NOTIFICATION_KEY = "marzban-menu-notification";
+
+export const shouldShowDonation = (): boolean => {
+ const date = localStorage.getItem(NOTIFICATION_KEY);
+ if (!date) return true;
+ try {
+ if (date && isValid(parseInt(date))) {
+ if (differenceInDays(new Date(), new Date(parseInt(date))) >= 7)
+ return true;
+ return false;
+ }
+ return true;
+ } catch (err) {
+ return true;
+ }
+};
+
+export const Header: FC<HeaderProps> = ({ actions }) => {
+ const {
+ onEditingHosts,
+ onResetAllUsage,
+ onEditingNodes,
+ onShowingNodesUsage,
+ } = useDashboard();
+ const { t } = useTranslation();
+ const { colorMode, toggleColorMode } = useColorMode();
+ const [showDonationNotif, setShowDonationNotif] = useState(
+ shouldShowDonation()
+ );
+ const gBtnColor = colorMode === "dark" ? "dark_dimmed" : colorMode;
+
+ const handleOnClose = () => {
+ localStorage.setItem(NOTIFICATION_KEY, new Date().getTime().toString());
+ setShowDonationNotif(false);
+ };
+
+ return (
+ <HStack
+ gap={2}
+ justifyContent="space-between"
+ __css={{
+ "& .menuList": {
+ direction: "ltr",
+ },
+ }}
+ position="relative"
+ >
+ <Text as="h1" fontWeight="semibold" fontSize="2xl">
+ {t("users")}
+ </Text>
+ {showDonationNotif && (
+ <NotificationCircle top="0" right="0" zIndex={9999} />
+ )}
+ <Box overflow="auto" css={{ direction: "rtl" }}>
+ <HStack alignItems="center">
+ <Menu>
+ <MenuButton
+ as={IconButton}
+ size="sm"
+ variant="outline"
+ icon={
+ <>
+ <SettingsIcon />
+ </>
+ }
+ position="relative"
+ ></MenuButton>
+ <MenuList minW="170px" zIndex={99999} className="menuList">
+ <MenuItem
+ maxW="170px"
+ fontSize="sm"
+ icon={<HostsIcon />}
+ onClick={onEditingHosts.bind(null, true)}
+ >
+ {t("header.hostSettings")}
+ </MenuItem>
+ <MenuItem
+ maxW="170px"
+ fontSize="sm"
+ icon={<NodesIcon />}
+ onClick={onEditingNodes.bind(null, true)}
+ >
+ {t("header.nodeSettings")}
+ </MenuItem>
+ <MenuItem
+ maxW="170px"
+ fontSize="sm"
+ icon={<NodesUsageIcon />}
+ onClick={onShowingNodesUsage.bind(null, true)}
+ >
+ {t("header.nodesUsage")}
+ </MenuItem>
+ <MenuItem
+ maxW="170px"
+ fontSize="sm"
+ icon={<ResetUsageIcon />}
+ onClick={onResetAllUsage.bind(null, true)}
+ >
+ {t("resetAllUsage")}
+ </MenuItem>
+ <Link to={DONATION_URL} target="_blank">
+ <MenuItem
+ maxW="170px"
+ fontSize="sm"
+ icon={<DonationIcon />}
+ position="relative"
+ onClick={handleOnClose}
+ >
+ {t("header.donation")}{" "}
+ {showDonationNotif && (
+ <NotificationCircle top="3" right="2" />
+ )}
+ </MenuItem>
+ </Link>
+ <Link to="/login">
+ <MenuItem maxW="170px" fontSize="sm" icon={<LogoutIcon />}>
+ {t("header.logout")}
+ </MenuItem>
+ </Link>
+ </MenuList>
+ </Menu>
+
+ <IconButton
+ size="sm"
+ variant="outline"
+ aria-label="core settings"
+ onClick={() => {
+ useDashboard.setState({ isEditingCore: true });
+ }}
+ >
+ <CoreSettingsIcon />
+ </IconButton>
+
+ <Language />
+
+ <IconButton
+ size="sm"
+ variant="outline"
+ aria-label="switch theme"
+ onClick={() => {
+ updateThemeColor(colorMode == "dark" ? "light" : "dark");
+ toggleColorMode();
+ }}
+ >
+ {colorMode === "light" ? <DarkIcon /> : <LightIcon />}
+ </IconButton>
+
+ <Box
+ css={{ direction: "ltr" }}
+ display="flex"
+ alignItems="center"
+ pr="2"
+ __css={{
+ "& span": {
+ display: "inline-flex",
+ },
+ }}
+ >
+ <GitHubButton
+ href={REPO_URL}
+ data-color-scheme={`no-preference: ${gBtnColor}; light: ${gBtnColor}; dark: ${gBtnColor};`}
+ data-size="large"
+ data-show-count="true"
+ aria-label="Star Marzban on GitHub"
+ >
+ Star
+ </GitHubButton>
+ </Box>
+ </HStack>
+ </Box>
+ </HStack>
+ );
+};
diff --git a/app/dashboard/src/components/HostsDialog.tsx b/app/dashboard/src/components/HostsDialog.tsx
@@ -0,0 +1,906 @@
+import {
+ Accordion,
+ AccordionButton,
+ AccordionIcon,
+ AccordionItem,
+ AccordionPanel,
+ Badge,
+ Box,
+ Button,
+ Select as ChakraSelect,
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ HStack,
+ IconButton,
+ InputGroup,
+ InputRightElement,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverTrigger,
+ Portal,
+ Text,
+ Tooltip,
+ VStack,
+ chakra,
+ useToast,
+} from "@chakra-ui/react";
+import { InformationCircleIcon, LinkIcon } from "@heroicons/react/24/outline";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ proxyALPN,
+ proxyFingerprint,
+ proxyHostSecurity,
+} from "constants/Proxies";
+import { useHosts } from "contexts/HostsContext";
+import { FC, useEffect, useState } from "react";
+import {
+ FormProvider,
+ useFieldArray,
+ useForm,
+ useFormContext,
+} from "react-hook-form";
+import { Trans, useTranslation } from "react-i18next";
+import "slick-carousel/slick/slick-theme.css";
+import "slick-carousel/slick/slick.css";
+import { z } from "zod";
+import { useDashboard } from "../contexts/DashboardContext";
+import { DeleteIcon } from "./DeleteUserModal";
+import { Icon } from "./Icon";
+import { Input as CustomInput } from "./Input";
+
+const Select = chakra(ChakraSelect, {
+ baseStyle: {
+ bg: "white",
+ _dark: {
+ bg: "gray.700",
+ },
+ },
+});
+
+const Input = chakra(CustomInput, {
+ baseStyle: {
+ bg: "white",
+ _dark: {
+ bg: "gray.700",
+ },
+ },
+});
+
+const ModalIcon = chakra(LinkIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+const InfoIcon = chakra(InformationCircleIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ color: "gray.400",
+ cursor: "pointer",
+ },
+});
+
+const hostsSchema = z.record(
+ z.string().min(1),
+ z.array(
+ z.object({
+ remark: z.string().min(1, "Remark is required"),
+ address: z.string().min(1, "Address is required"),
+ port: z
+ .string()
+ .or(z.number())
+ .nullable()
+ .transform((value) => {
+ if (typeof value === "number") return value;
+ if (value !== null && !isNaN(parseInt(value)))
+ return Number(parseInt(value));
+ return null;
+ }),
+ path: z.string().nullable(),
+ sni: z.string().nullable(),
+ host: z.string().nullable(),
+ security: z.string(),
+ alpn: z.string(),
+ fingerprint: z.string(),
+ }),
+ ),
+);
+
+const Error = chakra(FormErrorMessage, {
+ baseStyle: {
+ color: "red.400",
+ display: "block",
+ textAlign: "left",
+ w: "100%",
+ },
+});
+
+type AccordionInboundType = {
+ hostKey: string;
+ isOpen: boolean;
+ toggleAccordion: () => void;
+};
+
+const AccordionInbound: FC<AccordionInboundType> = ({
+ hostKey,
+ isOpen,
+ toggleAccordion,
+}) => {
+ const { inbounds } = useDashboard();
+ const inbound = [...inbounds.values()]
+ .flat()
+ .filter((inbound) => inbound.tag === hostKey)[0];
+
+ const form = useFormContext<z.infer<typeof hostsSchema>>();
+ const {
+ fields: hosts,
+ append: addHost,
+ remove: removeHost,
+ } = useFieldArray({
+ control: form.control,
+ name: hostKey,
+ });
+ const { errors } = form.formState;
+ const { t } = useTranslation();
+ const accordionErrors = errors[hostKey];
+ const handleAddHost = () => {
+ addHost({
+ host: "",
+ sni: "",
+ port: null,
+ path: null,
+ address: "",
+ remark: "",
+ security: "inbound_default",
+ alpn: "",
+ fingerprint: "",
+ });
+ };
+ useEffect(() => {
+ if (accordionErrors && !isOpen) {
+ toggleAccordion();
+ }
+ }, [accordionErrors]);
+ return (
+ <AccordionItem
+ border="1px solid"
+ _dark={{ borderColor: "gray.600" }}
+ _light={{ borderColor: "gray.200" }}
+ borderRadius="4px"
+ p={1}
+ w="full"
+ >
+ <AccordionButton px={2} borderRadius="3px" onClick={toggleAccordion}>
+ <Text
+ as="span"
+ fontWeight="medium"
+ fontSize="sm"
+ flex="1"
+ textAlign="left"
+ color="gray.700"
+ _dark={{ color: "gray.300" }}
+ >
+ {hostKey}
+ </Text>
+ <AccordionIcon />
+ </AccordionButton>
+ <AccordionPanel px={2} pb={2}>
+ <VStack gap={3}>
+ {hosts.map((host, index) => (
+ <VStack
+ key={index}
+ border="1px solid"
+ _dark={{ borderColor: "gray.600", bg: "#273142" }}
+ _light={{ borderColor: "gray.200", bg: "#fcfbfb" }}
+ p={2}
+ w="full"
+ borderRadius="4px"
+ >
+ <HStack w="100%" alignItems="flex-start">
+ <FormControl
+ position="relative"
+ zIndex={10}
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.remark)
+ }
+ >
+ <InputGroup>
+ <Input
+ {...form.register(hostKey + "." + index + ".remark")}
+ size="sm"
+ borderRadius="4px"
+ placeholder="Remark"
+ />
+ <InputRightElement>
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <Box mt="-8px">
+ <InfoIcon />
+ </Box>
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <PopoverBody>
+ <Box fontSize="xs">
+ <Text pr="20px">{t("hostsDialog.desc")}</Text>
+ <Text>
+ <Badge>
+ {"{"}SERVER_IP{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.currentServer")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}USERNAME{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.username")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_USAGE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.dataUsage")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingData")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_LIMIT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.dataLimit")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DAYS_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingDays")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}EXPIRE_DATE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.expireDate")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}JALALI_EXPIRE_DATE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.jalaliExpireDate")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}TIME_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingTime")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}STATUS_EMOJI{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.statusEmoji")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}PROTOCOL{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.proxyProtocol")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}TRANSPORT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.proxyMethod")}
+ </Text>
+ </Box>
+ </PopoverBody>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </InputRightElement>
+ </InputGroup>
+ {accordionErrors && accordionErrors[index]?.remark && (
+ <Error>{accordionErrors[index]?.remark?.message}</Error>
+ )}
+ </FormControl>
+ </HStack>
+ <FormControl
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.address)
+ }
+ >
+ <InputGroup>
+ <Input
+ size="sm"
+ borderRadius="4px"
+ placeholder="Address (e.g. example.com)"
+ {...form.register(hostKey + "." + index + ".address")}
+ />
+ <InputRightElement>
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <Box mt="-8px">
+ <InfoIcon />
+ </Box>
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <PopoverBody>
+ <Box fontSize="xs">
+ <Text pr="20px">{t("hostsDialog.desc")}</Text>
+ <Text>
+ <Badge>
+ {"{"}SERVER_IP{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.currentServer")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}USERNAME{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.username")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_USAGE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.dataUsage")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingData")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DATA_LIMIT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.dataLimit")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}DAYS_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingDays")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}EXPIRE_DATE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.expireDate")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}JALALI_EXPIRE_DATE{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.jalaliExpireDate")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}TIME_LEFT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.remainingTime")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}STATUS_EMOJI{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.statusEmoji")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}PROTOCOL{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.proxyProtocol")}
+ </Text>
+ <Text mt={1}>
+ <Badge>
+ {"{"}TRANSPORT{"}"}
+ </Badge>{" "}
+ {t("hostsDialog.proxyMethod")}
+ </Text>
+ </Box>
+ </PopoverBody>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </InputRightElement>
+ </InputGroup>
+ {accordionErrors && accordionErrors[index]?.address && (
+ <Error>{accordionErrors[index]?.address?.message}</Error>
+ )}
+ </FormControl>
+
+ <Accordion w="full" allowToggle>
+ <AccordionItem border="0">
+ <AccordionButton
+ display="flex"
+ justifyContent="space-between"
+ px={0}
+ py={1}
+ borderRadius={3}
+ _hover={{ bg: "transparent" }}
+ >
+ <Text
+ fontSize="xs"
+ color="gray.600"
+ _dark={{ color: "gray.500" }}
+ pl={1}
+ >
+ {t("hostsDialog.advancedOptions")}
+ <AccordionIcon fontSize="sm" ml={1} />
+ </Text>
+
+ <Tooltip label="Delete" placement="top">
+ <IconButton
+ aria-label="Delete"
+ size="sm"
+ colorScheme="red"
+ variant="ghost"
+ onClick={removeHost.bind(null, index)}
+ >
+ <DeleteIcon />
+ </IconButton>
+ </Tooltip>
+ </AccordionButton>
+ <AccordionPanel w="full" p={1}>
+ <VStack key={index} w="full" borderRadius="4px">
+ <FormControl
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.port)
+ }
+ >
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ justifyContent="space-between"
+ gap={1}
+ m="0"
+ >
+ <span>{t("hostsDialog.port")}</span>
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <InfoIcon />
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent p={2}>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <Text fontSize="xs" pr={5}>
+ {t("hostsDialog.port.info")}
+ </Text>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </FormLabel>
+ <Input
+ size="sm"
+ borderRadius="4px"
+ placeholder={String(inbound.port || "8080")}
+ type="number"
+ {...form.register(hostKey + "." + index + ".port")}
+ />
+ </FormControl>
+ <FormControl
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.sni)
+ }
+ >
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.sni")}</span>
+
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <InfoIcon />
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent p={2}>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <Text fontSize="xs" pr={5}>
+ {t("hostsDialog.sni.info")}
+ </Text>
+ <Text fontSize="xs" mt="2">
+ <Trans
+ i18nKey="hostsDialog.host.wildcard"
+ components={{
+ badge: <Badge />,
+ }}
+ />
+ </Text>
+ <Text fontSize="xs">
+ <Trans
+ i18nKey="hostsDialog.host.multiHost"
+ components={{
+ badge: <Badge />,
+ }}
+ />
+ </Text>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </FormLabel>
+ <Input
+ size="sm"
+ borderRadius="4px"
+ placeholder="SNI (e.g. example.com)"
+ {...form.register(hostKey + "." + index + ".sni")}
+ />
+ {accordionErrors && accordionErrors[index]?.sni && (
+ <Error>{accordionErrors[index]?.sni?.message}</Error>
+ )}
+ </FormControl>
+ <FormControl
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.host)
+ }
+ >
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.host")}</span>
+
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <InfoIcon />
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent p={2}>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <Text fontSize="xs" pr={5}>
+ {t("hostsDialog.host.info")}
+ </Text>
+ <Text fontSize="xs" mt="2">
+ <Trans
+ i18nKey="hostsDialog.host.wildcard"
+ components={{
+ badge: <Badge />,
+ }}
+ />
+ </Text>
+ <Text fontSize="xs">
+ <Trans
+ i18nKey="hostsDialog.host.multiHost"
+ components={{
+ badge: <Badge />,
+ }}
+ />
+ </Text>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </FormLabel>
+ <Input
+ size="sm"
+ borderRadius="4px"
+ placeholder="Host (e.g. example.com)"
+ {...form.register(hostKey + "." + index + ".host")}
+ />
+ {accordionErrors && accordionErrors[index]?.host && (
+ <Error>{accordionErrors[index]?.host?.message}</Error>
+ )}
+ </FormControl>
+
+ <FormControl
+ isInvalid={
+ !!(accordionErrors && accordionErrors[index]?.path)
+ }
+ >
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.path")}</span>
+
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <InfoIcon />
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent p={2}>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <Text fontSize="xs" pr={5}>
+ {t("hostsDialog.path.info")}
+ </Text>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </FormLabel>
+ <Input
+ size="sm"
+ borderRadius="4px"
+ placeholder="path (e.g. /vless)"
+ {...form.register(hostKey + "." + index + ".path")}
+ />
+ {accordionErrors && accordionErrors[index]?.path && (
+ <Error>{accordionErrors[index]?.path?.message}</Error>
+ )}
+ </FormControl>
+
+ <FormControl height="66px">
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.security")}</span>
+
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <InfoIcon />
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent p={2}>
+ <PopoverArrow />
+ <PopoverCloseButton />
+ <Text fontSize="xs" pr={5}>
+ {t("hostsDialog.security.info")}
+ </Text>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </FormLabel>
+ <Select
+ size="sm"
+ {...form.register(
+ hostKey + "." + index + ".security",
+ )}
+ >
+ {proxyHostSecurity.map((s) => {
+ return (
+ <option key={s.value} value={s.value}>
+ {s.title}
+ </option>
+ );
+ })}
+ </Select>
+ </FormControl>
+
+ <FormControl height="66px">
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.alpn")}</span>
+ </FormLabel>
+ <Select
+ size="sm"
+ {...form.register(hostKey + "." + index + ".alpn")}
+ >
+ {proxyALPN.map((s) => {
+ return (
+ <option key={s.value} value={s.value}>
+ {s.title}
+ </option>
+ );
+ })}
+ </Select>
+ </FormControl>
+
+ <FormControl height="66px">
+ <FormLabel
+ display="flex"
+ pb={1}
+ alignItems="center"
+ gap={1}
+ justifyContent="space-between"
+ m="0"
+ >
+ <span>{t("hostsDialog.fingerprint")}</span>
+ </FormLabel>
+ <Select
+ size="sm"
+ {...form.register(
+ hostKey + "." + index + ".fingerprint",
+ )}
+ >
+ {proxyFingerprint.map((s) => {
+ return (
+ <option key={s.value} value={s.value}>
+ {s.title}
+ </option>
+ );
+ })}
+ </Select>
+ </FormControl>
+ </VStack>
+ </AccordionPanel>
+ </AccordionItem>
+ </Accordion>
+ </VStack>
+ ))}
+ <Button
+ variant="outline"
+ w="full"
+ size="sm"
+ color=""
+ fontWeight={"normal"}
+ onClick={handleAddHost}
+ >
+ {t("hostsDialog.addHost")}
+ </Button>
+ </VStack>
+ </AccordionPanel>
+ </AccordionItem>
+ );
+};
+
+export const HostsDialog: FC = () => {
+ const { isEditingHosts, onEditingHosts, refetchUsers, inbounds } =
+ useDashboard();
+ const { isLoading, hosts, fetchHosts, isPostLoading, setHosts } = useHosts();
+ const toast = useToast();
+ const { t } = useTranslation();
+ const [openAccordions, setOpenAccordions] = useState<any>({});
+
+ useEffect(() => {
+ if (isEditingHosts) fetchHosts();
+ }, [isEditingHosts]);
+ const form = useForm<z.infer<typeof hostsSchema>>({
+ resolver: zodResolver(hostsSchema),
+ });
+
+ useEffect(() => {
+ if (hosts && isEditingHosts) {
+ form.reset(hosts);
+ }
+ }, [hosts]);
+
+ const onClose = () => {
+ setOpenAccordions({});
+ onEditingHosts(false);
+ };
+ const handleFormSubmit = (hosts: z.infer<typeof hostsSchema>) => {
+ setHosts(hosts)
+ .then(() => {
+ toast({
+ title: t("hostsDialog.savedSuccess"),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ refetchUsers();
+ })
+ .catch((err) => {
+ if (err?.response?.status === 409 || err?.response?.status === 400) {
+ toast({
+ title: err.response?._data?.detail,
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ }
+ if (err?.response?.status === 422) {
+ Object.keys(err.response._data.detail).forEach((key) => {
+ toast({
+ title: err.response._data.detail[key] + " (" + key + ")",
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ });
+ }
+ });
+ };
+
+ const toggleAccordion = (index: number) => {
+ if (openAccordions[String(index)]) {
+ delete openAccordions[String(index)];
+ } else openAccordions[String(index)] = {};
+
+ setOpenAccordions({ ...openAccordions });
+ };
+
+ return (
+ <Modal isOpen={isEditingHosts} onClose={onClose}>
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="fit-content" maxW="3xl">
+ <ModalHeader pt={6}>
+ <Icon color="primary">
+ <ModalIcon color="white" />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody w="440px" pb={3} pt={3}>
+ <FormProvider {...form}>
+ <form onSubmit={form.handleSubmit(handleFormSubmit)}>
+ <Text mb={3} opacity={0.8} fontSize="sm">
+ {t("hostsDialog.title")}
+ </Text>
+ {isLoading && t("hostsDialog.loading")}
+ {!isLoading &&
+ hosts &&
+ (Object.keys(hosts).length > 0 ? (
+ <Accordion
+ w="full"
+ allowToggle
+ allowMultiple
+ index={Object.keys(openAccordions).map((i) => parseInt(i))}
+ >
+ <VStack w="full">
+ {Object.keys(hosts).map((hostKey, index) => {
+ return (
+ <AccordionInbound
+ toggleAccordion={() => toggleAccordion(index)}
+ isOpen={openAccordions[String(index)]}
+ key={hostKey}
+ hostKey={hostKey}
+ />
+ );
+ })}
+ </VStack>
+ </Accordion>
+ ) : (
+ "No inbound found. Please check your Xray config file."
+ ))}
+
+ <HStack justifyContent="flex-end" py={2}>
+ <Button
+ variant="solid"
+ mt="2"
+ type="submit"
+ colorScheme="primary"
+ size="sm"
+ px={5}
+ isLoading={isPostLoading}
+ disabled={isPostLoading}
+ >
+ {t("hostsDialog.apply")}
+ </Button>
+ </HStack>
+ </form>
+ </FormProvider>
+ </ModalBody>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/Icon.tsx b/app/dashboard/src/components/Icon.tsx
@@ -0,0 +1,61 @@
+import { Box, Text } from "@chakra-ui/react";
+import { FC, PropsWithChildren } from "react";
+
+export type IconType = {
+ color: string;
+};
+
+export const Icon: FC<PropsWithChildren<IconType>> = ({ children, color }) => {
+ return (
+ <Box
+ position="relative"
+ width="36px"
+ height="36px"
+ display="flex"
+ justifyContent="center"
+ alignItems="center"
+ _before={{
+ content: '""',
+ display: "block",
+ position: "absolute",
+ top: "0",
+ left: "0",
+ width: "calc(100%)",
+ height: "calc(100%)",
+ bg: `${color}.400`,
+ opacity: ".5",
+ borderRadius: "5px",
+ zIndex: "1",
+ _dark: {
+ bg: `${color}.400`,
+ },
+ }}
+ _after={{
+ content: '""',
+ display: "block",
+ position: "absolute",
+ top: "0",
+ left: "0",
+ width: "calc(100% + 10px)",
+ height: "calc(100% + 10px)",
+ transform: "translate(-5px, -5px)",
+ bg: `${color}.400`,
+ opacity: ".4",
+ borderRadius: "8px",
+ zIndex: "1",
+ _dark: {
+ bg: `${color}.400`,
+ },
+ }}
+ >
+ <Text
+ color={`${color}.500`}
+ _dark={{ color: `${color}.900` }}
+ position="relative"
+ zIndex="2"
+ >
+ {children}
+ </Text>
+ </Box>
+ );
+};
diff --git a/app/dashboard/src/components/Input.tsx b/app/dashboard/src/components/Input.tsx
@@ -0,0 +1,188 @@
+import {
+ chakra,
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ Input as ChakraInput,
+ InputGroup,
+ InputLeftAddon,
+ InputProps as ChakraInputProps,
+ InputRightAddon,
+ InputRightElement,
+ NumberDecrementStepper,
+ NumberIncrementStepper,
+ NumberInput,
+ NumberInputField,
+ NumberInputStepper
+} from "@chakra-ui/react";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import classNames from "classnames";
+import React, { PropsWithChildren, ReactNode } from "react";
+
+const ClearIcon = chakra(XMarkIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export type InputProps = PropsWithChildren<
+ {
+ value?: string;
+ className?: string;
+ endAdornment?: ReactNode;
+ startAdornment?: ReactNode;
+ type?: string;
+ placeholder?: string;
+ onChange?: (e: any) => void;
+ onBlur?: (e: any) => void;
+ onClick?: (e: any) => void;
+ name?: string;
+ error?: string;
+ disabled?: boolean;
+ step?: number;
+ label?: string;
+ clearable?: boolean;
+ } & ChakraInputProps
+>;
+
+export const Input = React.forwardRef<HTMLInputElement, InputProps>(
+ (
+ {
+ disabled,
+ step,
+ label,
+ className,
+ startAdornment,
+ endAdornment,
+ type = "text",
+ placeholder,
+ onChange,
+ onBlur,
+ name,
+ value,
+ onClick,
+ error,
+ clearable = false,
+ ...props
+ },
+ ref
+ ) => {
+ const clear = () => {
+ if (onChange)
+ onChange({
+ target: {
+ value: "",
+ name,
+ },
+ });
+ };
+ const { size = "md" } = props;
+ const Component = type == "number" ? NumberInputField : ChakraInput;
+ const Wrapper = type == "number" ? NumberInput : React.Fragment;
+ const wrapperProps =
+ type == "number"
+ ? {
+ keepWithinRange: true,
+ precision: 5,
+ format: (value: string | number) => {
+ return isNaN(parseFloat(String(value)))
+ ? value
+ : Number(parseFloat(String(value)).toFixed(5)) === 0
+ ? value
+ : Number(parseFloat(String(value)).toFixed(5));
+ },
+ min: 0,
+ step,
+ name,
+ type,
+ placeholder,
+ onChange: (v: any) => {
+ if (onChange) onChange(v);
+ },
+ onBlur,
+ value,
+ onClick,
+ disabled,
+ flexGrow: 1,
+ size,
+ }
+ : {};
+ return (
+ <FormControl isInvalid={!!error}>
+ {label && <FormLabel>{label}</FormLabel>}
+ <InputGroup
+ size={size}
+ w="full"
+ rounded="md"
+ _focusWithin={{
+ outline: "2px solid",
+ outlineColor: "primary.200",
+ }}
+ bg={disabled ? "gray.100" : "transparent"}
+ _dark={{ bg: disabled ? "gray.600" : "transparent" }}
+ >
+ {startAdornment && <InputLeftAddon>{startAdornment}</InputLeftAddon>}
+ <Wrapper {...wrapperProps}>
+ {/* @ts-ignore */}
+ <Component
+ name={name}
+ ref={ref}
+ step={step}
+ className={classNames(className)}
+ type={type}
+ placeholder={placeholder}
+ onChange={onChange}
+ onBlur={onBlur}
+ value={value}
+ onClick={onClick}
+ disabled={disabled}
+ flexGrow={1}
+ _focusVisible={{
+ outline: "none",
+ borderTopColor: "transparent",
+ borderRightColor: "transparent",
+ borderBottomColor: "transparent",
+ }}
+ _disabled={{
+ cursor: "not-allowed",
+ }}
+ {...props}
+ roundedLeft={startAdornment ? "0" : "md"}
+ roundedRight={endAdornment ? "0" : "md"}
+ />
+ {type == "number" && (
+ <>
+ <NumberInputStepper>
+ <NumberIncrementStepper />
+ <NumberDecrementStepper />
+ </NumberInputStepper>
+ </>
+ )}
+ </Wrapper>
+ {endAdornment && (
+ <InputRightAddon
+ borderLeftRadius={0}
+ borderRightRadius="6px"
+ bg="transparent"
+ >
+ {endAdornment}
+ </InputRightAddon>
+ )}
+ {clearable && value && value.length && (
+ <InputRightElement
+ borderLeftRadius={0}
+ borderRightRadius="6px"
+ bg="transparent"
+ onClick={clear}
+ cursor="pointer"
+ >
+ <ClearIcon />
+ </InputRightElement>
+ )}
+ </InputGroup>
+ {!!error && <FormErrorMessage>{error}</FormErrorMessage>}
+ </FormControl>
+ );
+ }
+);
diff --git a/app/dashboard/src/components/JsonEditor/index.tsx b/app/dashboard/src/components/JsonEditor/index.tsx
@@ -0,0 +1,57 @@
+import { Box, useColorMode } from "@chakra-ui/react";
+import JSONEditor, { JSONEditorMode, JSONEditorOptions } from "jsoneditor";
+import "jsoneditor/dist/jsoneditor.css";
+import { forwardRef, useEffect, useRef } from "react";
+import "./styles.css";
+import "./themes.js";
+
+export type JSONEditorProps = {
+ onChange: (value: string) => void;
+ json: any;
+ mode?: JSONEditorMode;
+};
+export const JsonEditor = forwardRef<HTMLDivElement, JSONEditorProps>(
+ ({ json, onChange, mode = "code" }, ref) => {
+ const { colorMode } = useColorMode();
+ const options: JSONEditorOptions = {
+ mode,
+ onChangeText: onChange,
+ statusBar: false,
+ mainMenuBar: false,
+ theme: colorMode === "dark" ? "ace/theme/nord_dark" : "ace/theme/dawn",
+ };
+
+ const jsonEditorContainer = useRef<HTMLDivElement>(null);
+ const jsonEditorRef = useRef<JSONEditor | null>(null);
+
+ useEffect(() => {
+ jsonEditorRef.current = new JSONEditor(
+ jsonEditorContainer.current!,
+ options
+ );
+
+ return () => {
+ if (jsonEditorRef.current) jsonEditorRef.current.destroy();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (jsonEditorRef.current) jsonEditorRef.current.update(json);
+ }, [json]);
+
+ return (
+ <Box
+ ref={ref}
+ border="1px solid"
+ borderColor="gray.300"
+ _dark={{
+ borderColor: "gray.500",
+ }}
+ borderRadius={5}
+ h="full"
+ >
+ <Box height="full" ref={jsonEditorContainer} />
+ </Box>
+ );
+ }
+);
diff --git a/app/dashboard/src/components/JsonEditor/styles.css b/app/dashboard/src/components/JsonEditor/styles.css
@@ -0,0 +1,259 @@
+.ace_gutter-cell{
+ padding-left: 20px;
+ padding-right: 16px !important;
+}
+.ace_editor, .jsoneditor{
+ border-radius: 4px;
+}
+.ace_editor{
+ min-height: 300px;
+}
+.jsoneditor {
+ border: 0;
+}
+
+/* light theme */
+.ace-dawn .ace_gutter {
+ background: #ebebeb;
+ color: #333
+}
+
+.ace-dawn .ace_print-margin {
+ width: 1px;
+ background: #e8e8e8
+}
+
+.ace-dawn {
+ background-color: #F9F9F9;
+ color: #080808
+}
+
+.ace-dawn .ace_cursor {
+ color: #000000
+}
+
+.ace-dawn .ace_marker-layer .ace_selection {
+ background: rgba(39, 95, 255, 0.30)
+}
+
+.ace-dawn.ace_multiselect .ace_selection.ace_start {
+ box-shadow: 0 0 3px 0px #F9F9F9;
+}
+
+.ace-dawn .ace_marker-layer .ace_step {
+ background: rgb(255, 255, 0)
+}
+
+.ace-dawn .ace_marker-layer .ace_bracket {
+ margin: -1px 0 0 -1px;
+ border: 1px solid rgba(75, 75, 126, 0.50)
+}
+
+.ace-dawn .ace_marker-layer .ace_active-line {
+ background: rgba(36, 99, 180, 0.12)
+}
+
+.ace-dawn .ace_gutter-active-line {
+ background-color : #dcdcdc
+}
+
+.ace-dawn .ace_marker-layer .ace_selected-word {
+ border: 1px solid rgba(39, 95, 255, 0.30)
+}
+
+.ace-dawn .ace_invisible {
+ color: rgba(75, 75, 126, 0.50)
+}
+
+.ace-dawn .ace_keyword,
+.ace-dawn .ace_meta {
+ color: #794938
+}
+
+.ace-dawn .ace_constant,
+.ace-dawn .ace_constant.ace_character,
+.ace-dawn .ace_constant.ace_character.ace_escape,
+.ace-dawn .ace_constant.ace_other {
+ color: #811F24
+}
+
+.ace-dawn .ace_invalid.ace_illegal {
+ text-decoration: underline;
+ font-style: italic;
+ color: #F8F8F8;
+ background-color: #B52A1D
+}
+
+.ace-dawn .ace_invalid.ace_deprecated {
+ text-decoration: underline;
+ font-style: italic;
+ color: #B52A1D
+}
+
+.ace-dawn .ace_support {
+ color: #691C97
+}
+
+.ace-dawn .ace_support.ace_constant {
+ color: #B4371F
+}
+
+.ace-dawn .ace_fold {
+ background-color: #794938;
+ border-color: #080808
+}
+
+.ace-dawn .ace_list,
+.ace-dawn .ace_markup.ace_list,
+.ace-dawn .ace_support.ace_function {
+ color: #693A17
+}
+
+.ace-dawn .ace_storage {
+ font-style: italic;
+ color: #A71D5D
+}
+
+.ace-dawn .ace_string {
+ color: #0B6125
+}
+
+.ace-dawn .ace_string.ace_regexp {
+ color: #CF5628
+}
+
+.ace-dawn .ace_comment {
+ font-style: italic;
+ color: #5A525F
+}
+
+.ace-dawn .ace_heading,
+.ace-dawn .ace_markup.ace_heading {
+ color: #19356D
+}
+
+.ace-dawn .ace_variable {
+ color: #234A97
+}
+
+.ace-dawn .ace_indent-guide {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y
+}
+
+.ace-dawn .ace_indent-guide-active {
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAZSURBVHjaYvj///9/hivKyv8BAAAA//8DACLqBhbvk+/eAAAAAElFTkSuQmCC") right repeat-y;
+}
+
+
+/* dark theme */
+.ace-nord-dark .ace_gutter {
+ color: #616e88;
+}
+
+.ace-nord-dark .ace_print-margin {
+ width: 1px;
+ background: #4c566a;
+}
+
+.ace-nord-dark {
+ background-color: #2e3440;
+ color: #d8dee9;
+}
+
+.ace-nord-dark .ace_entity.ace_other.ace_attribute-name,
+.ace-nord-dark .ace_storage {
+ color: #d8dee9;
+}
+
+.ace-nord-dark .ace_cursor {
+ color: #d8dee9;
+}
+
+.ace-nord-dark .ace_string.ace_regexp {
+ color: #bf616a;
+}
+
+.ace-nord-dark .ace_marker-layer .ace_active-line {
+ background: #434c5ecc;
+}
+.ace-nord-dark .ace_marker-layer .ace_selection {
+ background: #434c5ecc;
+}
+
+.ace-nord-dark.ace_multiselect .ace_selection.ace_start {
+ box-shadow: 0 0 3px 0px #2e3440;
+}
+
+.ace-nord-dark .ace_marker-layer .ace_step {
+ background: #ebcb8b;
+}
+
+.ace-nord-dark .ace_marker-layer .ace_bracket {
+ margin: -1px 0 0 -1px;
+ border: 1px solid #88c0d066;
+}
+
+.ace-nord-dark .ace_gutter-active-line {
+ background-color: #434c5ecc;
+}
+
+.ace-nord-dark .ace_marker-layer .ace_selected-word {
+ border: 1px solid #88c0d066;
+}
+
+.ace-nord-dark .ace_invisible {
+ color: #4c566a;
+}
+
+.ace-nord-dark .ace_keyword,
+.ace-nord-dark .ace_meta,
+.ace-nord-dark .ace_support.ace_class,
+.ace-nord-dark .ace_support.ace_type {
+ color: #81a1c1;
+}
+
+.ace-nord-dark .ace_constant.ace_character,
+.ace-nord-dark .ace_constant.ace_other {
+ color: #d8dee9;
+}
+
+.ace-nord-dark .ace_constant.ace_language {
+ color: #5e81ac;
+}
+
+.ace-nord-dark .ace_constant.ace_escape {
+ color: #ebcB8b;
+}
+
+.ace-nord-dark .ace_constant.ace_numeric {
+ color: #b48ead;
+}
+
+.ace-nord-dark .ace_fold {
+ background-color: #4c566a;
+ border-color: #d8dee9;
+}
+
+.ace-nord-dark .ace_entity.ace_name.ace_function,
+.ace-nord-dark .ace_entity.ace_name.ace_tag,
+.ace-nord-dark .ace_support.ace_function,
+.ace-nord-dark .ace_variable,
+.ace-nord-dark .ace_variable.ace_language {
+ color: #8fbcbb;
+}
+
+.ace-nord-dark .ace_string {
+ color: #a3be8c;
+}
+
+.ace-nord-dark .ace_comment {
+ color: #616e88;
+}
+
+.ace-nord-dark .ace_indent-guide {
+ box-shadow: inset -1px 0 0 0 #434c5eb3;
+}
+
+.ace-nord-dark .ace_indent-guide-active {
+ box-shadow: inset -1px 0 0 0 #8395b8b3;
+}
diff --git a/app/dashboard/src/components/JsonEditor/themes.js b/app/dashboard/src/components/JsonEditor/themes.js
@@ -0,0 +1,22 @@
+window.ace.define(
+ "ace/theme/nord_dark",
+ ["require", "exports", "module", "ace/lib/dom"],
+ (acequire, exports, module) => {
+ exports.isDark = true;
+ exports.cssClass = "ace-nord-dark";
+ const dom = acequire("../lib/dom");
+ dom.importCssString(exports.cssText, exports.cssClass);
+ }
+);
+
+window.ace.define(
+ "ace/theme/dawn",
+ ["require", "exports", "module", "ace/lib/dom"],
+ (acequire, exports, module) => {
+ exports.isDark = false;
+ exports.cssClass = "ace-dawn";
+
+ const dom = acequire("../lib/dom");
+ dom.importCssString(exports.cssText, exports.cssClass);
+ }
+);
diff --git a/app/dashboard/src/components/Language.tsx b/app/dashboard/src/components/Language.tsx
@@ -0,0 +1,72 @@
+import {
+ chakra,
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+} from "@chakra-ui/react";
+import { LanguageIcon } from "@heroicons/react/24/outline";
+import { FC, ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+
+type HeaderProps = {
+ actions?: ReactNode;
+};
+
+const LangIcon = chakra(LanguageIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export const Language: FC<HeaderProps> = ({ actions }) => {
+ const { i18n } = useTranslation();
+
+ var changeLanguage = (lang: string) => {
+ i18n.changeLanguage(lang);
+ };
+
+ return (
+ <Menu placement="bottom-end">
+ <MenuButton
+ as={IconButton}
+ size="sm"
+ variant="outline"
+ icon={<LangIcon />}
+ position="relative"
+ />
+ <MenuList minW="100px" zIndex={9999}>
+ <MenuItem
+ maxW="100px"
+ fontSize="sm"
+ onClick={() => changeLanguage("en")}
+ >
+ English
+ </MenuItem>
+ <MenuItem
+ maxW="100px"
+ fontSize="sm"
+ onClick={() => changeLanguage("fa")}
+ >
+ فارسی
+ </MenuItem>
+ <MenuItem
+ maxW="100px"
+ fontSize="sm"
+ onClick={() => changeLanguage("zh-cn")}
+ >
+ 简体中文
+ </MenuItem>
+ <MenuItem
+ maxW="100px"
+ fontSize="sm"
+ onClick={() => changeLanguage("ru")}
+ >
+ Русский
+ </MenuItem>
+ </MenuList>
+ </Menu>
+ );
+};
diff --git a/app/dashboard/src/components/NodesModal.tsx b/app/dashboard/src/components/NodesModal.tsx
@@ -0,0 +1,595 @@
+import {
+ Accordion,
+ AccordionButton,
+ AccordionIcon,
+ AccordionItem,
+ AccordionPanel,
+ Alert,
+ AlertDescription,
+ AlertIcon,
+ Badge,
+ Box,
+ Button,
+ ButtonProps,
+ chakra,
+ Checkbox,
+ Collapse,
+ FormControl,
+ FormLabel,
+ HStack,
+ IconButton,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Switch,
+ Text,
+ Tooltip,
+ useToast,
+ VStack,
+} from "@chakra-ui/react";
+import {
+ EyeIcon,
+ EyeSlashIcon,
+ PlusIcon as HeroIconPlusIcon,
+ SquaresPlusIcon,
+} from "@heroicons/react/24/outline";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ FetchNodesQueryKey,
+ getNodeDefaultValues,
+ NodeSchema,
+ NodeType,
+ useNodes,
+ useNodesQuery,
+} from "contexts/NodesContext";
+import { FC, ReactNode, useState } from "react";
+import { Controller, useForm, UseFormReturn } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import {
+ UseMutateFunction,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "react-query";
+import "slick-carousel/slick/slick-theme.css";
+import "slick-carousel/slick/slick.css";
+import { Status } from "types/User";
+import {
+ generateErrorMessage,
+ generateSuccessMessage,
+} from "utils/toastHandler";
+import { useDashboard } from "../contexts/DashboardContext";
+import { DeleteNodeModal } from "./DeleteNodeModal";
+import { DeleteIcon } from "./DeleteUserModal";
+import { ReloadIcon } from "./Filters";
+import { Icon } from "./Icon";
+import { StatusBadge } from "./StatusBadge";
+
+import { fetch } from "service/http";
+import { Input } from "./Input";
+
+const CustomInput = chakra(Input, {
+ baseStyle: {
+ bg: "white",
+ _dark: {
+ bg: "gray.700",
+ },
+ },
+});
+
+const ModalIcon = chakra(SquaresPlusIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+const PlusIcon = chakra(HeroIconPlusIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ strokeWidth: 2,
+ },
+});
+
+type AccordionInboundType = {
+ toggleAccordion: () => void;
+ node: NodeType;
+};
+
+const NodeAccordion: FC<AccordionInboundType> = ({ toggleAccordion, node }) => {
+ const { updateNode, reconnectNode, setDeletingNode } = useNodes();
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+ const toast = useToast();
+ const form = useForm<NodeType>({
+ defaultValues: node,
+ resolver: zodResolver(NodeSchema),
+ });
+ const handleDeleteNode = setDeletingNode.bind(null, node);
+
+ const { isLoading, mutate } = useMutation(updateNode, {
+ onSuccess: () => {
+ generateSuccessMessage("Node updated successfully", toast);
+ queryClient.invalidateQueries(FetchNodesQueryKey);
+ },
+ onError: (e) => {
+ generateErrorMessage(e, toast, form);
+ },
+ });
+
+ const { isLoading: isReconnecting, mutate: reconnect } = useMutation(
+ reconnectNode.bind(null, node),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(FetchNodesQueryKey);
+ },
+ }
+ );
+
+ const nodeStatus: Status = isReconnecting
+ ? "connecting"
+ : node.status
+ ? node.status
+ : "error";
+
+ return (
+ <AccordionItem
+ border="1px solid"
+ _dark={{ borderColor: "gray.600" }}
+ _light={{ borderColor: "gray.200" }}
+ borderRadius="4px"
+ p={1}
+ w="full"
+ >
+ <AccordionButton px={2} borderRadius="3px" onClick={toggleAccordion}>
+ <HStack w="full" justifyContent="space-between" pr={2}>
+ <Text
+ as="span"
+ fontWeight="medium"
+ fontSize="sm"
+ flex="1"
+ textAlign="left"
+ color="gray.700"
+ _dark={{ color: "gray.300" }}
+ >
+ {node.name}
+ </Text>
+ <HStack>
+ {node.xray_version && (
+ <Badge
+ colorScheme="blue"
+ rounded="full"
+ display="inline-flex"
+ px={3}
+ py={1}
+ >
+ <Text
+ textTransform="capitalize"
+ fontSize="0.7rem"
+ fontWeight="medium"
+ letterSpacing="tighter"
+ >
+ Xray {node.xray_version}
+ </Text>
+ </Badge>
+ )}
+ {node.status && <StatusBadge status={nodeStatus} compact />}
+ </HStack>
+ </HStack>
+ <AccordionIcon />
+ </AccordionButton>
+ <AccordionPanel px={2} pb={2}>
+ <VStack pb={3} alignItems="flex-start">
+ {nodeStatus === "error" && (
+ <Alert status="error" size="xs">
+ <Box>
+ <HStack w="full">
+ <AlertIcon w={4} />
+ <Text marginInlineEnd={0}>{node.message}</Text>
+ </HStack>
+ <HStack justifyContent="flex-end" w="full">
+ <Button
+ size="sm"
+ aria-label="reconnect node"
+ leftIcon={<ReloadIcon />}
+ onClick={() => reconnect()}
+ disabled={isReconnecting}
+ >
+ {isReconnecting
+ ? t("nodes.reconnecting")
+ : t("nodes.reconnect")}
+ </Button>
+ </HStack>
+ </Box>
+ </Alert>
+ )}
+ </VStack>
+ <NodeForm
+ form={form}
+ mutate={mutate}
+ isLoading={isLoading}
+ submitBtnText={t("nodes.editNode")}
+ btnLeftAdornment={
+ <Tooltip label={t("delete")} placement="top">
+ <IconButton
+ colorScheme="red"
+ variant="ghost"
+ size="sm"
+ aria-label="delete node"
+ onClick={handleDeleteNode}
+ >
+ <DeleteIcon />
+ </IconButton>
+ </Tooltip>
+ }
+ />
+ </AccordionPanel>
+ </AccordionItem>
+ );
+};
+
+type AddNodeFormType = {
+ toggleAccordion: () => void;
+ resetAccordions: () => void;
+};
+
+const AddNodeForm: FC<AddNodeFormType> = ({
+ toggleAccordion,
+ resetAccordions,
+}) => {
+ const toast = useToast();
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+ const { addNode } = useNodes();
+ const form = useForm<NodeType>({
+ resolver: zodResolver(NodeSchema),
+ defaultValues: {
+ ...getNodeDefaultValues(),
+ add_as_new_host: true,
+ },
+ });
+ const { isLoading, mutate } = useMutation(addNode, {
+ onSuccess: () => {
+ generateSuccessMessage(
+ t("nodes.addNodeSuccess", { name: form.getValues("name") }),
+ toast
+ );
+ queryClient.invalidateQueries(FetchNodesQueryKey);
+ form.reset();
+ resetAccordions();
+ },
+ onError: (e) => {
+ generateErrorMessage(e, toast, form);
+ },
+ });
+ return (
+ <AccordionItem
+ border="1px solid"
+ _dark={{ borderColor: "gray.600" }}
+ _light={{ borderColor: "gray.200" }}
+ borderRadius="4px"
+ p={1}
+ w="full"
+ >
+ <AccordionButton px={2} borderRadius="3px" onClick={toggleAccordion}>
+ <Text
+ as="span"
+ fontWeight="medium"
+ fontSize="sm"
+ flex="1"
+ textAlign="left"
+ color="gray.700"
+ _dark={{ color: "gray.300" }}
+ display="flex"
+ gap={1}
+ >
+ <PlusIcon display={"inline-block"} />{" "}
+ <span>{t("nodes.addNewMarzbanNode")}</span>
+ </Text>
+ </AccordionButton>
+ <AccordionPanel px={2} py={4}>
+ <NodeForm
+ form={form}
+ mutate={mutate}
+ isLoading={isLoading}
+ submitBtnText={t("nodes.addNode")}
+ btnProps={{ variant: "solid" }}
+ addAsHost
+ />
+ </AccordionPanel>
+ </AccordionItem>
+ );
+};
+
+type NodeFormType = FC<{
+ form: UseFormReturn<NodeType>;
+ mutate: UseMutateFunction<unknown, unknown, any>;
+ isLoading: boolean;
+ submitBtnText: string;
+ btnProps?: Partial<ButtonProps>;
+ btnLeftAdornment?: ReactNode;
+ addAsHost?: boolean;
+}>;
+
+const NodeForm: NodeFormType = ({
+ form,
+ mutate,
+ isLoading,
+ submitBtnText,
+ btnProps = {},
+ btnLeftAdornment,
+ addAsHost = false,
+}) => {
+ const { t } = useTranslation();
+ const [showCertificate, setShowCertificate] = useState(false);
+ const { data: nodeSettings, isLoading: nodeSettingsLoading } = useQuery({
+ queryKey: "node-settings",
+ queryFn: () =>
+ fetch<{
+ min_node_version: string;
+ certificate: string;
+ }>("/node/settings"),
+ });
+ function selectText(node: HTMLElement) {
+ // @ts-ignore
+ if (document.body.createTextRange) {
+ // @ts-ignore
+ const range = document.body.createTextRange();
+ range.moveToElementText(node);
+ range.select();
+ } else if (window.getSelection) {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(node);
+ selection!.removeAllRanges();
+ selection!.addRange(range);
+ } else {
+ console.warn("Could not select text in node: Unsupported browser.");
+ }
+ }
+
+ return (
+ <form onSubmit={form.handleSubmit((v) => mutate(v))}>
+ <VStack>
+ {nodeSettings && nodeSettings.certificate && (
+ <Alert status="info" alignItems="start">
+ <AlertDescription
+ display="flex"
+ flexDirection="column"
+ overflow="hidden"
+ >
+ <span>{t("nodes.connection-hint")}</span>
+ <HStack justify="end" py={2}>
+ <Button
+ as="a"
+ colorScheme="primary"
+ size="xs"
+ download="ssl_client_cert.pem"
+ href={URL.createObjectURL(
+ new Blob([nodeSettings.certificate], { type: "text/plain" })
+ )}
+ >
+ {t("nodes.download-certificate")}
+ </Button>
+ <Tooltip
+ placement="top"
+ label={t(
+ !showCertificate
+ ? "nodes.show-certificate"
+ : "nodes.show-certificate"
+ )}
+ >
+ <IconButton
+ aria-label={t(
+ !showCertificate
+ ? "nodes.show-certificate"
+ : "nodes.show-certificate"
+ )}
+ onClick={setShowCertificate.bind(null, !showCertificate)}
+ colorScheme="whiteAlpha"
+ color="primary"
+ size="xs"
+ >
+ {!showCertificate ? (
+ <EyeIcon width="15px" />
+ ) : (
+ <EyeSlashIcon width="15px" />
+ )}
+ </IconButton>
+ </Tooltip>
+ </HStack>
+ <Collapse in={showCertificate} animateOpacity>
+ <Text
+ bg="rgba(255,255,255,.5)"
+ _dark={{
+ bg: "rgba(255,255,255,.2)",
+ }}
+ rounded="md"
+ p="2"
+ lineHeight="1.2"
+ fontSize="10px"
+ fontFamily="Courier"
+ whiteSpace="pre"
+ overflow="auto"
+ onClick={(e) => {
+ selectText(e.target as HTMLElement);
+ }}
+ >
+ {nodeSettings.certificate}
+ </Text>
+ </Collapse>
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <HStack w="full">
+ <FormControl>
+ <CustomInput
+ label={t("nodes.nodeName")}
+ size="sm"
+ placeholder="Marzban-S2"
+ {...form.register("name")}
+ error={form.formState?.errors?.name?.message}
+ />
+ </FormControl>
+ <HStack px={1}>
+ <Controller
+ name="status"
+ control={form.control}
+ render={({ field }) => {
+ return (
+ <Tooltip
+ key={field.value}
+ placement="top"
+ label={
+ `${t("usersTable.status")}: ` +
+ (field.value !== "disabled" ? t("active") : t("disabled"))
+ }
+ textTransform="capitalize"
+ >
+ <Box mt="6">
+ <Switch
+ colorScheme="primary"
+ isChecked={field.value !== "disabled"}
+ onChange={(e) => {
+ if (e.target.checked) {
+ field.onChange("connecting");
+ } else {
+ field.onChange("disabled");
+ }
+ }}
+ />
+ </Box>
+ </Tooltip>
+ );
+ }}
+ />
+ </HStack>
+ </HStack>
+ <HStack alignItems="flex-start">
+ <Box w="50%">
+ <CustomInput
+ label={t("nodes.nodeAddress")}
+ size="sm"
+ placeholder="51.20.12.13"
+ {...form.register("address")}
+ error={form.formState?.errors?.address?.message}
+ />
+ </Box>
+ <Box w="25%">
+ <CustomInput
+ label={t("nodes.nodePort")}
+ size="sm"
+ placeholder="62050"
+ {...form.register("port")}
+ error={form.formState?.errors?.port?.message}
+ />
+ </Box>
+ <Box w="25%">
+ <CustomInput
+ label={t("nodes.nodeAPIPort")}
+ size="sm"
+ placeholder="62051"
+ {...form.register("api_port")}
+ error={form.formState?.errors?.api_port?.message}
+ />
+ </Box>
+ </HStack>
+ {addAsHost && (
+ <FormControl py={1}>
+ <Checkbox {...form.register("add_as_new_host")}>
+ <FormLabel m={0}>{t("nodes.addHostForEveryInbound")}</FormLabel>
+ </Checkbox>
+ </FormControl>
+ )}
+ <HStack w="full">
+ {btnLeftAdornment}
+ <Button
+ flexGrow={1}
+ type="submit"
+ colorScheme="primary"
+ size="sm"
+ px={5}
+ w="full"
+ isLoading={isLoading}
+ {...btnProps}
+ >
+ {submitBtnText}
+ </Button>
+ </HStack>
+ </VStack>
+ </form>
+ );
+};
+
+export const NodesDialog: FC = () => {
+ const { isEditingNodes, onEditingNodes } = useDashboard();
+ const { t } = useTranslation();
+ const [openAccordions, setOpenAccordions] = useState<any>({});
+ const { data: nodes, isLoading } = useNodesQuery();
+
+ const onClose = () => {
+ setOpenAccordions({});
+ onEditingNodes(false);
+ };
+
+ const toggleAccordion = (index: number | string) => {
+ if (openAccordions[String(index)]) {
+ delete openAccordions[String(index)];
+ } else openAccordions[String(index)] = {};
+
+ setOpenAccordions({ ...openAccordions });
+ };
+
+ return (
+ <>
+ <Modal isOpen={isEditingNodes} onClose={onClose}>
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="fit-content" maxW="3xl">
+ <ModalHeader pt={6}>
+ <Icon color="primary">
+ <ModalIcon color="white" />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody w="440px" pb={6} pt={3}>
+ <Text mb={3} opacity={0.8} fontSize="sm">
+ {t("nodes.title")}
+ </Text>
+ {isLoading && "loading..."}
+
+ <Accordion
+ w="full"
+ allowToggle
+ index={Object.keys(openAccordions).map((i) => parseInt(i))}
+ >
+ <VStack w="full">
+ {!isLoading &&
+ nodes &&
+ nodes.map((node, index) => {
+ return (
+ <NodeAccordion
+ toggleAccordion={() => toggleAccordion(index)}
+ key={node.name}
+ node={node}
+ />
+ );
+ })}
+
+ <AddNodeForm
+ toggleAccordion={() => toggleAccordion((nodes || []).length)}
+ resetAccordions={() => setOpenAccordions({})}
+ />
+ </VStack>
+ </Accordion>
+ </ModalBody>
+ </ModalContent>
+ </Modal>
+ <DeleteNodeModal deleteCallback={() => setOpenAccordions({})} />
+ </>
+ );
+};
diff --git a/app/dashboard/src/components/NodesUsage.tsx b/app/dashboard/src/components/NodesUsage.tsx
@@ -0,0 +1,114 @@
+import {
+ Box,
+ CircularProgress,
+ HStack,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ VStack,
+ chakra,
+ useColorMode,
+} from "@chakra-ui/react";
+import { ChartPieIcon } from "@heroicons/react/24/outline";
+import { FilterUsageType, useDashboard } from "contexts/DashboardContext";
+import { useNodes } from "contexts/NodesContext";
+import dayjs from "dayjs";
+import { FC, Suspense, useEffect, useState } from "react";
+import ReactApexChart from "react-apexcharts";
+import { useTranslation } from "react-i18next";
+import { Icon } from "./Icon";
+import { UsageFilter, createUsageConfig } from "./UsageFilter";
+
+const UsageIcon = chakra(ChartPieIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type NodesUsageProps = {};
+
+export const NodesUsage: FC<NodesUsageProps> = () => {
+ const { isShowingNodesUsage, onShowingNodesUsage } = useDashboard();
+ const { fetchNodesUsage } = useNodes();
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const { colorMode } = useColorMode();
+
+ const usageTitle = t("userDialog.total");
+ const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
+ const [usageFilter, setUsageFilter] = useState("1m");
+ const fetchUsageWithFilter = (query: FilterUsageType) => {
+ fetchNodesUsage(query).then((data: any) => {
+ const labels = [];
+ const series = [];
+ for (const key in data.usages) {
+ const entry = data.usages[key];
+ series.push(entry.uplink + entry.downlink);
+ labels.push(entry.node_name);
+ }
+ setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
+ });
+ };
+
+ useEffect(() => {
+ if (isShowingNodesUsage) {
+ fetchUsageWithFilter({
+ start: dayjs().utc().subtract(30, "day").format("YYYY-MM-DDTHH:00:00"),
+ });
+ }
+ }, [isShowingNodesUsage]);
+
+ const onClose = () => {
+ onShowingNodesUsage(false);
+ setUsageFilter("1m");
+ };
+
+ const disabled = loading;
+
+ return (
+ <Modal isOpen={isShowingNodesUsage} onClose={onClose} size="2xl">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="full">
+ <ModalHeader pt={6}>
+ <HStack gap={2}>
+ <Icon color="primary">
+ <UsageIcon color="white" />
+ </Icon>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("header.nodesUsage")}
+ </Text>
+ </HStack>
+ </ModalHeader>
+ <ModalCloseButton mt={3} disabled={disabled} />
+ <ModalBody>
+ <VStack gap={4}>
+ <UsageFilter
+ defaultValue={usageFilter}
+ onChange={(filter, query) => {
+ setUsageFilter(filter);
+ fetchUsageWithFilter(query);
+ }}
+ />
+ <Box justifySelf="center" w="full" maxW="300px" mt="4">
+ <Suspense fallback={<CircularProgress isIndeterminate />}>
+ <ReactApexChart
+ options={usage.options}
+ series={usage.series}
+ type="donut"
+ height="500px"
+ />
+ </Suspense>
+ </Box>
+ </VStack>
+ </ModalBody>
+ <ModalFooter mt="3"></ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/OnlineBadge.tsx b/app/dashboard/src/components/OnlineBadge.tsx
@@ -0,0 +1,31 @@
+import {FC} from "react";
+
+type UserStatusProps = {
+ lastOnline?: string | null;
+};
+
+const convertDateFormat = (lastOnline: string | null | undefined): number | null => {
+ if (!lastOnline) {
+ return null;
+ }
+
+ const date = new Date(lastOnline + "Z");
+ return Math.floor(date.getTime() / 1000);
+};
+
+export const OnlineBadge: FC<UserStatusProps> = ({lastOnline}) => {
+ const currentTimeInSeconds = Math.floor(Date.now() / 1000);
+ const unixTime = convertDateFormat(lastOnline);
+
+ if (typeof lastOnline === 'undefined' || lastOnline === null) {
+ return <div className="circle pulse orange"></div>;
+ }
+
+ const timeDifferenceInSeconds = unixTime ? currentTimeInSeconds - unixTime : Infinity;
+
+ if (timeDifferenceInSeconds > 0 && timeDifferenceInSeconds <= 60) {
+ return <div className="circle pulse green"></div>;
+ }
+
+ return <div className="circle pulse red"></div>;
+};
\ No newline at end of file
diff --git a/app/dashboard/src/components/OnlineStatus.tsx b/app/dashboard/src/components/OnlineStatus.tsx
@@ -0,0 +1,43 @@
+import {FC} from "react";
+import {Text} from "@chakra-ui/react";
+import {relativeExpiryDate} from "utils/dateFormatter";
+
+type UserStatusProps = {
+ lastOnline: string | null;
+};
+
+const convertDateFormat = (lastOnline: string | null): number | null => {
+ if (!lastOnline) {
+ return null;
+ }
+
+ const date = new Date(lastOnline + "Z");
+ return Math.floor(date.getTime() / 1000);
+};
+
+export const OnlineStatus: FC<UserStatusProps> = ({lastOnline}) => {
+ const currentTimeInSeconds = Math.floor(Date.now() / 1000);
+ const unixTime = convertDateFormat(lastOnline);
+
+ const timeDifferenceInSeconds = unixTime ? currentTimeInSeconds - unixTime : null;
+ const dateInfo = unixTime ? relativeExpiryDate(unixTime) : {status: "", time: "Not Connected Yet"};
+
+ return (
+ <Text
+ display="inline-block"
+ fontSize="xs"
+ fontWeight="medium"
+ ml="2"
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ >
+ {timeDifferenceInSeconds && timeDifferenceInSeconds <= 60
+ ? "Online"
+ : timeDifferenceInSeconds
+ ? `${dateInfo.time} ago`
+ : dateInfo.time}
+ </Text>
+ );
+};
\ No newline at end of file
diff --git a/app/dashboard/src/components/Pagination.tsx b/app/dashboard/src/components/Pagination.tsx
@@ -0,0 +1,168 @@
+import {
+ Box,
+ Button,
+ ButtonGroup,
+ chakra,
+ HStack,
+ Select,
+ Text,
+} from "@chakra-ui/react";
+import {
+ ArrowLongLeftIcon,
+ ArrowLongRightIcon,
+} from "@heroicons/react/24/outline";
+import { useDashboard } from "contexts/DashboardContext";
+import { ChangeEvent, FC } from "react";
+import { useTranslation } from "react-i18next";
+import { setUsersPerPageLimitSize } from "utils/userPreferenceStorage";
+
+const PrevIcon = chakra(ArrowLongLeftIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+const NextIcon = chakra(ArrowLongRightIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export type PaginationType = {
+ count: number;
+ perPage: number;
+ page: number;
+ onChange?: (page: number) => void;
+};
+
+const MINIMAL_PAGE_ITEM_COUNT = 5;
+
+/**
+ * Generate numeric page items around current page.
+ * - Always include first and last page
+ * - Add ellipsis if needed
+ */
+function generatePageItems(total: number, current: number, width: number) {
+ if (width < MINIMAL_PAGE_ITEM_COUNT) {
+ throw new Error(
+ `Must allow at least ${MINIMAL_PAGE_ITEM_COUNT} page items`
+ );
+ }
+ if (width % 2 === 0) {
+ throw new Error(`Must allow odd number of page items`);
+ }
+ if (total < width) {
+ return [...new Array(total).keys()];
+ }
+ const left = Math.max(
+ 0,
+ Math.min(total - width, current - Math.floor(width / 2))
+ );
+ const items: (string | number)[] = new Array(width);
+ for (let i = 0; i < width; i += 1) {
+ items[i] = i + left;
+ }
+ // replace non-ending items with placeholders
+ if (items[0] > 0) {
+ items[0] = 0;
+ items[1] = "prev-more";
+ }
+ if (items[items.length - 1] < total - 1) {
+ items[items.length - 1] = total - 1;
+ items[items.length - 2] = "next-more";
+ }
+ return items;
+}
+
+export const Pagination: FC = () => {
+ const {
+ filters,
+ onFilterChange,
+ users: { total },
+ } = useDashboard();
+ const { limit: perPage, offset } = filters;
+
+ const page = (offset || 0) / (perPage || 1);
+ const noPages = Math.ceil(total / (perPage || 1));
+ const pages = generatePageItems(noPages, page, 7);
+
+ const changePage = (page: number) => {
+ onFilterChange({
+ ...filters,
+ offset: page * (perPage as number),
+ });
+ };
+
+ const handlePageSizeChange = (e: ChangeEvent<HTMLSelectElement>) => {
+ onFilterChange({
+ ...filters,
+ limit: parseInt(e.target.value),
+ });
+ setUsersPerPageLimitSize(e.target.value);
+ };
+
+ const { t } = useTranslation();
+
+ return (
+ <HStack
+ justifyContent="space-between"
+ mt={4}
+ w="full"
+ display="flex"
+ columnGap={{ lg: 4, md: 0 }}
+ rowGap={{ md: 0, base: 4 }}
+ flexDirection={{ md: "row", base: "column" }}
+ >
+ <Box order={{ base: 2, md: 1 }}>
+ <HStack>
+ <Select
+ minW="60px"
+ value={perPage}
+ onChange={handlePageSizeChange}
+ size="sm"
+ rounded="md"
+ >
+ <option>10</option>
+ <option>20</option>
+ <option>30</option>
+ </Select>
+ <Text whiteSpace={"nowrap"} fontSize="sm">
+ {t("itemsPerPage")}
+ </Text>
+ </HStack>
+ </Box>
+
+ <ButtonGroup size="sm" isAttached variant="outline" order={{ base: 1, md: 2 }}>
+ <Button
+ leftIcon={<PrevIcon />}
+ onClick={changePage.bind(null, page - 1)}
+ isDisabled={page === 0 || noPages === 0}
+ >
+ {t("previous")}
+ </Button>
+ {pages.map((pageIndex) => {
+ if (typeof pageIndex === "string")
+ return <Button key={pageIndex}>...</Button>;
+ return (
+ <Button
+ key={pageIndex}
+ variant={(pageIndex as number) === page ? "solid" : "outline"}
+ onClick={changePage.bind(null, pageIndex)}
+ >
+ {(pageIndex as number) + 1}
+ </Button>
+ );
+ })}
+
+ <Button
+ rightIcon={<NextIcon />}
+ onClick={changePage.bind(null, page + 1)}
+ isDisabled={page + 1 === noPages || noPages === 0}
+ >
+ {t("next")}
+ </Button>
+ </ButtonGroup>
+ </HStack>
+ );
+};
diff --git a/app/dashboard/src/components/QRCodeDialog.tsx b/app/dashboard/src/components/QRCodeDialog.tsx
@@ -0,0 +1,175 @@
+import {
+ Box,
+ chakra,
+ HStack,
+ IconButton,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ QrCodeIcon,
+} from "@heroicons/react/24/outline";
+import { QRCodeCanvas } from "qrcode.react";
+import { FC, useState } from "react";
+import { useTranslation } from "react-i18next";
+import Slider from "react-slick";
+import "slick-carousel/slick/slick-theme.css";
+import "slick-carousel/slick/slick.css";
+import { useDashboard } from "../contexts/DashboardContext";
+import { Icon } from "./Icon";
+
+const QRCode = chakra(QRCodeCanvas);
+const NextIcon = chakra(ChevronRightIcon, {
+ baseStyle: {
+ w: 6,
+ h: 6,
+ color: "gray.600",
+ _dark: {
+ color: "white",
+ },
+ },
+});
+const PrevIcon = chakra(ChevronLeftIcon, {
+ baseStyle: {
+ w: 6,
+ h: 6,
+ color: "gray.600",
+ _dark: {
+ color: "white",
+ },
+ },
+});
+const QRIcon = chakra(QrCodeIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export const QRCodeDialog: FC = () => {
+ const { QRcodeLinks, setQRCode, setSubLink, subscribeUrl } = useDashboard();
+ const isOpen = QRcodeLinks !== null;
+ const [index, setIndex] = useState(0);
+ const { t } = useTranslation();
+ const onClose = () => {
+ setQRCode(null);
+ setSubLink(null);
+ };
+
+ const subscribeQrLink = String(subscribeUrl).startsWith("/")
+ ? window.location.origin + subscribeUrl
+ : String(subscribeUrl);
+
+ return (
+ <Modal isOpen={isOpen} onClose={onClose}>
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3" w="fit-content" maxW="3xl">
+ <ModalHeader pt={6}>
+ <Icon color="primary">
+ <QRIcon color="white" />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ {QRcodeLinks && (
+ <ModalBody
+ gap={{
+ base: "20px",
+ lg: "50px",
+ }}
+ pr={{
+ lg: "60px",
+ }}
+ px={{
+ base: "50px",
+ }}
+ display="flex"
+ justifyContent="center"
+ flexDirection={{
+ base: "column",
+ lg: "row",
+ }}
+ >
+ {subscribeUrl && (
+ <VStack>
+ <QRCode
+ mx="auto"
+ size={300}
+ p="2"
+ level={"L"}
+ includeMargin={false}
+ value={subscribeQrLink}
+ bg="white"
+ />
+ <Text display="block" textAlign="center" pb={3} mt={1}>
+ {t("qrcodeDialog.sublink")}
+ </Text>
+ </VStack>
+ )}
+ <Box w="300px">
+ <Slider
+ centerPadding="0px"
+ centerMode={true}
+ slidesToShow={1}
+ slidesToScroll={1}
+ dots={false}
+ afterChange={setIndex}
+ onInit={() => setIndex(0)}
+ nextArrow={
+ <IconButton
+ size="sm"
+ position="absolute"
+ display="flex !important"
+ _before={{ content: '""' }}
+ aria-label="next"
+ mr="-4"
+ >
+ <NextIcon />
+ </IconButton>
+ }
+ prevArrow={
+ <IconButton
+ size="sm"
+ position="absolute"
+ display="flex !important"
+ _before={{ content: '""' }}
+ aria-label="prev"
+ ml="-4"
+ >
+ <PrevIcon />
+ </IconButton>
+ }
+ >
+ {QRcodeLinks.map((link, i) => {
+ return (
+ <HStack key={i}>
+ <QRCode
+ mx="auto"
+ size={300}
+ p="2"
+ level={"L"}
+ includeMargin={false}
+ value={link}
+ bg="white"
+ />
+ </HStack>
+ );
+ })}
+ </Slider>
+ <Text display="block" textAlign="center" pb={3} mt={1}>
+ {index + 1} / {QRcodeLinks.length}
+ </Text>
+ </Box>
+ </ModalBody>
+ )}
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/RadioGroup.tsx b/app/dashboard/src/components/RadioGroup.tsx
@@ -0,0 +1,537 @@
+import {
+ Accordion,
+ AccordionButton,
+ AccordionItem,
+ AccordionPanel,
+ Badge,
+ Box,
+ chakra,
+ Checkbox,
+ FormControl,
+ HStack,
+ IconButton,
+ Input,
+ Select,
+ SimpleGrid,
+ Text,
+ useCheckbox,
+ useCheckboxGroup,
+ UseRadioProps,
+ VStack,
+} from "@chakra-ui/react";
+import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
+import { shadowsocksMethods, XTLSFlows } from "constants/Proxies";
+import {
+ InboundType,
+ ProtocolType,
+ useDashboard,
+} from "contexts/DashboardContext";
+import { t } from "i18next";
+import { FC, forwardRef, PropsWithChildren, useState } from "react";
+import {
+ ControllerRenderProps,
+ useFormContext,
+ useWatch,
+} from "react-hook-form";
+
+const SettingsIcon = chakra(EllipsisVerticalIcon, {
+ baseStyle: {
+ strokeWidth: "2px",
+ w: 5,
+ h: 5,
+ },
+});
+
+const InboundCard: FC<
+ PropsWithChildren<UseRadioProps & { inbound: InboundType }>
+> = ({ inbound, ...props }) => {
+ const { getCheckboxProps, getInputProps, getLabelProps, htmlProps } =
+ useCheckbox(props);
+ const inputProps = getInputProps();
+ return (
+ <Box as="label">
+ <input {...inputProps} />
+ <Box
+ w="fll"
+ position="relative"
+ {...htmlProps}
+ cursor="pointer"
+ borderRadius="sm"
+ border="1px solid"
+ borderColor={"gray.200"}
+ _dark={{
+ borderColor: "gray.600",
+ }}
+ display="flex"
+ alignItems="center"
+ justifyContent="space-between"
+ overflow="hidden"
+ _checked={{
+ bg: "gray.50",
+ outline: "2px",
+ boxShadow: "outline",
+ outlineColor: "primary.500",
+ borderColor: "transparent",
+ fontWeight: "medium",
+ _dark: {
+ bg: "gray.750",
+ borderColor: "transparent",
+ },
+ "& p": {
+ opacity: 1,
+ },
+ }}
+ __css={{
+ "& p": {
+ opacity: 0.8,
+ },
+ }}
+ textTransform="capitalize"
+ px={3}
+ py={2}
+ fontWeight="medium"
+ {...getCheckboxProps()}
+ >
+ <Checkbox
+ size="sm"
+ w="full"
+ maxW="full"
+ color="gray.700"
+ _dark={{ color: "gray.300" }}
+ textTransform="uppercase"
+ colorScheme="primary"
+ className="inbound-item"
+ isChecked={inputProps.checked}
+ pointerEvents="none"
+ flexGrow={1}
+ >
+ <HStack
+ justify="space-between"
+ w="full"
+ maxW="calc(100% - 20px)"
+ spacing={0}
+ gap={2}
+ overflow="hidden"
+ >
+ <Text isTruncated {...getLabelProps()} fontSize="xs">
+ {inbound.tag} <Text as="span">({inbound.network})</Text>
+ </Text>
+ </HStack>
+ </Checkbox>
+ {inbound.tls && inbound.tls != "none" && (
+ <Badge fontSize="xs" opacity=".8" size="xs">
+ {inbound.tls}
+ </Badge>
+ )}
+ </Box>
+ </Box>
+ );
+};
+
+const RadioCard: FC<
+ PropsWithChildren<
+ UseRadioProps & {
+ disabled?: boolean;
+ title: string;
+ description: string;
+ toggleAccordion: () => void;
+ isSelected: boolean;
+ }
+ >
+> = ({
+ disabled,
+ title,
+ description,
+ toggleAccordion,
+ isSelected,
+ ...props
+}) => {
+ const form = useFormContext();
+ const { inbounds } = useDashboard();
+ const { getCheckboxProps, getInputProps, getLabelProps, htmlProps } =
+ useCheckbox(props);
+
+ const inputProps = getInputProps();
+
+ const [inBoundDefaultValue] = useWatch({
+ name: [`inbounds.${title}`],
+ control: form.control,
+ });
+
+ const { getCheckboxProps: getInboundCheckboxProps } = useCheckboxGroup({
+ value: inBoundDefaultValue,
+ onChange: (selectedInbounds) => {
+ form.setValue(`inbounds.${title}`, selectedInbounds);
+ if (selectedInbounds.length === 0) {
+ const selected_proxies = form.getValues("selected_proxies");
+ form.setValue(
+ `selected_proxies`,
+ selected_proxies.filter((p: string) => p !== title)
+ );
+ toggleAccordion();
+ }
+ },
+ });
+
+ const isPartialSelected =
+ inBoundDefaultValue &&
+ isSelected &&
+ (useDashboard.getState().inbounds.get(title as ProtocolType) || [])
+ .length !== inBoundDefaultValue.length;
+
+ const protocolHasInbound =
+ (useDashboard.getState().inbounds.get(title as ProtocolType) || []).length >
+ 0;
+
+ const shouldBeDisabled = !isSelected && !protocolHasInbound;
+
+ return (
+ <AccordionItem
+ isDisabled={!protocolHasInbound}
+ borderRadius="md"
+ borderStyle="solid"
+ border="1px"
+ borderColor="gray.200"
+ bg={shouldBeDisabled ? "gray.100" : "transparent"}
+ _dark={{
+ borderColor: "gray.600",
+ bg: shouldBeDisabled ? "#364154" : "transparent",
+ }}
+ _checked={{
+ bg: "gray.50",
+ outline: "2px",
+ boxShadow: "outline",
+ outlineColor: "primary.500",
+ borderColor: "transparent",
+ }}
+ {...getCheckboxProps()}
+ >
+ <Box as={shouldBeDisabled ? "span" : "label"} position="relative">
+ {isPartialSelected && (
+ <Box
+ position="absolute"
+ w="2"
+ h="2"
+ bg="yellow.500"
+ top="-1"
+ right="-1"
+ rounded="full"
+ zIndex={999}
+ />
+ )}
+ <input {...inputProps} />
+ <Box
+ w="fll"
+ position="relative"
+ {...htmlProps}
+ borderRadius="md"
+ cursor={shouldBeDisabled ? "not-allowed" : "pointer"}
+ _checked={{
+ fontWeight: "medium",
+ _dark: {
+ bg: "gray.750",
+ borderColor: "transparent",
+ },
+ "& > svg": {
+ opacity: 1,
+ "&.checked": {
+ display: "block",
+ },
+ "&.unchecked": {
+ display: "none",
+ },
+ },
+ "& p": {
+ opacity: 1,
+ },
+ }}
+ __css={{
+ "& > svg": {
+ opacity: 0.3,
+ "&.checked": {
+ display: "none",
+ },
+ "&.unchecked": {
+ display: "block",
+ },
+ },
+ "& p": {
+ opacity: 0.8,
+ },
+ }}
+ textTransform="capitalize"
+ px={3}
+ py={2}
+ fontWeight="medium"
+ {...getCheckboxProps()}
+ >
+ <AccordionButton
+ display={
+ inputProps.checked && protocolHasInbound ? "block" : "none"
+ }
+ as="span"
+ className="checked"
+ color="primary.200"
+ position="absolute"
+ right="3"
+ top="3"
+ w="auto"
+ p={0}
+ onClick={toggleAccordion}
+ >
+ <IconButton size="sm" aria-label="inbound settings">
+ <SettingsIcon />
+ </IconButton>
+ </AccordionButton>
+
+ <Text
+ fontSize="sm"
+ color={shouldBeDisabled ? "gray.400" : "gray.700"}
+ _dark={{ color: shouldBeDisabled ? "gray.500" : "gray.300" }}
+ {...getLabelProps()}
+ >
+ {title}
+ </Text>
+ <Text
+ fontWeight="medium"
+ color={shouldBeDisabled ? "gray.400" : "gray.600"}
+ _dark={{ color: shouldBeDisabled ? "gray.500" : "gray.400" }}
+ fontSize="xs"
+ >
+ {description}
+ </Text>
+ </Box>
+ </Box>
+ <AccordionPanel
+ px={2}
+ pb={3}
+ roundedBottom="5px"
+ pt={3}
+ _dark={{ bg: inputProps.checked && "gray.750" }}
+ >
+ <VStack
+ w="full"
+ rowGap={2}
+ borderStyle="solid"
+ borderWidth="1px"
+ borderRadius="md"
+ pl={3}
+ pr={3}
+ pt={1.5}
+ _dark={{ bg: "gray.700" }}
+ >
+ <VStack alignItems="flex-start" w="full">
+ <Text fontSize="sm">{t("inbound")}</Text>
+ <SimpleGrid
+ gap={2}
+ alignItems="flex-start"
+ w="full"
+ columns={1}
+ spacing={1}
+ >
+ {(
+ (inbounds.get(title as ProtocolType) as InboundType[]) || []
+ ).map((inbound) => {
+ return (
+ <InboundCard
+ key={inbound.tag}
+ {...getInboundCheckboxProps({ value: inbound.tag })}
+ inbound={inbound}
+ />
+ );
+ })}
+ </SimpleGrid>
+ </VStack>
+ {title === "vmess" && isSelected && (
+ <VStack alignItems="flex-start" w="full">
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ ID
+ </Text>
+ <Input
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ pl={2}
+ pr={2}
+ placeholder={t("userDialog.generatedByDefault")}
+ {...form.register("proxies.vmess.id")}
+ />
+ </FormControl>
+ </VStack>
+ )}
+ {title === "vless" && isSelected && (
+ <VStack alignItems="flex-start" w="full">
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ ID
+ </Text>
+ <Input
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ pl={2}
+ pr={2}
+ placeholder={t("userDialog.generatedByDefault")}
+ {...form.register("proxies.vless.id")}
+ />
+ </FormControl>
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ Flow
+ </Text>
+ <Select
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ {...form.register("proxies.vless.flow")}
+ >
+ {XTLSFlows.map((entry) => (
+ <option key={entry.title} value={entry.value}>
+ {entry.title}
+ </option>
+ ))}
+ </Select>
+ </FormControl>
+ </VStack>
+ )}
+ {title === "trojan" && isSelected && (
+ <VStack alignItems="flex-start" w="full">
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ {t("password")}
+ </Text>
+ <Input
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ pl={2}
+ pr={2}
+ placeholder={t("userDialog.generatedByDefault")}
+ {...form.register("proxies.trojan.password")}
+ />
+ </FormControl>
+ </VStack>
+ )}
+ {title === "shadowsocks" && isSelected && (
+ <VStack alignItems="flex-start" w="full">
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ {t("password")}
+ </Text>
+ <Input
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ pl={2}
+ pr={2}
+ placeholder={t("userDialog.generatedByDefault")}
+ {...form.register("proxies.shadowsocks.password")}
+ />
+ </FormControl>
+ <FormControl height="66px">
+ <Text fontSize="sm" pb={1}>
+ {t("userDialog.method")}
+ </Text>
+ <Select
+ fontSize="xs"
+ size="sm"
+ borderRadius="6px"
+ {...form.register("proxies.shadowsocks.method")}
+ >
+ {shadowsocksMethods.map((method) => (
+ <option key={method} value={method}>
+ {method}
+ </option>
+ ))}
+ </Select>
+ </FormControl>
+ </VStack>
+ )}
+ </VStack>
+ </AccordionPanel>
+ </AccordionItem>
+ );
+};
+
+export type RadioListType = {
+ title: string;
+ description: string;
+};
+
+export type RadioGroupProps = ControllerRenderProps & {
+ list: RadioListType[];
+ disabled?: boolean;
+};
+
+export const RadioGroup = forwardRef<any, RadioGroupProps>(
+ ({ name, list, onChange, disabled, ...props }, ref) => {
+ const form = useFormContext();
+ const [expandedAccordions, setExpandedAccordions] = useState<number[]>([]);
+
+ const toggleAccordion = (i: number) => {
+ if (expandedAccordions.includes(i))
+ expandedAccordions.splice(expandedAccordions.indexOf(i), 1);
+ else expandedAccordions.push(i);
+ setExpandedAccordions([...expandedAccordions]);
+ };
+
+ const { getCheckboxProps } = useCheckboxGroup({
+ value: props.value,
+ onChange: (value) => {
+ // active all inbounds when a proxy selected
+ const selectedItem = value.filter((el) => !props.value.includes(el));
+ if (selectedItem[0]) {
+ form.setValue(
+ `inbounds.${selectedItem[0]}`,
+ useDashboard
+ .getState()
+ .inbounds.get(selectedItem[0] as ProtocolType)
+ ?.map((i) => i.tag)
+ );
+ }
+
+ setExpandedAccordions(
+ expandedAccordions.filter((i) => {
+ return value.find((title) => title === list[i].title);
+ })
+ );
+
+ onChange({
+ target: {
+ value,
+ name,
+ },
+ });
+ },
+ });
+
+ return (
+ <Accordion allowToggle index={expandedAccordions}>
+ <SimpleGrid
+ ref={ref}
+ gap={2}
+ alignItems="flex-start"
+ columns={1}
+ spacing={1}
+ >
+ {list.map((value, index) => {
+ return (
+ <RadioCard
+ toggleAccordion={toggleAccordion.bind(null, index)}
+ disabled={disabled}
+ key={value.title}
+ title={value.title}
+ description={value.description}
+ isSelected={
+ !!(props.value as string[]).find((v) => v === value.title)
+ }
+ {...getCheckboxProps({ value: value.title })}
+ />
+ );
+ })}
+ </SimpleGrid>
+ </Accordion>
+ );
+ }
+);
diff --git a/app/dashboard/src/components/ResetAllUsageModal.tsx b/app/dashboard/src/components/ResetAllUsageModal.tsx
@@ -0,0 +1,105 @@
+import {
+ Button,
+ chakra,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
+import { FC, useState } from "react";
+import { DocumentMinusIcon } from "@heroicons/react/24/outline";
+import { Icon } from "./Icon";
+import { useDashboard } from "contexts/DashboardContext";
+import { useTranslation } from "react-i18next";
+
+export const ResetIcon = chakra(DocumentMinusIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type DeleteUserModalProps = {};
+
+export const ResetAllUsageModal: FC<DeleteUserModalProps> = () => {
+ const [loading, setLoading] = useState(false);
+ const { isResetingAllUsage, onResetAllUsage, resetAllUsage } = useDashboard();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const onClose = () => {
+ onResetAllUsage(false);
+ };
+ const onReset = () => {
+ setLoading(true);
+ resetAllUsage()
+ .then(() => {
+ toast({
+ title: t("resetAllUsage.success"),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .catch(() => {
+ toast({
+ title: t("resetAllUsage.error"),
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+ return (
+ <Modal isCentered isOpen={isResetingAllUsage} onClose={onClose} size="sm">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3">
+ <ModalHeader pt={6}>
+ <Icon color="red">
+ <ResetIcon />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("resetAllUsage.title")}
+ </Text>
+ {isResetingAllUsage && (
+ <Text
+ mt={1}
+ fontSize="sm"
+ _dark={{ color: "gray.400" }}
+ color="gray.600"
+ >
+ {t("resetAllUsage.prompt")}
+ </Text>
+ )}
+ </ModalBody>
+ <ModalFooter display="flex">
+ <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
+ {t("cancel")}
+ </Button>
+ <Button
+ size="sm"
+ w="full"
+ colorScheme="red"
+ onClick={onReset}
+ leftIcon={loading ? <Spinner size="xs" /> : undefined}
+ >
+ {t("reset")}
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/ResetUserUsageModal.tsx b/app/dashboard/src/components/ResetUserUsageModal.tsx
@@ -0,0 +1,112 @@
+import {
+ Box,
+ Button,
+ chakra,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+ Text,
+ Toast,
+ useToast,
+} from "@chakra-ui/react";
+import { FC, useEffect, useRef, useState } from "react";
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
+import { Icon } from "./Icon";
+import { useDashboard } from "contexts/DashboardContext";
+import { useTranslation, Trans } from "react-i18next";
+
+export const ResetIcon = chakra(ArrowPathIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type DeleteUserModalProps = {};
+
+export const ResetUserUsageModal: FC<DeleteUserModalProps> = () => {
+ const [loading, setLoading] = useState(false);
+ const { resetUsageUser: user, resetDataUsage } = useDashboard();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const onClose = () => {
+ useDashboard.setState({ resetUsageUser: null });
+ };
+ const onReset = () => {
+ if (user) {
+ setLoading(true);
+ resetDataUsage(user)
+ .then(() => {
+ toast({
+ title: t("resetUserUsage.success", {username: user.username}),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .catch(() => {
+ toast({
+ title: t("resetUserUsage.error"),
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+ };
+ return (
+ <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3">
+ <ModalHeader pt={6}>
+ <Icon color="blue">
+ <ResetIcon />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("resetUserUsage.title")}
+ </Text>
+ {user && (
+ <Text
+ mt={1}
+ fontSize="sm"
+ _dark={{ color: "gray.400" }}
+ color="gray.600"
+ >
+ <Trans
+ components={{b: <b /> }}>
+ {t("resetUserUsage.prompt", {username: user.username})}
+ </Trans>
+ </Text>
+ )}
+ </ModalBody>
+ <ModalFooter display="flex">
+ <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
+ {t("cancel")}
+ </Button>
+ <Button
+ size="sm"
+ w="full"
+ colorScheme="blue"
+ onClick={onReset}
+ leftIcon={loading ? <Spinner size="xs" /> : undefined}
+ >
+ {t("reset")}
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/RevokeSubscriptionModal.tsx b/app/dashboard/src/components/RevokeSubscriptionModal.tsx
@@ -0,0 +1,109 @@
+import {
+ Button,
+ chakra,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spinner,
+ Text,
+ useToast,
+} from "@chakra-ui/react";
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
+import { useDashboard } from "contexts/DashboardContext";
+import { FC, useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { Icon } from "./Icon";
+
+export const ResetIcon = chakra(ArrowPathIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type RevokeSubscriptionModalProps = {};
+
+export const RevokeSubscriptionModal: FC<RevokeSubscriptionModalProps> = () => {
+ const [loading, setLoading] = useState(false);
+ const { revokeSubscriptionUser: user, revokeSubscription } = useDashboard();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const onClose = () => {
+ useDashboard.setState({ revokeSubscriptionUser: null });
+ };
+ const onReset = () => {
+ if (user) {
+ setLoading(true);
+ revokeSubscription(user)
+ .then(() => {
+ toast({
+ title: t("revokeUserSub.success", { username: user.username }),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .catch(() => {
+ toast({
+ title: t("revokeUserSub.error"),
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+ };
+ return (
+ <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <ModalContent mx="3">
+ <ModalHeader pt={6}>
+ <Icon color="blue">
+ <ResetIcon />
+ </Icon>
+ </ModalHeader>
+ <ModalCloseButton mt={3} />
+ <ModalBody>
+ <Text fontWeight="semibold" fontSize="lg">
+ {t("revokeUserSub.title")}
+ </Text>
+ {user && (
+ <Text
+ mt={1}
+ fontSize="sm"
+ _dark={{ color: "gray.400" }}
+ color="gray.600"
+ >
+ <Trans components={{ b: <b /> }}>
+ {t("revokeUserSub.prompt", { username: user.username })}
+ </Trans>
+ </Text>
+ )}
+ </ModalBody>
+ <ModalFooter display="flex">
+ <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
+ {t("cancel")}
+ </Button>
+ <Button
+ size="sm"
+ w="full"
+ colorScheme="blue"
+ onClick={onReset}
+ leftIcon={loading ? <Spinner size="xs" /> : undefined}
+ >
+ {t("revoke")}
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ );
+};
diff --git a/app/dashboard/src/components/ServicesSelectList.tsx b/app/dashboard/src/components/ServicesSelectList.tsx
@@ -0,0 +1,132 @@
+
+import {
+ Box,
+ chakra,
+ ChakraComponent,
+ Flex,
+ Spacer,
+ Stack,
+ Text,
+} from "@chakra-ui/react";
+import { ServerStackIcon, UsersIcon } from "@heroicons/react/24/outline";
+import {
+ useDashboard,
+} from "contexts/DashboardContext";
+import { FC, forwardRef, PropsWithChildren, useEffect, useState } from "react";
+import {
+ useFormContext,
+} from "react-hook-form";
+import { Service } from "types/Service";
+
+const ServiceIconBaseStyle = {
+ baseStyle: {
+ strokeWidth: "2px",
+ color: "primary.500",
+ w: 5,
+ h: 5,
+ mx: "6px",
+ },
+}
+
+const UsersServiceIcon = chakra(UsersIcon, ServiceIconBaseStyle);
+
+const InboundsServiceIcon = chakra(ServerStackIcon, ServiceIconBaseStyle);
+
+type ServicePropertyStatusProps = {
+ value: number;
+ StatusIcon: ChakraComponent;
+}
+
+const ServicePropertyStatus: FC<ServicePropertyStatusProps> = ({ value, StatusIcon }) => {
+ const [valueState, _] = useState(value);
+ return (
+ <Flex flexDirection="row">
+ <StatusIcon />
+ <span>{valueState}</span>
+ </Flex>
+ );
+};
+
+type CustomCheckboxProps = {
+ isChecked: boolean;
+ onChange: () => void;
+ service: Service;
+ value: string;
+}
+
+const CustomCheckbox = forwardRef(
+ ({ isChecked, onChange, service, ...props }: CustomCheckboxProps, ref: Ref<HTMLDivElement>) => (
+ <Box
+ ref={ref}
+ border={isChecked ? '5px' : '2px'}
+ w="full"
+ borderColor={isChecked ? 'primary.500' : 'gray.600'}
+ boxShadow={isChecked ? "0 0 0 3px rgba(102, 126, 234, 0.6)" : "0 0 0 1px rgba(102, 126, 234, 0.6)"}
+ bg={isChecked ? 'gray.100' : 'white'}
+ py="4px"
+ px="8px"
+ borderRadius="md"
+ display="flex"
+ alignItems="center"
+ justifyContent="center"
+ _hover={{
+ cursor: 'pointer',
+ borderColor: 'primary.300',
+ }}
+ onClick={onChange}
+ {...props}
+ >
+ <Text fontSize="sm" color="gray.700" fontWeight="medium">
+ {service.name}
+ </Text>
+ <Spacer />
+ <Flex flexDirection="row" alignItems="center">
+ <ServicePropertyStatus value={service.users.length} StatusIcon={UsersServiceIcon} />
+ <Text color="primary.600" mx="8px">/</Text>
+ <ServicePropertyStatus value={service.inbounds.length} StatusIcon={InboundsServiceIcon} />
+ </Flex>
+ </Box>
+ )
+);
+
+
+interface ServicesSelectListProps {
+ list: Service[];
+}
+
+export const ServicesSelectList = forwardRef<HTMLDivElement, ServicesSelectListProps>(
+ function({ list, ...props }, ref) {
+ const form = useFormContext();
+
+ const handleServiceToggle = (serviceId: number) => {
+ const currentServices = form.getValues('services');
+ const isSelected = currentServices.includes(serviceId);
+
+ let updatedServices;
+
+ if (isSelected) {
+ updatedServices = currentServices.filter((id: number) => id !== serviceId);
+ } else {
+ updatedServices = [...currentServices, serviceId];
+ }
+
+ form.setValue('services', updatedServices, { shouldValidate: true });
+ };
+
+ return (
+ <Stack spacing={3} ref={ref}>
+ {list.map((service: Service) => (
+ <Box key={service.id}>
+ <Flex align="center">
+ <CustomCheckbox
+ isChecked={form.getValues('services').includes(service.id)}
+ onChange={() => handleServiceToggle(service.id)}
+ service={service}
+ value={String(service.id)}
+ />
+ </Flex>
+ </Box>
+ ))}
+ </Stack>
+ );
+ });
diff --git a/app/dashboard/src/components/Statistics.tsx b/app/dashboard/src/components/Statistics.tsx
@@ -0,0 +1,195 @@
+import { Box, BoxProps, Card, chakra, HStack, Text } from "@chakra-ui/react";
+import {
+ ChartBarIcon,
+ ChartPieIcon,
+ UsersIcon,
+} from "@heroicons/react/24/outline";
+import { useDashboard } from "contexts/DashboardContext";
+import { FC, PropsWithChildren, ReactElement, ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+import { useQuery } from "react-query";
+import { fetch } from "service/http";
+import { formatBytes, numberWithCommas } from "utils/formatByte";
+
+const TotalUsersIcon = chakra(UsersIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ position: "relative",
+ zIndex: "2",
+ },
+});
+
+const NetworkIcon = chakra(ChartBarIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ position: "relative",
+ zIndex: "2",
+ },
+});
+
+const MemoryIcon = chakra(ChartPieIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ position: "relative",
+ zIndex: "2",
+ },
+});
+
+type StatisticCardProps = {
+ title: string;
+ content: ReactNode;
+ icon: ReactElement;
+};
+
+const StatisticCard: FC<PropsWithChildren<StatisticCardProps>> = ({
+ title,
+ content,
+ icon,
+}) => {
+ return (
+ <Card
+ p={6}
+ borderWidth="1px"
+ borderColor="light-border"
+ bg="#F9FAFB"
+ _dark={{ borderColor: "gray.600", bg: "gray.750" }}
+ borderStyle="solid"
+ boxShadow="none"
+ borderRadius="12px"
+ width="full"
+ display="flex"
+ justifyContent="space-between"
+ flexDirection="row"
+ >
+ <HStack alignItems="center" columnGap="4">
+ <Box
+ p="2"
+ position="relative"
+ color="white"
+ _before={{
+ content: `""`,
+ position: "absolute",
+ top: 0,
+ left: 0,
+ bg: "primary.400",
+ display: "block",
+ w: "full",
+ h: "full",
+ borderRadius: "5px",
+ opacity: ".5",
+ z: "1",
+ }}
+ _after={{
+ content: `""`,
+ position: "absolute",
+ top: "-5px",
+ left: "-5px",
+ bg: "primary.400",
+ display: "block",
+ w: "calc(100% + 10px)",
+ h: "calc(100% + 10px)",
+ borderRadius: "8px",
+ opacity: ".4",
+ z: "1",
+ }}
+ >
+ {icon}
+ </Box>
+ <Text
+ color="gray.600"
+ _dark={{
+ color: "gray.300",
+ }}
+ fontWeight="medium"
+ textTransform="capitalize"
+ fontSize="sm"
+ >
+ {title}
+ </Text>
+ </HStack>
+ <Box fontSize="3xl" fontWeight="semibold" mt="2">
+ {content}
+ </Box>
+ </Card>
+ );
+};
+export const StatisticsQueryKey = "statistics-query-key";
+export const Statistics: FC<BoxProps> = (props) => {
+ const { version } = useDashboard();
+ const { data: systemData } = useQuery({
+ queryKey: StatisticsQueryKey,
+ queryFn: () => fetch("/system"),
+ refetchInterval: 5000,
+ onSuccess: ({ version: currentVersion }) => {
+ if (version !== currentVersion)
+ useDashboard.setState({ version: currentVersion });
+ },
+ });
+ const { t } = useTranslation();
+ return (
+ <HStack
+ justifyContent="space-between"
+ gap={0}
+ columnGap={{ lg: 4, md: 0 }}
+ rowGap={{ lg: 0, base: 4 }}
+ display="flex"
+ flexDirection={{ lg: "row", base: "column" }}
+ {...props}
+ >
+ <StatisticCard
+ title={t("activeUsers")}
+ content={
+ systemData && (
+ <HStack alignItems="flex-end">
+ <Text>{numberWithCommas(systemData.users_active)}</Text>
+ <Text
+ fontWeight="normal"
+ fontSize="lg"
+ as="span"
+ display="inline-block"
+ pb="5px"
+ >
+ / {numberWithCommas(systemData.total_user)}
+ </Text>
+ </HStack>
+ )
+ }
+ icon={<TotalUsersIcon />}
+ />
+ <StatisticCard
+ title={t("dataUsage")}
+ content={
+ systemData &&
+ formatBytes(
+ systemData.incoming_bandwidth + systemData.outgoing_bandwidth
+ )
+ }
+ icon={<NetworkIcon />}
+ />
+ <StatisticCard
+ title={t("memoryUsage")}
+ content={
+ systemData && (
+ <HStack alignItems="flex-end">
+ <Text>{formatBytes(systemData.mem_used, 1, true)[0]}</Text>
+ <Text
+ fontWeight="normal"
+ fontSize="lg"
+ as="span"
+ display="inline-block"
+ pb="5px"
+ >
+ {formatBytes(systemData.mem_used, 1, true)[1]} /{" "}
+ {formatBytes(systemData.mem_total, 1)}
+ </Text>
+ </HStack>
+ )
+ }
+ icon={<MemoryIcon />}
+ />
+ </HStack>
+ );
+};
diff --git a/app/dashboard/src/components/StatusBadge.tsx b/app/dashboard/src/components/StatusBadge.tsx
@@ -0,0 +1,67 @@
+import { Badge, Text } from "@chakra-ui/react";
+
+import { statusColors } from "constants/UserSettings";
+import { FC } from "react";
+import { useTranslation } from "react-i18next";
+import { Status as UserStatusType } from "types/User";
+import { relativeExpiryDate } from "utils/dateFormatter";
+
+type UserStatusProps = {
+ expiryDate?: number | null;
+ status: UserStatusType;
+ compact?: boolean;
+ showDetail?: boolean;
+ extraText?: string | null;
+};
+export const StatusBadge: FC<UserStatusProps> = ({
+ expiryDate,
+ status: userStatus,
+ compact = false,
+ showDetail = true,
+ extraText,
+}) => {
+ const { t } = useTranslation();
+ const dateInfo = relativeExpiryDate(expiryDate);
+ const Icon = statusColors[userStatus].icon;
+ return (
+ <>
+ <Badge
+ colorScheme={statusColors[userStatus].statusColor}
+ rounded="full"
+ display="inline-flex"
+ px={3}
+ py={1}
+ columnGap={compact ? 1 : 2}
+ alignItems="center"
+ >
+ <Icon w={compact ? 3 : 4} />
+ {showDetail && (
+ <Text
+ textTransform="capitalize"
+ fontSize={compact ? ".7rem" : ".875rem"}
+ lineHeight={compact ? "1rem" : "1.25rem"}
+ fontWeight="medium"
+ letterSpacing="tighter"
+ >
+ {userStatus}
+ {extraText && `: ${extraText}`}
+ </Text>
+ )}
+ </Badge>
+ {showDetail && expiryDate && (
+ <Text
+ display="inline-block"
+ fontSize="xs"
+ fontWeight="medium"
+ ml="2"
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ >
+ {t(dateInfo.status, { time: dateInfo.time })}
+ </Text>
+ )}
+ </>
+ );
+};
diff --git a/app/dashboard/src/components/Textarea.tsx b/app/dashboard/src/components/Textarea.tsx
@@ -0,0 +1,142 @@
+import {
+ chakra,
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ InputGroup,
+ InputLeftAddon,
+ InputRightAddon,
+ InputRightElement,
+ Textarea as ChakraTextarea,
+ TextareaProps as ChakraTextareaProps,
+} from "@chakra-ui/react";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import classNames from "classnames";
+import React, { PropsWithChildren, ReactNode } from "react";
+
+const ClearIcon = chakra(XMarkIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+});
+
+export type TextareaProps = PropsWithChildren<
+ {
+ value?: string;
+ className?: string;
+ endAdornment?: ReactNode;
+ startAdornment?: ReactNode;
+ type?: string;
+ placeholder?: string;
+ onChange?: (e: any) => void;
+ onBlur?: (e: any) => void;
+ onClick?: (e: any) => void;
+ name?: string;
+ error?: string;
+ disabled?: boolean;
+ step?: number;
+ label?: string;
+ clearable?: boolean;
+ } & ChakraTextareaProps
+>;
+
+export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+ (
+ {
+ disabled,
+ step,
+ label,
+ className,
+ startAdornment,
+ endAdornment,
+ placeholder,
+ onChange,
+ onBlur,
+ name,
+ value,
+ onClick,
+ error,
+ clearable = false,
+ ...props
+ },
+ ref
+ ) => {
+ const clear = () => {
+ if (onChange)
+ onChange({
+ target: {
+ value: "",
+ name,
+ },
+ });
+ };
+ const { size = "md" } = props;
+
+ return (
+ <FormControl isInvalid={!!error}>
+ {label && <FormLabel>{label}</FormLabel>}
+ <InputGroup
+ size={size}
+ w="full"
+ rounded="md"
+ _focusWithin={{
+ outline: "2px solid",
+ outlineColor: "primary.200",
+ }}
+ bg={disabled ? "gray.100" : "transparent"}
+ _dark={{ bg: disabled ? "gray.600" : "transparent" }}
+ >
+ {startAdornment && <InputLeftAddon>{startAdornment}</InputLeftAddon>}
+ {/* @ts-ignore */}
+ <ChakraTextarea
+ name={name}
+ ref={ref}
+ step={step}
+ className={classNames(className)}
+ placeholder={placeholder}
+ onChange={onChange}
+ onBlur={onBlur}
+ value={value}
+ onClick={onClick}
+ disabled={disabled}
+ flexGrow={1}
+ _focusVisible={{
+ outline: "none",
+ borderTopColor: "transparent",
+ borderRightColor: "transparent",
+ borderBottomColor: "transparent",
+ }}
+ _disabled={{
+ cursor: "not-allowed",
+ }}
+ {...props}
+ roundedLeft={startAdornment ? "0" : "md"}
+ roundedRight={endAdornment ? "0" : "md"}
+ />
+ {endAdornment && (
+ <InputRightAddon
+ borderLeftRadius={0}
+ borderRightRadius="6px"
+ bg="transparent"
+ >
+ {endAdornment}
+ </InputRightAddon>
+ )}
+ {clearable && value && value.length && (
+ <InputRightElement
+ borderLeftRadius={0}
+ borderRightRadius="6px"
+ bg="transparent"
+ onClick={clear}
+ cursor="pointer"
+ >
+ <ClearIcon />
+ </InputRightElement>
+ )}
+ </InputGroup>
+ {!!error && <FormErrorMessage>{error}</FormErrorMessage>}
+ </FormControl>
+ );
+ }
+);
diff --git a/app/dashboard/src/components/UsageFilter.tsx b/app/dashboard/src/components/UsageFilter.tsx
@@ -0,0 +1,382 @@
+import {
+ Box,
+ ColorMode,
+ HStack,
+ Icon,
+ SimpleGrid,
+ Tab,
+ TabList,
+ TabPanel,
+ TabPanels,
+ Tabs,
+ Text,
+ UseRadioProps,
+ VStack,
+ useBreakpointValue,
+ useColorMode,
+ useDisclosure,
+ useOutsideClick,
+ useRadio,
+ useRadioGroup,
+} from "@chakra-ui/react";
+import { CalendarIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
+import { ApexOptions } from "apexcharts";
+import { FilterUsageType } from "contexts/DashboardContext";
+import dayjs, { ManipulateType } from "dayjs";
+import { FC, useRef, useState } from "react";
+import ReactDatePicker from "react-datepicker";
+import { useTranslation } from "react-i18next";
+import { generateDistinctColors } from "utils/color";
+import { formatBytes } from "utils/formatByte";
+
+type DateType = Date | null;
+
+const FilterItem: FC<UseRadioProps & { border?: boolean } & any> = ({
+ border,
+ ...props
+}) => {
+ const { getInputProps, getRadioProps } = useRadio(props);
+ const fontSize = useBreakpointValue({ base: "xs", md: "sm" });
+ return (
+ <Box as="label">
+ <input {...getInputProps()} />
+ <Box
+ {...getRadioProps()}
+ minW="48px"
+ w="full"
+ h="full"
+ textAlign="center"
+ cursor="pointer"
+ fontSize={fontSize}
+ borderWidth={border ? "1px" : "0px"}
+ borderRadius="md"
+ _checked={{
+ bg: "primary.500",
+ color: "white",
+ borderColor: "primary.500",
+ }}
+ _focus={{
+ boxShadow: "outline",
+ }}
+ px={3}
+ py={1}
+ >
+ {props.children}
+ </Box>
+ </Box>
+ );
+};
+
+export type UsageFilterProps = {
+ onChange: (filter: string, query: FilterUsageType) => void;
+ defaultValue: string;
+};
+
+export const UsageFilter: FC<UsageFilterProps> = ({
+ onChange,
+ defaultValue,
+ ...props
+}) => {
+ const { t, i18n } = useTranslation();
+ const { colorMode } = useColorMode();
+
+ const filterOptions = useBreakpointValue({
+ base: ["7h", "1d", "3d", "1w"],
+ md: ["7h", "1d", "3d", "1w", "1m", "3m"],
+ })!;
+ const filterOptionTypes = {
+ h: "hour",
+ d: "day",
+ w: "week",
+ m: "month",
+ y: "year",
+ };
+ const customFilterOptions = useBreakpointValue({
+ base: [
+ { title: "hours", options: ["1h", "3h", "6h", "12h"] },
+ { title: "days", options: ["1d", "2d", "3d", "4d"] },
+ { title: "weeks", options: ["1w", "2w", "3w", "4w"] },
+ { title: "months", options: ["1m", "2m", "3m", "6m"] },
+ ],
+ md: [
+ { title: "hours", options: ["1h", "2h", "3h", "6h", "8h", "12h"] },
+ { title: "days", options: ["1d", "2d", "3d", "4d", "5d", "6d"] },
+ { title: "weeks", options: ["1w", "2w", "3w", "4w"] },
+ { title: "months", options: ["1m", "2m", "3m", "6m", "8m"] },
+ ],
+ })!;
+ const {
+ getRootProps,
+ getRadioProps,
+ setValue: setDefaultFilter,
+ } = useRadioGroup({
+ name: "filter",
+ defaultValue: defaultValue,
+ onChange: (value: string) => {
+ if (value === "custom") {
+ return;
+ }
+
+ closeCustom();
+
+ if (filterOptions.indexOf(value) >= 0) {
+ setCustomLabel(t("userDialog.custom"));
+ setCustom(false);
+ } else {
+ setCustomLabel(t("userDialog.custom") + ` (${value})`);
+ setCustom(true);
+ }
+
+ const num = Number(value.substring(0, value.length - 1));
+ const unit =
+ filterOptionTypes[
+ value[value.length - 1] as keyof typeof filterOptionTypes
+ ];
+ onChange(value, {
+ start: dayjs()
+ .utc()
+ .subtract(num, unit as ManipulateType)
+ .format("YYYY-MM-DDTHH:00:00"),
+ });
+ },
+ });
+
+ const {
+ isOpen: isCustomOpen,
+ onOpen: openCustom,
+ onClose: closeCustom,
+ } = useDisclosure();
+ const customRef = useRef(null);
+ useOutsideClick({ ref: customRef, handler: closeCustom });
+
+ const [customLabel, setCustomLabel] = useState(t("userDialog.custom"));
+ const [custom, setCustom] = useState(false);
+ const [tabIndex, setTabIndex] = useState(0);
+
+ const monthsShown = useBreakpointValue({ base: 1, md: 2 });
+ const fontSize = useBreakpointValue({ base: "xs", md: "sm" });
+ const [startDate, setStartDate] = useState(null as DateType);
+ const [endDate, setEndDate] = useState(null as DateType);
+ const onDateChange = (dates: [DateType, DateType]) => {
+ const [start, end] = dates;
+ if (endDate && !end) {
+ setStartDate(null);
+ setEndDate(null);
+ } else {
+ setStartDate(start);
+ setEndDate(end);
+ if (start && end) {
+ closeCustom();
+ onChange("custom", {
+ start: dayjs(start).format("YYYY-MM-DDT00:00:00"),
+ end: dayjs(end).format("YYYY-MM-DDT23:59:59"),
+ });
+ }
+ }
+ };
+
+ return (
+ <VStack {...props}>
+ {tabIndex == 0 && (
+ <SimpleGrid
+ {...getRootProps()}
+ gap={0}
+ display="flex"
+ borderWidth="1px"
+ borderRadius="md"
+ minW={{ base: "320px", md: "400px" }}
+ >
+ {filterOptions.map((value) => {
+ return (
+ <FilterItem key={value} {...getRadioProps({ value })}>
+ {value}
+ </FilterItem>
+ );
+ })}
+ <Box
+ onClick={() => {
+ setStartDate(null);
+ setEndDate(null);
+ openCustom();
+ }}
+ cursor="pointer"
+ borderRadius="md"
+ w="full"
+ fontSize={fontSize}
+ px={3}
+ py={1}
+ bg={custom ? "primary.500" : "unset"}
+ color={custom ? "white" : "unset"}
+ borderColor={custom ? "primary.500" : "unset"}
+ >
+ <HStack>
+ <Text>{customLabel}</Text>
+ <Icon as={CalendarIcon} boxSize="18px" />
+ </HStack>
+ </Box>
+ </SimpleGrid>
+ )}
+ {tabIndex == 1 && (
+ <HStack
+ onClick={openCustom}
+ cursor="pointer"
+ fontSize={fontSize}
+ borderRadius="md"
+ px={3}
+ py={1}
+ minW={{ base: "320px", md: "400px" }}
+ borderWidth="1px"
+ >
+ <Text w="full" color={startDate ? "unset" : "gray.500"}>
+ {startDate
+ ? dayjs(startDate).format("YYYY-MM-DD (00:00)")
+ : t("userDialog.startDate")}
+ </Text>
+ <Icon as={ChevronRightIcon} boxSize="18px" />
+ <Text w="full" color={endDate ? "unset" : "gray.500"}>
+ {endDate
+ ? dayjs(endDate).format("YYYY-MM-DD (23:59)")
+ : t("userDialog.endDate")}
+ </Text>
+ <Icon as={CalendarIcon} boxSize="18px" />
+ </HStack>
+ )}
+ <VStack
+ ref={customRef}
+ marginTop="40px !important"
+ borderRadius="md"
+ borderWidth="1px"
+ position="absolute"
+ zIndex="1"
+ backgroundColor="white"
+ _dark={{
+ backgroundColor: "gray.700",
+ }}
+ display={isCustomOpen ? "unset" : "none"}
+ >
+ <Tabs onChange={(index) => setTabIndex(index)}>
+ <TabList>
+ <Tab fontSize={fontSize}>{t("userDialog.relative")}</Tab>
+ <Tab fontSize={fontSize}>{t("userDialog.absolute")}</Tab>
+ </TabList>
+
+ <TabPanels>
+ <TabPanel>
+ {customFilterOptions.map((row) => {
+ return (
+ <VStack key={row.title} alignItems="start" pl={2} pr={2}>
+ <HStack justifyItems="flex-start" mb={4}>
+ <Text fontSize={fontSize} minW="60px">
+ {t("userDialog." + row.title)}
+ </Text>
+ {row.options.map((value: string) => {
+ return (
+ <FilterItem
+ key={value + ".custom"}
+ border={true}
+ {...getRadioProps({ value })}
+ >
+ {value}
+ </FilterItem>
+ );
+ })}
+ </HStack>
+ </VStack>
+ );
+ })}
+ </TabPanel>
+ <TabPanel className="datepicker-panel">
+ <VStack>
+ <ReactDatePicker
+ locale={i18n.language.toLocaleLowerCase()}
+ selected={startDate}
+ onChange={onDateChange}
+ startDate={startDate}
+ endDate={endDate}
+ selectsRange={true}
+ maxDate={new Date()}
+ monthsShown={monthsShown}
+ peekNextMonth={false}
+ inline
+ />
+ </VStack>
+ </TabPanel>
+ </TabPanels>
+ </Tabs>
+ </VStack>
+ </VStack>
+ );
+};
+
+export function createUsageConfig(
+ colorMode: ColorMode,
+ title: string,
+ series: any = [],
+ labels: any = []
+) {
+ const total = formatBytes((series as [number]).reduce((t, c) => (t += c), 0));
+ return {
+ series: series,
+ options: {
+ labels: labels,
+ chart: {
+ width: "100%",
+ height: "100%",
+ type: "donut",
+ animations: {
+ enabled: false,
+ },
+ },
+ title: {
+ text: `${title}${total}`,
+ align: "center",
+ style: {
+ fontWeight: "var(--chakra-fontWeights-medium)",
+ color:
+ colorMode === "dark" ? "var(--chakra-colors-gray-300)" : undefined,
+ },
+ },
+ legend: {
+ position: "bottom",
+ labels: {
+ colors: colorMode === "dark" ? "#CBD5E0" : undefined,
+ useSeriesColors: false,
+ },
+ },
+ stroke: {
+ width: 1,
+ colors: undefined,
+ },
+ dataLabels: {
+ formatter: (val, { seriesIndex, w }) => {
+ return formatBytes(w.config.series[seriesIndex], 1);
+ },
+ },
+ tooltip: {
+ custom: ({ series, seriesIndex, dataPointIndex, w }) => {
+ const readable = formatBytes(series[seriesIndex], 1);
+ const total = Math.max(
+ (series as [number]).reduce((t, c) => (t += c)),
+ 1
+ );
+ const percent =
+ Math.round((series[seriesIndex] / total) * 1000) / 10 + "%";
+ return `
+ <div style="
+ background-color: ${w.globals.colors[seriesIndex]};
+ padding-left:12px;
+ padding-right:12px;
+ padding-top:6px;
+ padding-bottom:6px;
+ font-size:0.725rem;
+ "
+ >
+ ${w.config.labels[seriesIndex]}: <b>${percent}, ${readable}</b>
+ </div>
+ `;
+ },
+ },
+ colors: generateDistinctColors(series.length),
+ } as ApexOptions,
+ };
+}
diff --git a/app/dashboard/src/components/UserDialog/DefaultValues.ts b/app/dashboard/src/components/UserDialog/DefaultValues.ts
@@ -0,0 +1,15 @@
+
+import { FormType } from "./FormSchema";
+
+export const getDefaultValues = (): FormType => {
+
+ return {
+ services: [],
+ data_limit: 0,
+ expire: null,
+ username: "",
+ data_limit_reset_strategy: "no_reset",
+ status: "active",
+ note: "",
+ };
+};
diff --git a/app/dashboard/src/components/UserDialog/FormSchema.ts b/app/dashboard/src/components/UserDialog/FormSchema.ts
@@ -0,0 +1,22 @@
+
+import { z } from "zod";
+import { UserCreate } from "types/User";
+
+export type FormType = Pick<UserCreate, keyof UserCreate>;
+export const schema = z.object({
+ username: z.string().min(1, { message: "Username is required" }),
+ note: z.string().nullable(),
+ data_limit: z
+ .string()
+ .min(0, "The minimum number is 0")
+ .or(z.number())
+ .nullable()
+ .transform((str) => {
+ if (str) return Number((parseFloat(String(str)) * 1073741824).toFixed(5));
+ return 0;
+ }),
+ expire: z.number().nullable(),
+ data_limit_reset_strategy: z.string(),
+ status: z.string(),
+ services: z.array(z.number()).nonempty({ message: "At least one service is required" }),
+});
diff --git a/app/dashboard/src/components/UserDialog/ModalFooter.tsx b/app/dashboard/src/components/UserDialog/ModalFooter.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import {
+ Button,
+ Tooltip,
+ HStack,
+ IconButton,
+ Spinner,
+} from "@chakra-ui/react";
+import { DeleteIcon } from "../DeleteUserModal";
+import { UserUsageIcon } from "./UserDialogIcons";
+import { useTranslation } from 'react-i18next';
+import { User } from 'types/User';
+import { ModalFooter } from '@chakra-ui/react';
+
+interface UserDialogFooterProps {
+ isEditing: boolean;
+ onDeletingUser: (user: User) => void;
+ handleUsageToggle: () => void;
+ handleResetUsage: () => void;
+ handleRevokeSubscription: () => void;
+ loading: boolean;
+ editingUser: User | null;
+ onClose: () => void;
+}
+
+export const UserDialogModalFooter: React.FC<UserDialogFooterProps> = ({
+ isEditing,
+ onClose, editingUser,
+ onDeletingUser,
+ handleUsageToggle,
+ handleResetUsage,
+ handleRevokeSubscription,
+ loading,
+}) => {
+
+ const { t, i18n } = useTranslation();
+ return (
+
+ <ModalFooter mt="3">
+ <HStack
+ justifyContent="space-between"
+ w="full"
+ gap={3}
+ flexDirection={{
+ base: "column",
+ sm: "row",
+ }}
+ >
+ <HStack
+ justifyContent="flex-start"
+ w={{
+ base: "full",
+ sm: "unset",
+ }}
+ >
+ {isEditing && editingUser !== null && (
+ <>
+ <Tooltip label={t("delete")} placement="top">
+ <IconButton
+ aria-label="Delete"
+ size="sm"
+ onClick={() => {
+ onDeletingUser(editingUser);
+ onClose();
+ }}
+ >
+ <DeleteIcon />
+ </IconButton>
+ </Tooltip>
+ <Tooltip label={t("userDialog.usage")} placement="top">
+ <IconButton
+ aria-label="usage"
+ size="sm"
+ onClick={handleUsageToggle}
+ >
+ <UserUsageIcon />
+ </IconButton>
+ </Tooltip>
+ <Button onClick={handleResetUsage} size="sm">
+ {t("userDialog.resetUsage")}
+ </Button>
+ <Button onClick={handleRevokeSubscription} size="sm">
+ {t("userDialog.revokeSubscription")}
+ </Button>
+ </>
+ )}
+ </HStack>
+ <HStack
+ w="full"
+ maxW={{ md: "50%", base: "full" }}
+ justify="end"
+ >
+ <Button
+ type="submit"
+ size="sm"
+ px="8"
+ colorScheme="primary"
+ leftIcon={loading ? <Spinner size="xs" /> : undefined}
+ disabled={false}
+ >
+ {isEditing ? t("userDialog.editUser") : t("createUser")}
+ </Button>
+ </HStack>
+ </HStack>
+ </ModalFooter >
+ );
+};
diff --git a/app/dashboard/src/components/UserDialog/ModalHeader.tsx b/app/dashboard/src/components/UserDialog/ModalHeader.tsx
@@ -0,0 +1,18 @@
+
+import { ChakraComponent, Text, HStack, ModalHeader } from "@chakra-ui/react";
+import { Icon } from "../Icon";
+
+export const UserDialogModalHeader = ({ HeaderIcon, title }: { HeaderIcon: ChakraComponent, title: string }) => {
+ return (
+ <ModalHeader pt={6}>
+ <HStack gap={2}>
+ <Icon color="primary">
+ {HeaderIcon && <HeaderIcon color="white" />}
+ </Icon>
+ <Text fontWeight="semibold" fontSize="lg">
+ {title}
+ </Text>
+ </HStack>
+ </ModalHeader>
+ );
+}
diff --git a/app/dashboard/src/components/UserDialog/UserDialogIcons.ts b/app/dashboard/src/components/UserDialog/UserDialogIcons.ts
@@ -0,0 +1,28 @@
+
+import { chakra } from "@chakra-ui/react";
+import {
+ ChartPieIcon,
+ PencilIcon,
+ UserPlusIcon,
+} from "@heroicons/react/24/outline";
+
+export const AddUserIcon = chakra(UserPlusIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export const EditUserIcon = chakra(PencilIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export const UserUsageIcon = chakra(ChartPieIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
diff --git a/app/dashboard/src/components/UserDialog/index.tsx b/app/dashboard/src/components/UserDialog/index.tsx
@@ -0,0 +1,513 @@
+import {
+ Alert,
+ AlertIcon,
+ Box,
+ Collapse,
+ Flex,
+ FormControl,
+ FormErrorMessage,
+ FormHelperText,
+ FormLabel,
+ Grid,
+ GridItem,
+ HStack,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ Modal,
+ Select,
+ Switch,
+ Textarea,
+ Tooltip,
+ VStack,
+ useColorMode,
+ useToast,
+ Spinner,
+ ModalOverlay,
+} from "@chakra-ui/react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { resetStrategy } from "constants/UserSettings";
+import { FilterUsageType, useDashboard } from "contexts/DashboardContext";
+import dayjs from "dayjs";
+import { FC, useEffect, useState } from "react";
+import ReactApexChart from "react-apexcharts";
+import ReactDatePicker from "react-datepicker";
+import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import {
+ User,
+ UserCreate,
+} from "types/User";
+import { relativeExpiryDate } from "utils/dateFormatter";
+import { Input } from "../Input";
+import { ServicesSelectList } from "../ServicesSelectList";
+import { UsageFilter, createUsageConfig } from "../UsageFilter";
+import { AddUserIcon, EditUserIcon } from "./UserDialogIcons";
+import { DevTool } from "@hookform/devtools";
+import { UserDialogModalHeader } from "./ModalHeader";
+import { UserDialogModalFooter } from "./ModalFooter";
+import { getDefaultValues } from "./DefaultValues";
+import { schema, FormType } from "./FormSchema";
+
+
+const formatUser = (user: User): FormType => {
+ return {
+ ...user,
+ data_limit: user.data_limit
+ ? Number((user.data_limit / 1073741824).toFixed(5))
+ : user.data_limit,
+ };
+};
+
+
+export type UserDialogProps = {};
+
+export const UserDialog: FC<UserDialogProps> = () => {
+ const {
+ editingUser,
+ isCreatingNewUser,
+ services,
+ onCreateUser,
+ editUser,
+ fetchUserUsage,
+ refetchServices,
+ onEditingUser,
+ createUser,
+ onDeletingUser,
+ } = useDashboard();
+ const isEditing = !!editingUser;
+ const isOpen = isCreatingNewUser || isEditing;
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>("");
+ const toast = useToast();
+ const { t, i18n } = useTranslation();
+
+ const { colorMode } = useColorMode();
+
+ const [usageVisible, setUsageVisible] = useState(false);
+ const handleUsageToggle = () => {
+ setUsageVisible((current) => !current);
+ };
+
+ const form = useForm<FormType>({
+ defaultValues: getDefaultValues(),
+ resolver: zodResolver(schema),
+ });
+
+ const [dataLimit] = useWatch({
+ control: form.control,
+ name: ["data_limit"],
+ });
+
+ const usageTitle = t("userDialog.total");
+ const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
+ const [usageFilter, setUsageFilter] = useState("1m");
+ const fetchUsageWithFilter = (query: FilterUsageType) => {
+ fetchUserUsage(editingUser!, query).then((data: any) => {
+ const labels = [];
+ const series = [];
+ for (const key in data.usages) {
+ series.push(data.usages[key].used_traffic);
+ labels.push(data.usages[key].node_name);
+ }
+ setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
+ });
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ refetchServices();
+ }
+ }, [isOpen]);
+
+
+ useEffect(() => {
+ if (editingUser) {
+ form.reset(formatUser(editingUser));
+
+ fetchUsageWithFilter({
+ start: dayjs().utc().subtract(30, "day").format("YYYY-MM-DDTHH:00:00"),
+ });
+ }
+ }, [editingUser]);
+
+ const submit = async (values: FormType) => {
+ setLoading(true);
+ const methods = { edited: editUser, created: createUser };
+ const method = isEditing ? "edited" : "created";
+ setError(null);
+ console.log('Form errors:', form.formState.errors);
+
+ console.log("For Value", values);
+ const { services, username, ...rest } = values;
+
+ let body: UserCreate = {
+ ...rest,
+ username,
+ services,
+ data_limit: values.data_limit,
+ data_limit_reset_strategy:
+ values.data_limit && values.data_limit > 0
+ ? values.data_limit_reset_strategy
+ : "no_reset",
+ status:
+ values.status === "active" || values.status === "disabled"
+ ? values.status
+ : "active",
+ };
+
+ await methods[method](body)
+ .then(() => {
+ toast({
+ title: t(
+ isEditing ? "userDialog.userEdited" : "userDialog.userCreated",
+ { username: values.username }
+ ),
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ onClose();
+ })
+ .catch((err) => {
+ if (err?.response?.status === 409 || err?.response?.status === 400)
+ setError(err?.response?._data?.detail);
+ if (err?.response?.status === 422) {
+ Object.keys(err.response._data.detail).forEach((key) => {
+ setError(err?.response._data.detail[key] as string);
+ form.setError(
+ key as "services" | "username" | "data_limit" | "expire",
+ {
+ type: "custom",
+ message: err.response._data.detail[key],
+ }
+ );
+ });
+ }
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ const onClose = () => {
+ form.reset(getDefaultValues());
+ onCreateUser(false);
+ onEditingUser(null);
+ setError(null);
+ setUsageVisible(false);
+ setUsageFilter("1m");
+ };
+
+ const handleResetUsage = () => {
+ useDashboard.setState({ resetUsageUser: editingUser });
+ };
+
+ const handleRevokeSubscription = () => {
+ useDashboard.setState({ revokeSubscriptionUser: editingUser });
+ };
+
+ const disabled = loading;
+
+ return (
+ <Modal isOpen={isOpen} onClose={onClose} size="2xl">
+ <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
+ <FormProvider {...form} formState={form.formState}>
+ <ModalContent mx="3">
+ <form onSubmit={form.handleSubmit(submit)}>
+ <UserDialogModalHeader HeaderIcon={isEditing ? EditUserIcon : AddUserIcon} title={isEditing ? t("userDialog.editUserTitle") : t("createNewUser")} />
+ <ModalCloseButton mt={3} disabled={disabled} />
+ <ModalBody>
+ <Grid
+ templateColumns={{
+ base: "repeat(1, 1fr)",
+ md: "repeat(2, 1fr)",
+ }}
+ gap={3}
+ >
+ <GridItem>
+ <VStack justifyContent="space-between">
+ <Flex
+ flexDirection="column"
+ gridAutoRows="min-content"
+ w="full"
+ >
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("username")}</FormLabel>
+ <HStack>
+ <Input
+ size="sm"
+ type="text"
+ borderRadius="6px"
+ error={form.formState.errors.username?.message}
+ disabled={disabled || isEditing}
+ {...form.register("username")}
+ />
+ {isEditing && (
+ <HStack px={1}>
+ <Controller
+ name="status"
+ control={form.control}
+ render={({ field }) => {
+ return (
+ <Tooltip
+ placement="top"
+ label={"status: " + field.value}
+ textTransform="capitalize"
+ >
+ <Box>
+ <Switch
+ colorScheme="primary"
+ disabled={
+ field.value !== "active" &&
+ field.value !== "disabled"
+ }
+ isChecked={field.value === "active"}
+ onChange={(e) => {
+ if (e.target.checked) {
+ field.onChange("active");
+ } else {
+ field.onChange("disabled");
+ }
+ }}
+ />
+ </Box>
+ </Tooltip>
+ );
+ }}
+ />
+ </HStack>
+ )}
+ </HStack>
+ </FormControl>
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("userDialog.dataLimit")}</FormLabel>
+ <Controller
+ control={form.control}
+ name="data_limit"
+ render={({ field }) => {
+ return (
+ <Input
+ endAdornment="GB"
+ type="number"
+ size="sm"
+ borderRadius="6px"
+ onChange={field.onChange}
+ disabled={disabled}
+ error={
+ form.formState.errors.data_limit?.message
+ }
+ value={field.value ? String(field.value) : ""}
+ />
+ );
+ }}
+ />
+ </FormControl>
+ <Collapse
+ in={!!(dataLimit && dataLimit > 0)}
+ animateOpacity
+ style={{ width: "100%" }}
+ >
+ <FormControl height="66px">
+ <FormLabel>
+ {t("userDialog.periodicUsageReset")}
+ </FormLabel>
+ <Controller
+ control={form.control}
+ name="data_limit_reset_strategy"
+ render={({ field }) => {
+ return (
+ <Select size="sm" {...field}>
+ {resetStrategy.map((s) => {
+ return (
+ <option key={s.value} value={s.value}>
+ {t(
+ "userDialog.resetStrategy" + s.title
+ )}
+ </option>
+ );
+ })}
+ </Select>
+ );
+ }}
+ />
+ </FormControl>
+ </Collapse>
+ <FormControl mb={"10px"}>
+ <FormLabel>{t("userDialog.expiryDate")}</FormLabel>
+ <Controller
+ name="expire"
+ control={form.control}
+ render={({ field }) => {
+ function createDateAsUTC(num: number) {
+ return dayjs(
+ dayjs(num * 1000).utc()
+ // .format("MMMM D, YYYY") // exception with: dayjs.locale(lng);
+ ).toDate();
+ }
+ const { status, time } = relativeExpiryDate(
+ field.value
+ );
+ return (
+ <>
+ <ReactDatePicker
+ locale={i18n.language.toLocaleLowerCase()}
+ dateFormat={t("dateFormat")}
+ minDate={new Date()}
+ selected={
+ field.value
+ ? createDateAsUTC(field.value)
+ : undefined
+ }
+ onChange={(date: Date) => {
+ field.onChange({
+ target: {
+ value: date
+ ? dayjs(
+ dayjs(date)
+ .set("hour", 23)
+ .set("minute", 59)
+ .set("second", 59)
+ )
+ .utc()
+ .valueOf() / 1000
+ : 0,
+ name: "expire",
+ },
+ });
+ }}
+ customInput={
+ <Input
+ size="sm"
+ type="text"
+ borderRadius="6px"
+ clearable
+ disabled={disabled}
+ error={
+ form.formState.errors.expire?.message
+ }
+ />
+ }
+ />
+ {field.value ? (
+ <FormHelperText>
+ {t(status, { time: time })}
+ </FormHelperText>
+ ) : (
+ ""
+ )}
+ </>
+ );
+ }}
+ />
+ </FormControl>
+
+ <FormControl
+ mb={"10px"}
+ isInvalid={!!form.formState.errors.note}
+ >
+ <FormLabel>{t("userDialog.note")}</FormLabel>
+ <Textarea {...form.register("note")} />
+ <FormErrorMessage>
+ {form.formState.errors?.note?.message}
+ </FormErrorMessage>
+ </FormControl>
+ </Flex>
+ {error && (
+ <Alert
+ status="error"
+ display={{ base: "none", md: "flex" }}
+ >
+ <AlertIcon />
+ {error}
+ </Alert>
+ )}
+ </VStack>
+ </GridItem>
+
+ <GridItem>
+ {services.length === 0 ? (
+ <Spinner />
+ ) :
+ <FormControl
+ isInvalid={
+ !!form.formState.errors.services?.message
+ }
+ >
+ <FormLabel>{t("userDialog.services")}</FormLabel>
+ <Controller
+ control={form.control}
+ name="services"
+ render={({ field }) => {
+ return (
+ <ServicesSelectList
+ list={services}
+ disabled={disabled}
+ {...field}
+ />
+ );
+ }}
+ />
+ <FormErrorMessage>
+ {t(
+ form.formState.errors.services
+ ?.message as string
+ )}
+ </FormErrorMessage>
+ </FormControl>
+ }
+ </GridItem>
+
+ {isEditing && usageVisible && (
+ <GridItem pt={6} colSpan={{ base: 1, md: 2 }}>
+ <VStack gap={4}>
+ <UsageFilter
+ defaultValue={usageFilter}
+ onChange={(filter, query) => {
+ setUsageFilter(filter);
+ fetchUsageWithFilter(query);
+ }}
+ />
+ <Box
+ width={{ base: "100%", md: "70%" }}
+ justifySelf="center"
+ >
+ <ReactApexChart
+ options={usage.options}
+ series={usage.series}
+ type="donut"
+ />
+ </Box>
+ </VStack>
+ </GridItem>
+ )}
+ </Grid>
+
+ {error && (
+ <Alert
+ mt="3"
+ status="error"
+ display={{ base: "flex", md: "none" }}
+ >
+ <AlertIcon />
+ {error}
+ </Alert>
+ )}
+ </ModalBody>
+ <UserDialogModalFooter
+ editingUser={isEditing ? editingUser : null}
+ onClose={onClose}
+ isEditing={isEditing}
+ onDeletingUser={onDeletingUser}
+ handleUsageToggle={handleUsageToggle}
+ handleResetUsage={handleResetUsage}
+ handleRevokeSubscription={handleRevokeSubscription}
+ loading={loading}
+ />
+ </form>
+ </ModalContent>
+ </FormProvider>
+ </Modal >
+ );
+};
diff --git a/app/dashboard/src/components/UsersTable.tsx b/app/dashboard/src/components/UsersTable.tsx
@@ -0,0 +1,802 @@
+import {
+ Accordion,
+ AccordionButton,
+ AccordionItem,
+ AccordionPanel,
+ Box,
+ Button,
+ chakra,
+ ExpandedIndex,
+ HStack,
+ IconButton,
+ Select,
+ Slider,
+ SliderFilledTrack,
+ SliderProps,
+ SliderTrack,
+ Table,
+ TableProps,
+ Tbody,
+ Td,
+ Text,
+ Th,
+ Thead,
+ Tooltip,
+ Tr,
+ useBreakpointValue,
+ VStack,
+} from "@chakra-ui/react";
+import {
+ CheckIcon,
+ ChevronDownIcon,
+ ClipboardIcon,
+ LinkIcon,
+ PencilIcon,
+ QrCodeIcon,
+} from "@heroicons/react/24/outline";
+import { ReactComponent as AddFileIcon } from "assets/add_file.svg";
+import classNames from "classnames";
+import { resetStrategy, statusColors } from "constants/UserSettings";
+import { useDashboard } from "contexts/DashboardContext";
+import { t } from "i18next";
+import { FC, Fragment, useEffect, useState } from "react";
+import CopyToClipboard from "react-copy-to-clipboard";
+import { useTranslation } from "react-i18next";
+import { User } from "types/User";
+import { formatBytes } from "utils/formatByte";
+import { OnlineBadge } from "./OnlineBadge";
+import { OnlineStatus } from "./OnlineStatus";
+import { Pagination } from "./Pagination";
+import { StatusBadge } from "./StatusBadge";
+
+const EmptySectionIcon = chakra(AddFileIcon);
+
+const iconProps = {
+ baseStyle: {
+ w: {
+ base: 4,
+ md: 5,
+ },
+ h: {
+ base: 4,
+ md: 5,
+ },
+ },
+};
+const CopyIcon = chakra(ClipboardIcon, iconProps);
+const AccordionArrowIcon = chakra(ChevronDownIcon, iconProps);
+const CopiedIcon = chakra(CheckIcon, iconProps);
+const SubscriptionLinkIcon = chakra(LinkIcon, iconProps);
+const QRIcon = chakra(QrCodeIcon, iconProps);
+const EditIcon = chakra(PencilIcon, iconProps);
+const SortIcon = chakra(ChevronDownIcon, {
+ baseStyle: {
+ width: "15px",
+ height: "15px",
+ },
+});
+type UsageSliderProps = {
+ used: number;
+ total: number | null;
+ dataLimitResetStrategy: string | null;
+ totalUsedTraffic: number;
+} & SliderProps;
+
+const getResetStrategy = (strategy: string): string => {
+ for (var i = 0; i < resetStrategy.length; i++) {
+ const entry = resetStrategy[i];
+ if (entry.value == strategy) {
+ return entry.title;
+ }
+ }
+ return "No";
+};
+const UsageSliderCompact: FC<UsageSliderProps> = (props) => {
+ const { used, total, dataLimitResetStrategy, totalUsedTraffic } = props;
+ const isUnlimited = total === 0 || total === null;
+ return (
+ <HStack
+ justifyContent="space-between"
+ fontSize="xs"
+ fontWeight="medium"
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ >
+ <Text>
+ {formatBytes(used)} /{" "}
+ {isUnlimited ? (
+ <Text as="span" fontFamily="system-ui">
+ ∞
+ </Text>
+ ) : (
+ formatBytes(total)
+ )}
+ </Text>
+ </HStack>
+ );
+};
+const UsageSlider: FC<UsageSliderProps> = (props) => {
+ const {
+ used,
+ total,
+ dataLimitResetStrategy,
+ totalUsedTraffic,
+ ...restOfProps
+ } = props;
+ const isUnlimited = total === 0 || total === null;
+ const isReached = !isUnlimited && (used / total) * 100 >= 100;
+ return (
+ <>
+ <Slider
+ orientation="horizontal"
+ value={isUnlimited ? 100 : Math.min((used / total) * 100, 100)}
+ colorScheme={isReached ? "red" : "primary"}
+ {...restOfProps}
+ >
+ <SliderTrack h="6px" borderRadius="full">
+ <SliderFilledTrack borderRadius="full" />
+ </SliderTrack>
+ </Slider>
+ <HStack
+ justifyContent="space-between"
+ fontSize="xs"
+ fontWeight="medium"
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ >
+ <Text>
+ {formatBytes(used)} /{" "}
+ {isUnlimited ? (
+ <Text as="span" fontFamily="system-ui">
+ ∞
+ </Text>
+ ) : (
+ formatBytes(total) +
+ (dataLimitResetStrategy && dataLimitResetStrategy !== "no_reset"
+ ? " " +
+ t(
+ "userDialog.resetStrategy" +
+ getResetStrategy(dataLimitResetStrategy)
+ )
+ : "")
+ )}
+ </Text>
+ <Text>
+ {t("usersTable.total")}: {formatBytes(totalUsedTraffic)}
+ </Text>
+ </HStack>
+ </>
+ );
+};
+export type SortType = {
+ sort: string;
+ column: string;
+};
+export const Sort: FC<SortType> = ({ sort, column }) => {
+ if (sort.includes(column))
+ return (
+ <SortIcon
+ transform={sort.startsWith("-") ? undefined : "rotate(180deg)"}
+ />
+ );
+ return null;
+};
+type UsersTableProps = {} & TableProps;
+export const UsersTable: FC<UsersTableProps> = (props) => {
+ const {
+ filters,
+ users: { users },
+ users: totalUsers,
+ onEditingUser,
+ onFilterChange,
+ } = useDashboard();
+
+ const { t } = useTranslation();
+ const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
+ undefined
+ );
+ const marginTop = useBreakpointValue({ base: 120, lg: 72 }) || 72;
+ const [top, setTop] = useState(`${marginTop}px`);
+ const useTable = useBreakpointValue({ base: false, md: true });
+
+ useEffect(() => {
+ const calcTop = () => {
+ const el = document.querySelectorAll("#filters")[0] as HTMLElement;
+ setTop(`${el.offsetHeight}px`);
+ };
+ window.addEventListener("scroll", calcTop);
+ () => window.removeEventListener("scroll", calcTop);
+ }, []);
+
+ const isFiltered = users.length !== totalUsers.total;
+
+ const handleSort = (column: string) => {
+ let newSort = filters.sort;
+ if (newSort.includes(column)) {
+ if (newSort.startsWith("-")) {
+ newSort = "-created_at";
+ } else {
+ newSort = "-" + column;
+ }
+ } else {
+ newSort = column;
+ }
+ onFilterChange({
+ sort: newSort,
+ });
+ };
+ const handleStatusFilter = (e: any) => {
+ onFilterChange({
+ status: e.target.value.length > 0 ? e.target.value : undefined,
+ });
+ };
+
+ const toggleAccordion = (index: number) => {
+ setSelectedRow(index === selectedRow ? undefined : index);
+ };
+
+ return (
+ <Box id="users-table" overflowX={{ base: "unset", md: "unset" }}>
+ <Accordion
+ allowMultiple
+ display={{ base: "block", md: "none" }}
+ index={selectedRow}
+ >
+ <Table orientation="vertical" zIndex="docked" {...props}>
+ <Thead zIndex="docked" position="relative">
+ <Tr>
+ <Th
+ position="sticky"
+ top={top}
+ minW="120px"
+ pl={4}
+ pr={4}
+ cursor={"pointer"}
+ onClick={handleSort.bind(null, "username")}
+ >
+ <HStack>
+ <span>{t("users")}</span>
+ <Sort sort={filters.sort} column="username" />
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={top}
+ minW="50px"
+ pl={0}
+ pr={0}
+ w="140px"
+ cursor={"pointer"}
+ >
+ <HStack spacing={0} position="relative">
+ <Text
+ position="absolute"
+ _dark={{
+ bg: "gray.750",
+ }}
+ _light={{
+ bg: "#F9FAFB",
+ }}
+ userSelect="none"
+ pointerEvents="none"
+ zIndex={1}
+ w="100%"
+ >
+ {t("usersTable.status")}
+ {filters.status ? ": " + filters.status : ""}
+ </Text>
+ <Select
+ value={filters.sort}
+ fontSize="xs"
+ fontWeight="extrabold"
+ textTransform="uppercase"
+ cursor="pointer"
+ p={0}
+ border={0}
+ h="auto"
+ w="auto"
+ icon={<></>}
+ _focusVisible={{
+ border: "0 !important",
+ }}
+ onChange={handleStatusFilter}
+ >
+ <option></option>
+ <option>active</option>
+ <option>disabled</option>
+ <option>limited</option>
+ <option>expired</option>
+ </Select>
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={top}
+ minW="100px"
+ cursor={"pointer"}
+ pr={0}
+ onClick={handleSort.bind(null, "used_traffic")}
+ >
+ <HStack>
+ <span>{t("usersTable.dataUsage")}</span>
+ <Sort sort={filters.sort} column="used_traffic" />
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={top}
+ minW="32px"
+ w="32px"
+ p={0}
+ cursor={"pointer"}
+ ></Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {!useTable &&
+ users?.map((user, i) => {
+ return (
+ <Fragment key={user.username}>
+ <Tr
+ onClick={toggleAccordion.bind(null, i)}
+ cursor="pointer"
+ >
+ <Td borderBottom={0} minW="100px" pl={4} pr={4}>
+ <div className="flex-status">
+ <OnlineBadge lastOnline={user.online_at} />
+ {user.username}
+ </div>
+ </Td>
+ <Td borderBottom={0} minW="50px" pl={0} pr={0}>
+ <StatusBadge
+ compact
+ showDetail={false}
+ expiryDate={user.expire}
+ status={user.status}
+ />
+ </Td>
+ <Td borderBottom={0} minW="100px" pr={0}>
+ <UsageSliderCompact
+ totalUsedTraffic={user.lifetime_used_traffic}
+ dataLimitResetStrategy={
+ user.data_limit_reset_strategy
+ }
+ used={user.used_traffic}
+ total={user.data_limit}
+ colorScheme={statusColors[user.status].bandWidthColor}
+ />
+ </Td>
+ <Td p={0} borderBottom={0} w="32px" minW="32px">
+ <AccordionArrowIcon
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ transition="transform .2s ease-out"
+ transform={
+ selectedRow === i ? "rotate(180deg)" : "0deg"
+ }
+ />
+ </Td>
+ </Tr>
+ <Tr
+ className="collapsible"
+ onClick={toggleAccordion.bind(null, i)}
+ >
+ <Td p={0} colSpan={4}>
+ <AccordionItem border={0}>
+ <AccordionButton display="none"></AccordionButton>
+ <AccordionPanel
+ border={0}
+ cursor="pointer"
+ px={6}
+ py={3}
+ >
+ <VStack justifyContent="space-between" spacing="4">
+ <VStack
+ alignItems="flex-start"
+ w="full"
+ spacing={-1}
+ >
+ <Text
+ textTransform="capitalize"
+ fontSize="xs"
+ fontWeight="bold"
+ color="gray.600"
+ _dark={{
+ color: "gray.400",
+ }}
+ >
+ {t("usersTable.dataUsage")}
+ </Text>
+ <Box width="full" minW="230px">
+ <UsageSlider
+ totalUsedTraffic={
+ user.lifetime_used_traffic
+ }
+ dataLimitResetStrategy={
+ user.data_limit_reset_strategy
+ }
+ used={user.used_traffic}
+ total={user.data_limit}
+ colorScheme={
+ statusColors[user.status].bandWidthColor
+ }
+ />
+ </Box>
+ </VStack>
+ <HStack w="full" justifyContent="space-between">
+ <Box width="full">
+ <StatusBadge
+ compact
+ expiryDate={user.expire}
+ status={user.status}
+ />
+ <OnlineStatus lastOnline={user.online_at} />
+ </Box>
+ <HStack>
+ <ActionButtons user={user} />
+ <Tooltip
+ label={t("userDialog.editUser")}
+ placement="top"
+ >
+ <IconButton
+ p="0 !important"
+ aria-label="Edit user"
+ bg="transparent"
+ _dark={{
+ _hover: {
+ bg: "gray.700",
+ },
+ }}
+ size={{
+ base: "sm",
+ md: "md",
+ }}
+ onClick={(e) => {
+ e.stopPropagation();
+ onEditingUser(user);
+ }}
+ >
+ <EditIcon />
+ </IconButton>
+ </Tooltip>
+ </HStack>
+ </HStack>
+ </VStack>
+ </AccordionPanel>
+ </AccordionItem>
+ </Td>
+ </Tr>
+ </Fragment>
+ );
+ })}
+ </Tbody>
+ </Table>
+ </Accordion>
+ <Table
+ orientation="vertical"
+ display={{ base: "none", md: "table" }}
+ {...props}
+ >
+ <Thead zIndex="docked" position="relative">
+ <Tr>
+ <Th
+ position="sticky"
+ top={{ base: "unset", md: top }}
+ minW="140px"
+ cursor={"pointer"}
+ onClick={handleSort.bind(null, "username")}
+ >
+ <HStack>
+ <span>{t("username")}</span>
+ <Sort sort={filters.sort} column="username" />
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={{ base: "unset", md: top }}
+ width="400px"
+ minW="150px"
+ cursor={"pointer"}
+ >
+ <HStack spacing={0} position="relative">
+ <Text
+ position="absolute"
+ _dark={{
+ bg: "gray.750",
+ }}
+ _light={{
+ bg: "#F9FAFB",
+ }}
+ userSelect="none"
+ pointerEvents="none"
+ zIndex={1}
+ w="100%"
+ >
+ {t("usersTable.status")}
+ {filters.status ? ": " + filters.status : ""}
+ </Text>
+ <Select
+ fontSize="xs"
+ fontWeight="extrabold"
+ textTransform="uppercase"
+ cursor="pointer"
+ p={0}
+ border={0}
+ h="auto"
+ w="auto"
+ icon={<></>}
+ _focusVisible={{
+ border: "0 !important",
+ }}
+ value={filters.sort}
+ onChange={handleStatusFilter}
+ >
+ <option></option>
+ <option>active</option>
+ <option>disabled</option>
+ <option>limited</option>
+ <option>expired</option>
+ </Select>
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={{ base: "unset", md: top }}
+ width="350px"
+ minW="230px"
+ cursor={"pointer"}
+ onClick={handleSort.bind(null, "used_traffic")}
+ >
+ <HStack>
+ <span>{t("usersTable.dataUsage")}</span>
+ <Sort sort={filters.sort} column="used_traffic" />
+ </HStack>
+ </Th>
+ <Th
+ position="sticky"
+ top={{ base: "unset", md: top }}
+ width="200px"
+ minW="180px"
+ />
+ </Tr>
+ </Thead>
+ <Tbody>
+ {useTable &&
+ users?.map((user, i) => {
+ return (
+ <Tr
+ key={user.username}
+ className={classNames("interactive", {
+ "last-row": i === users.length - 1,
+ })}
+ onClick={() => onEditingUser(user)}
+ >
+ <Td minW="140px">
+ <div className="flex-status">
+ <OnlineBadge lastOnline={user.online_at} />
+ {user.username}
+ <OnlineStatus lastOnline={user.online_at} />
+ </div>
+ </Td>
+ <Td width="400px" minW="150px">
+ <StatusBadge
+ expiryDate={user.expire}
+ status={user.status}
+ />
+ </Td>
+ <Td width="350px" minW="230px">
+ <UsageSlider
+ totalUsedTraffic={user.lifetime_used_traffic}
+ dataLimitResetStrategy={user.data_limit_reset_strategy}
+ used={user.used_traffic}
+ total={user.data_limit}
+ colorScheme={statusColors[user.status].bandWidthColor}
+ />
+ </Td>
+ <Td width="200px" minW="180px">
+ <ActionButtons user={user} />
+ </Td>
+ </Tr>
+ );
+ })}
+ {users.length == 0 && (
+ <Tr>
+ <Td colSpan={4}>
+ <EmptySection isFiltered={isFiltered} />
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </Table>
+ <Pagination />
+ </Box>
+ );
+};
+
+type ActionButtonsProps = {
+ user: User;
+};
+
+const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
+ const { setQRCode, setSubLink } = useDashboard();
+
+ const proxyLinks = user.links.join("\r\n");
+
+ const [copied, setCopied] = useState([-1, false]);
+ useEffect(() => {
+ if (copied[1]) {
+ setTimeout(() => {
+ setCopied([-1, false]);
+ }, 1000);
+ }
+ }, [copied]);
+ return (
+ <HStack
+ justifyContent="flex-end"
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ <CopyToClipboard
+ text={
+ user.subscription_url.startsWith("/")
+ ? window.location.origin + user.subscription_url
+ : user.subscription_url
+ }
+ onCopy={() => {
+ setCopied([0, true]);
+ }}
+ >
+ <div>
+ <Tooltip
+ label={
+ copied[0] == 0 && copied[1]
+ ? t("usersTable.copied")
+ : t("usersTable.copyLink")
+ }
+ placement="top"
+ >
+ <IconButton
+ p="0 !important"
+ aria-label="copy subscription link"
+ bg="transparent"
+ _dark={{
+ _hover: {
+ bg: "gray.700",
+ },
+ }}
+ size={{
+ base: "sm",
+ md: "md",
+ }}
+ >
+ {copied[0] == 0 && copied[1] ? (
+ <CopiedIcon />
+ ) : (
+ <SubscriptionLinkIcon />
+ )}
+ </IconButton>
+ </Tooltip>
+ </div>
+ </CopyToClipboard>
+ <CopyToClipboard
+ text={proxyLinks}
+ onCopy={() => {
+ setCopied([1, true]);
+ }}
+ >
+ <div>
+ <Tooltip
+ label={
+ copied[0] == 1 && copied[1]
+ ? t("usersTable.copied")
+ : t("usersTable.copyConfigs")
+ }
+ placement="top"
+ >
+ <IconButton
+ p="0 !important"
+ aria-label="copy configs"
+ bg="transparent"
+ _dark={{
+ _hover: {
+ bg: "gray.700",
+ },
+ }}
+ size={{
+ base: "sm",
+ md: "md",
+ }}
+ >
+ {copied[0] == 1 && copied[1] ? <CopiedIcon /> : <CopyIcon />}
+ </IconButton>
+ </Tooltip>
+ </div>
+ </CopyToClipboard>
+ <Tooltip label="QR Code" placement="top">
+ <IconButton
+ p="0 !important"
+ aria-label="qr code"
+ bg="transparent"
+ _dark={{
+ _hover: {
+ bg: "gray.700",
+ },
+ }}
+ size={{
+ base: "sm",
+ md: "md",
+ }}
+ onClick={() => {
+ setQRCode(user.links);
+ setSubLink(user.subscription_url);
+ }}
+ >
+ <QRIcon />
+ </IconButton>
+ </Tooltip>
+ </HStack>
+ );
+};
+
+type EmptySectionProps = {
+ isFiltered: boolean;
+};
+
+const EmptySection: FC<EmptySectionProps> = ({ isFiltered }) => {
+ const { onCreateUser } = useDashboard();
+ return (
+ <Box
+ padding="5"
+ py="8"
+ display="flex"
+ alignItems="center"
+ flexDirection="column"
+ gap={4}
+ w="full"
+ >
+ <EmptySectionIcon
+ maxHeight="200px"
+ maxWidth="200px"
+ _dark={{
+ 'path[fill="#fff"]': {
+ fill: "gray.800",
+ },
+ 'path[fill="#f2f2f2"], path[fill="#e6e6e6"], path[fill="#ccc"]': {
+ fill: "gray.700",
+ },
+ 'circle[fill="#3182CE"]': {
+ fill: "primary.300",
+ },
+ }}
+ _light={{
+ 'path[fill="#f2f2f2"], path[fill="#e6e6e6"], path[fill="#ccc"]': {
+ fill: "gray.300",
+ },
+ 'circle[fill="#3182CE"]': {
+ fill: "primary.500",
+ },
+ }}
+ />
+ <Text fontWeight="medium" color="gray.600" _dark={{ color: "gray.400" }}>
+ {isFiltered ? t("usersTable.noUserMatched") : t("usersTable.noUser")}
+ </Text>
+ {!isFiltered && (
+ <Button
+ size="sm"
+ colorScheme="primary"
+ onClick={() => onCreateUser(true)}
+ >
+ {t("createUser")}
+ </Button>
+ )}
+ </Box>
+ );
+};
diff --git a/app/dashboard/src/components/modules/Router.tsx b/app/dashboard/src/components/modules/Router.tsx
@@ -0,0 +1,29 @@
+import { createBrowserRouter, createHashRouter } from "react-router-dom";
+import { Dashboard } from "../../pages/Dashboard";
+import { Login } from "../../pages/Login";
+import { fetch } from "../../service/http";
+import { getAuthToken } from "../../utils/authStorage";
+
+const fetchAdminLoader = () => {
+ return fetch("/admin", {
+ headers: {
+ Authorization: `Bearer ${getAuthToken()}`,
+ },
+ });
+};
+
+export const router = createBrowserRouter(
+ [
+ {
+ path: "/",
+ element: <Dashboard />,
+ errorElement: <Login />,
+ loader: fetchAdminLoader,
+ },
+ {
+ path: "/login/",
+ element: <Login />,
+ },
+ ],
+ { basename: import.meta.env.BASE_URL }
+);
diff --git a/app/dashboard/src/constants/NodesSettings.tsx b/app/dashboard/src/constants/NodesSettings.tsx
@@ -0,0 +1,18 @@
+export const statusColors: {
+ [key: string]: {
+ statusColor: string;
+ };
+} = {
+ connected: {
+ statusColor: "green",
+ },
+ disabled: {
+ statusColor: "gray",
+ },
+ connecting: {
+ statusColor: "orange",
+ },
+ error: {
+ statusColor: "red",
+ },
+};
diff --git a/app/dashboard/src/constants/Project.ts b/app/dashboard/src/constants/Project.ts
@@ -0,0 +1,3 @@
+export const REPO_URL = "https://github.com/Gozargah/Marzban";
+export const ORGANIZATION_URL = "https://github.com/Gozargah";
+export const DONATION_URL = "https://github.com/Gozargah/Marzban#donation";
diff --git a/app/dashboard/src/constants/Proxies.tsx b/app/dashboard/src/constants/Proxies.tsx
@@ -0,0 +1,66 @@
+import { ProxyHostSecurity } from "types/ProxyHosts";
+
+export const proxyHostSecurity: { title: string; value: ProxyHostSecurity }[] =
+ [
+ {
+ title: "Inbound's default",
+ value: "inbound_default",
+ },
+ {
+ title: "TLS",
+ value: "tls",
+ },
+ {
+ title: "None",
+ value: "none",
+ },
+ ];
+
+export const proxyALPN: { title: string; value: string }[] = [
+ {
+ title: "",
+ value: "",
+ },
+ {
+ title: "h2",
+ value: "h2",
+ },
+ {
+ title: "http/1.1",
+ value: "http/1.1",
+ },
+ {
+ title: "h2, http/1.1",
+ value: "h2,http/1.1",
+ },
+];
+
+export const proxyFingerprint: { title: string; value: string }[] = [
+ {
+ title: "",
+ value: "",
+ },
+ ...[
+ "chrome",
+ "firefox",
+ "safari",
+ "ios",
+ "android",
+ "edge",
+ "360",
+ "qq",
+ "random",
+ "randomized",
+ ].map((key) => ({ title: key, value: key })),
+];
+
+export const XTLSFlows = [
+ { title: "none", value: "" },
+ { title: "xtls-rprx-vision", value: "xtls-rprx-vision" },
+];
+
+export const shadowsocksMethods = [
+ "aes-128-gcm",
+ "aes-256-gcm",
+ "chacha20-ietf-poly1305",
+];
diff --git a/app/dashboard/src/constants/UserSettings.tsx b/app/dashboard/src/constants/UserSettings.tsx
@@ -0,0 +1,95 @@
+import { chakra, ChakraComponent} from "@chakra-ui/react";
+import {
+ ClockIcon,
+ ExclamationCircleIcon,
+ NoSymbolIcon,
+ WifiIcon,
+} from "@heroicons/react/24/outline";
+import { ForwardRefExoticComponent, SVGProps } from "react";
+import { DataLimitResetStrategy } from "types/User";
+
+const iconProps = {
+ baseStyle: {
+ strokeWidth: "2px",
+ w: 4,
+ h: 4,
+ },
+};
+const ActiveStatusIcon = chakra(WifiIcon, iconProps);
+const DisabledStatusIcon = chakra(NoSymbolIcon, iconProps);
+const LimitedStatusIcon = chakra(ExclamationCircleIcon, iconProps);
+const ExpiredStatusIcon = chakra(ClockIcon, iconProps);
+const On_holdStatusIcon = chakra(ClockIcon, iconProps);
+
+export const resetStrategy: { title: string; value: DataLimitResetStrategy }[] =
+ [
+ {
+ title: "No",
+ value: "no_reset",
+ },
+ {
+ title: "Daily",
+ value: "day",
+ },
+ {
+ title: "Weekly",
+ value: "week",
+ },
+ {
+ title: "Monthly",
+ value: "month",
+ },
+ {
+ title: "Annually",
+ value: "year",
+ },
+ ];
+
+export const statusColors: {
+ [key: string]: {
+ statusColor: string;
+ bandWidthColor: string;
+ icon: ChakraComponent<ForwardRefExoticComponent<SVGProps<SVGSVGElement>>>;
+ };
+} = {
+ active: {
+ statusColor: "green",
+ bandWidthColor: "primary",
+ icon: ActiveStatusIcon,
+ },
+ connected: {
+ statusColor: "green",
+ bandWidthColor: "primary",
+ icon: ActiveStatusIcon,
+ },
+ disabled: {
+ statusColor: "gray",
+ bandWidthColor: "gray",
+ icon: DisabledStatusIcon,
+ },
+ expired: {
+ statusColor: "orange",
+ bandWidthColor: "orange",
+ icon: ExpiredStatusIcon,
+ },
+ on_hold: {
+ statusColor: "purple",
+ bandWidthColor: "purple",
+ icon: On_holdStatusIcon,
+ },
+ connecting: {
+ statusColor: "orange",
+ bandWidthColor: "orange",
+ icon: ExpiredStatusIcon,
+ },
+ limited: {
+ statusColor: "red",
+ bandWidthColor: "red",
+ icon: LimitedStatusIcon,
+ },
+ error: {
+ statusColor: "red",
+ bandWidthColor: "red",
+ icon: LimitedStatusIcon,
+ },
+};
diff --git a/app/dashboard/src/contexts/CoreSettingsContext.tsx b/app/dashboard/src/contexts/CoreSettingsContext.tsx
@@ -0,0 +1,41 @@
+import { fetch } from "service/http";
+import { create } from "zustand";
+
+type CoreSettingsStore = {
+ isLoading: boolean;
+ isPostLoading: boolean;
+ fetchCoreSettings: () => void;
+ updateConfig: (json: string) => Promise<void>;
+ restartCore: () => Promise<void>;
+ version: string | null;
+ started: boolean | null;
+ logs_websocket: string | null;
+ config: string;
+};
+
+export const useCoreSettings = create<CoreSettingsStore>((set) => ({
+ isLoading: true,
+ isPostLoading: false,
+ version: null,
+ started: false,
+ logs_websocket: null,
+ config: "",
+ fetchCoreSettings: () => {
+ set({ isLoading: true });
+ Promise.all([
+ fetch("/core").then(({ version, started, logs_websocket }) =>
+ set({ version, started, logs_websocket })
+ ),
+ fetch("/core/config").then((config) => set({ config })),
+ ]).finally(() => set({ isLoading: false }));
+ },
+ updateConfig: (body) => {
+ set({ isPostLoading: true });
+ return fetch("/core/config", { method: "PUT", body }).finally(() => {
+ set({ isPostLoading: false });
+ });
+ },
+ restartCore: () => {
+ return fetch("/core/restart", { method: "POST" });
+ },
+}));
diff --git a/app/dashboard/src/contexts/DashboardContext.tsx b/app/dashboard/src/contexts/DashboardContext.tsx
@@ -0,0 +1,235 @@
+import { StatisticsQueryKey } from "components/Statistics";
+import { fetch } from "service/http";
+import { User, UserCreate } from "types/User";
+import { Service } from "types/Service";
+import { queryClient } from "utils/react-query";
+import { getUsersPerPageLimitSize } from "utils/userPreferenceStorage";
+import { create } from "zustand";
+import { subscribeWithSelector } from "zustand/middleware";
+
+export type FilterType = {
+ username?: string;
+ limit?: number;
+ offset?: number;
+ sort: string;
+ status?: "active" | "disabled" | "limited" | "expired" | "on_hold";
+};
+export type ProtocolType = "vmess" | "vless" | "trojan" | "shadowsocks";
+
+export type FilterUsageType = {
+ start?: string;
+ end?: string;
+};
+
+export type InboundType = {
+ tag: string;
+ protocol: ProtocolType;
+ network: string;
+ tls: string;
+ port?: number;
+};
+
+export type Inbounds = Map<ProtocolType, InboundType[]>;
+
+type DashboardStateType = {
+ isCreatingNewUser: boolean;
+ editingUser: User | null | undefined;
+ deletingUser: User | null;
+ version: string | null;
+ users: {
+ users: User[];
+ total: number;
+ };
+ inbounds: Inbounds;
+ services: Service[];
+ loading: boolean;
+ filters: FilterType;
+ subscribeUrl: string | null;
+ QRcodeLinks: string[] | null;
+ isEditingHosts: boolean;
+ isEditingNodes: boolean;
+ isShowingNodesUsage: boolean;
+ isResetingAllUsage: boolean;
+ resetUsageUser: User | null;
+ revokeSubscriptionUser: User | null;
+ isEditingCore: boolean;
+ onCreateUser: (isOpen: boolean) => void;
+ onEditingUser: (user: User | null) => void;
+ onDeletingUser: (user: User | null) => void;
+ onResetAllUsage: (isResetingAllUsage: boolean) => void;
+ refetchUsers: () => void;
+ refetchServices: () => void;
+ resetAllUsage: () => Promise<void>;
+ onFilterChange: (filters: Partial<FilterType>) => void;
+ deleteUser: (user: User) => Promise<void>;
+ createUser: (user: UserCreate) => Promise<void>;
+ editUser: (user: UserCreate) => Promise<void>;
+ fetchUserUsage: (user: User, query: FilterUsageType) => Promise<void>;
+ setQRCode: (links: string[] | null) => void;
+ setSubLink: (subscribeURL: string | null) => void;
+ onEditingHosts: (isEditingHosts: boolean) => void;
+ onEditingNodes: (isEditingHosts: boolean) => void;
+ onShowingNodesUsage: (isShowingNodesUsage: boolean) => void;
+ resetDataUsage: (user: User) => Promise<void>;
+ revokeSubscription: (user: User) => Promise<void>;
+};
+
+export const fetchUsers = async (query: FilterType): Promise<User[]> => {
+ for (const key in query) {
+ if (!query[key as keyof FilterType]) delete query[key as keyof FilterType];
+ }
+ useDashboard.setState({ loading: true });
+ return fetch("/users", { query })
+ .then((users) => {
+ useDashboard.setState({ users });
+ return users;
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const fetchServices = async (): Promise<Service[]> => {
+ useDashboard.setState({ loading: true });
+ return fetch("/services")
+ .then((services) => {
+ useDashboard.setState({ services });
+ return services;
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const fetchInbounds = async () => {
+ return fetch("/inbounds")
+ .then((inbounds: Inbounds) => {
+ useDashboard.setState({
+ inbounds: new Map(Object.entries(inbounds)) as Inbounds,
+ });
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const useDashboard = create(
+ subscribeWithSelector<DashboardStateType>((set, get) => ({
+ version: null,
+ editingUser: null,
+ deletingUser: null,
+ isCreatingNewUser: false,
+ QRcodeLinks: null,
+ subscribeUrl: null,
+ users: {
+ users: [],
+ total: 0,
+ },
+ loading: true,
+ services: [],
+ isResetingAllUsage: false,
+ isEditingHosts: false,
+ isEditingNodes: false,
+ isShowingNodesUsage: false,
+ resetUsageUser: null,
+ revokeSubscriptionUser: null,
+ filters: {
+ username: "",
+ limit: getUsersPerPageLimitSize(),
+ sort: "-created_at",
+ },
+ inbounds: new Map(),
+ isEditingCore: false,
+ refetchServices: () => {
+ fetchServices();
+ },
+ refetchUsers: () => {
+ fetchUsers(get().filters);
+ },
+ resetAllUsage: async () => {
+ return fetch(`/users/reset`, { method: "POST" }).then(() => {
+ get().onResetAllUsage(false);
+ get().refetchUsers();
+ });
+ },
+ onResetAllUsage: (isResetingAllUsage) => set({ isResetingAllUsage }),
+ onCreateUser: (isCreatingNewUser) => set({ isCreatingNewUser }),
+ onEditingUser: (editingUser) => {
+ set({ editingUser });
+ },
+ onDeletingUser: (deletingUser) => {
+ set({ deletingUser });
+ },
+ onFilterChange: (filters) => {
+ set({
+ filters: {
+ ...get().filters,
+ ...filters,
+ },
+ });
+ get().refetchUsers();
+ },
+ setQRCode: (QRcodeLinks) => {
+ set({ QRcodeLinks });
+ },
+ deleteUser: async (user: User) => {
+ set({ editingUser: null });
+ return fetch(`/user/${user.username}`, { method: "DELETE" }).then(() => {
+ set({ deletingUser: null });
+ get().refetchUsers();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ createUser: async (body: UserCreate) => {
+ return fetch(`/user`, { method: "POST", body }).then(() => {
+ set({ editingUser: null });
+ get().refetchUsers();
+ get().refetchServices();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ editUser: async (body: UserCreate) => {
+ return fetch(`/user/${body.username}`, { method: "PUT", body }).then(
+ () => {
+ get().onEditingUser(null);
+ get().refetchUsers();
+ }
+ );
+ },
+ fetchUserUsage: (body: User, query: FilterUsageType) => {
+ for (const key in query) {
+ if (!query[key as keyof FilterUsageType])
+ delete query[key as keyof FilterUsageType];
+ }
+ return fetch(`/user/${body.username}/usage`, { method: "GET", query });
+ },
+ onEditingHosts: (isEditingHosts: boolean) => {
+ set({ isEditingHosts });
+ },
+ onEditingNodes: (isEditingNodes: boolean) => {
+ set({ isEditingNodes });
+ },
+ onShowingNodesUsage: (isShowingNodesUsage: boolean) => {
+ set({ isShowingNodesUsage });
+ },
+ setSubLink: (subscribeUrl) => {
+ set({ subscribeUrl });
+ },
+ resetDataUsage: async (user) => {
+ return fetch(`/user/${user.username}/reset`, { method: "POST" }).then(
+ () => {
+ set({ resetUsageUser: null });
+ get().refetchUsers();
+ }
+ );
+ },
+ revokeSubscription: async (user) => {
+ return fetch(`/user/${user.username}/revoke_sub`, {
+ method: "POST",
+ }).then((user) => {
+ set({ revokeSubscriptionUser: null, editingUser: user });
+ get().refetchUsers();
+ });
+ },
+ }))
+);
diff --git a/app/dashboard/src/contexts/HostsContext.tsx b/app/dashboard/src/contexts/HostsContext.tsx
@@ -0,0 +1,39 @@
+import { fetch } from "service/http";
+import { create } from "zustand";
+
+type HostsSchema = Record<
+ string,
+ {
+ remark: string;
+ address: string;
+ port: number | null;
+ path: string | null;
+ sni: string | null;
+ host: string | null;
+ }[]
+>;
+
+type HostsStore = {
+ isLoading: boolean;
+ isPostLoading: boolean;
+ hosts: HostsSchema;
+ fetchHosts: () => void;
+ setHosts: (hosts: HostsSchema) => Promise<void>;
+};
+export const useHosts = create<HostsStore>((set) => ({
+ isLoading: false,
+ isPostLoading: false,
+ hosts: {},
+ fetchHosts: () => {
+ set({ isLoading: true });
+ fetch("/hosts")
+ .then((hosts) => set({ hosts }))
+ .finally(() => set({ isLoading: false }));
+ },
+ setHosts: (body) => {
+ set({ isPostLoading: true });
+ return fetch("/hosts", { method: "PUT", body }).finally(() => {
+ set({ isPostLoading: false });
+ });
+ },
+}));
\ No newline at end of file
diff --git a/app/dashboard/src/contexts/NodesContext.tsx b/app/dashboard/src/contexts/NodesContext.tsx
@@ -0,0 +1,92 @@
+import { useQuery } from "react-query";
+import { fetch } from "service/http";
+import { z } from "zod";
+import { create } from "zustand";
+import { FilterUsageType, useDashboard } from "./DashboardContext";
+
+export const NodeSchema = z.object({
+ name: z.string().min(1),
+ address: z.string().min(1),
+ port: z
+ .number()
+ .min(1)
+ .or(z.string().transform((v) => parseFloat(v))),
+ api_port: z
+ .number()
+ .min(1)
+ .or(z.string().transform((v) => parseFloat(v))),
+ xray_version: z.string().nullable().optional(),
+ id: z.number().nullable().optional(),
+ status: z
+ .enum(["connected", "connecting", "error", "disabled"])
+ .nullable()
+ .optional(),
+ message: z.string().nullable().optional(),
+ add_as_new_host: z.boolean().optional(),
+});
+
+export type NodeType = z.infer<typeof NodeSchema>;
+
+export const getNodeDefaultValues = (): NodeType => ({
+ name: "",
+ address: "",
+ port: 62050,
+ api_port: 62051,
+ xray_version: "",
+});
+
+export const FetchNodesQueryKey = "fetch-nodes-query-key";
+
+export type NodeStore = {
+ nodes: NodeType[];
+ addNode: (node: NodeType) => Promise<unknown>;
+ fetchNodes: () => Promise<NodeType[]>;
+ fetchNodesUsage: (query: FilterUsageType) => Promise<void>;
+ updateNode: (node: NodeType) => Promise<unknown>;
+ reconnectNode: (node: NodeType) => Promise<unknown>;
+ deletingNode?: NodeType | null;
+ deleteNode: () => Promise<unknown>;
+ setDeletingNode: (node: NodeType | null) => void;
+};
+
+export const useNodesQuery = () => {
+ const { isEditingNodes } = useDashboard();
+ return useQuery({
+ queryKey: FetchNodesQueryKey,
+ queryFn: useNodes.getState().fetchNodes,
+ refetchInterval: isEditingNodes ? 3000 : undefined,
+ refetchOnWindowFocus: false,
+ });
+};
+
+export const useNodes = create<NodeStore>((set, get) => ({
+ nodes: [],
+ addNode(body) {
+ return fetch("/node", { method: "POST", body });
+ },
+ fetchNodes() {
+ return fetch("/nodes");
+ },
+ fetchNodesUsage(query: FilterUsageType) {
+ return fetch("/nodes/usage", { query });
+ },
+ updateNode(body) {
+ return fetch(`/node/${body.id}`, {
+ method: "PUT",
+ body,
+ });
+ },
+ setDeletingNode(node) {
+ set({ deletingNode: node });
+ },
+ reconnectNode(body) {
+ return fetch(`/node/${body.id}/reconnect`, {
+ method: "POST",
+ });
+ },
+ deleteNode: () => {
+ return fetch(`/node/${get().deletingNode?.id}`, {
+ method: "DELETE",
+ });
+ },
+}));
diff --git a/app/dashboard/src/index.scss b/app/dashboard/src/index.scss
@@ -0,0 +1,282 @@
+@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
+
+.bg-blue-animate {
+ animation: blur-animate 200ms ease-in;
+ backdrop-filter: blur(10px);
+}
+
+@keyframes blur-animate {
+ from {
+ backdrop-filter: blur(1px);
+ }
+
+ to {
+ backdrop-filter: blur(10px);
+ }
+}
+
+.animate-spin {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+table thead {
+ th:first-of-type {
+ border-top-left-radius: 8px;
+ }
+
+ th:last-of-type {
+ border-top-right-radius: 8px;
+ }
+}
+
+.last-row {
+ td:first-of-type {
+ border-bottom-left-radius: 8px;
+ }
+
+ td:last-of-type {
+ border-bottom-right-radius: 8px;
+ }
+}
+
+.slick-prev {
+ left: -40px;
+}
+
+.slick-next {
+ right: -40px;
+}
+
+.slick-prev,
+.slick-next {
+ z-index: 100;
+}
+
+.chakra-popover__popper {
+ z-index: 9999 !important;
+}
+
+.inbound-item .chakra-checkbox__label {
+ max-width: 100%;
+ width: 100%;
+}
+
+// data picker
+.react-datepicker__navigation.react-datepicker__navigation--previous,
+.react-datepicker__navigation.react-datepicker__navigation--next {
+ padding-top: 6px;
+}
+
+.react-datepicker {
+ background-color: var(--chakra-colors-white) !important;
+ border: 1px solid var(--chakra-colors-gray-200);
+ border-radius: var(--chakra-radii-md) !important;
+ position: relative;
+}
+
+.chakra-ui-dark .react-datepicker {
+ background-color: var(--chakra-colors-gray-700) !important;
+ border: 1px solid var(--chakra-colors-gray-600);
+ border-radius: var(--chakra-radii-md) !important;
+ position: relative;
+}
+
+.react-datepicker__header {
+ text-align: center;
+ background-color: unset;
+ position: relative;
+ border-bottom: 1px solid var(--chakra-colors-gray-200);
+}
+
+.chakra-ui-dark .react-datepicker__header {
+ border-bottom: 1px solid var(--chakra-colors-gray-600);
+}
+
+.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::after {
+ border-top: none;
+ border-bottom-color: var(--chakra-colors-white);
+}
+
+.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before {
+ top: -1px;
+ border-bottom-color: var(--chakra-colors-gray-200);
+}
+
+.chakra-ui-dark .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::after {
+ border-top: none;
+ border-bottom-color: var(--chakra-colors-gray-700);
+}
+
+.chakra-ui-dark .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before {
+ top: -1px;
+ border-bottom-color: var(--chakra-colors-gray-600);
+}
+
+.react-datepicker__current-month,
+.react-datepicker__day-name,
+.react-datepicker__day {
+ color: var(--chakra-colors-chakra-body-text) !important;
+}
+
+.chakra-ui-dark .react-datepicker__current-month,
+.chakra-ui-dark .react-datepicker__day-name,
+.chakra-ui-dark .react-datepicker__day {
+ color: var(--chakra-colors-gray-300) !important;
+}
+
+.react-datepicker__day:hover {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-300) !important;
+ color: var(--chakra-colors-white) !important;
+}
+
+.chakra-ui-dark .react-datepicker__day:hover {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-500) !important;
+ color: var(--chakra-colors-gray-800) !important;
+}
+
+.react-datepicker__day--disabled {
+ color: var(--chakra-colors-gray-400) !important;
+}
+
+.react-datepicker__day--disabled:hover {
+ background-color: transparent !important;
+ color: var(--chakra-colors-gray-400) !important;
+}
+
+.chakra-ui-dark .react-datepicker__day--disabled {
+ color: var(--chakra-colors-gray-500) !important;
+}
+
+.chakra-ui-dark .react-datepicker__day--disabled:hover {
+ background-color: transparent !important;
+ color: var(--chakra-colors-gray-500) !important;
+}
+
+.react-datepicker__day--keyboard-selected,
+.react-datepicker__day--selected,
+.react-datepicker__day--in-selecting-range,
+.react-datepicker__day--in-range {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-500) !important;
+ color: var(--chakra-colors-white) !important;
+}
+
+.chakra-ui-dark .react-datepicker__day--keyboard-selected,
+.chakra-ui-dark .react-datepicker__day--selected,
+.chakra-ui-dark .react-datepicker__day--in-selecting-range,
+.chakra-ui-dark .react-datepicker__day--in-range {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-200) !important;
+ color: var(--chakra-colors-gray-800) !important;
+}
+
+.react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range) {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-400) !important;
+}
+
+.chakra-ui-dark .react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range) {
+ border-radius: 0.3rem;
+ background-color: var(--chakra-colors-primary-400) !important;
+}
+
+.react-datepicker__day--outside-month {
+ visibility: hidden;
+}
+
+.datepicker-panel .react-datepicker {
+ background-color: unset;
+ border: unset;
+ border-radius: unset;
+ display: inline-block;
+ position: relative;
+}
+
+.green {
+ background: #66ff99;
+}
+
+.red {
+ background: #e53e3e;
+}
+
+.orange {
+ background: #fbd38d;
+}
+
+.circle {
+ min-width: 10px;
+ min-height: 10px;
+ border-radius: 50%;
+
+ box-shadow: 0px 0px 1px 1px #0000001a;
+}
+
+.pulse {
+ animation: pulse-animation 3s infinite;
+}
+.pulse.green {
+ animation: green-pulse-animation 3s infinite;
+}
+.pulse.red{
+ animation: red-pulse-animation 3s infinite;
+}
+.pulse.orange{
+ animation: orange-pulse-animation 3s infinite;
+}
+
+@keyframes pulse-animation {
+ 0% {
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.4);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
+ }
+}
+@keyframes green-pulse-animation {
+ 0% {
+ box-shadow: 0 0 0 0px #66ff9975;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 10px #66ff9900;
+ }
+}
+@keyframes red-pulse-animation {
+ 0% {
+ box-shadow: 0 0 0 0px #e53e3e8c;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 10px #e53e3e00;
+ }
+}
+@keyframes orange-pulse-animation {
+ 0% {
+ box-shadow: 0 0 0 0px #fbd38d85;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 10px #fbd38d00;
+ }
+}
+
+.flex-status {
+ display: flex;
+ align-items: center;
+ justify-content: start;
+ gap: .7rem;
+}
\ No newline at end of file
diff --git a/app/dashboard/src/index.tsx b/app/dashboard/src/index.tsx
@@ -0,0 +1,34 @@
+import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import Duration from "dayjs/plugin/duration";
+import LocalizedFormat from "dayjs/plugin/localizedFormat";
+import RelativeTime from "dayjs/plugin/relativeTime";
+import Timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import "locales/i18n";
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { QueryClientProvider } from "react-query";
+import { queryClient } from "utils/react-query";
+import { updateThemeColor } from "utils/themeColor";
+import { theme } from "../chakra.config";
+import App from "./App";
+import "index.scss";
+
+dayjs.extend(Timezone);
+dayjs.extend(LocalizedFormat);
+dayjs.extend(utc);
+dayjs.extend(RelativeTime);
+dayjs.extend(Duration);
+
+updateThemeColor(localStorageManager.get() || "light");
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+ <React.StrictMode>
+ <ChakraProvider theme={theme}>
+ <QueryClientProvider client={queryClient}>
+ <App />
+ </QueryClientProvider>
+ </ChakraProvider>
+ </React.StrictMode>
+);
diff --git a/app/dashboard/src/locales/i18n.ts b/app/dashboard/src/locales/i18n.ts
@@ -0,0 +1,56 @@
+import { joinPaths } from "@remix-run/router";
+
+import fa from "date-fns/locale/fa-IR";
+import ru from "date-fns/locale/ru";
+import zh from "date-fns/locale/zh-CN";
+import dayjs from "dayjs";
+import i18n from "i18next";
+import LanguageDetector from "i18next-browser-languagedetector";
+import HttpApi from "i18next-http-backend";
+import { registerLocale } from "react-datepicker";
+import { initReactI18next } from "react-i18next";
+
+declare module "i18next" {
+ interface CustomTypeOptions {
+ returnNull: false;
+ }
+}
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .use(HttpApi)
+ .init(
+ {
+ debug: import.meta.env.NODE_ENV === "development",
+ returnNull: false,
+ fallbackLng: "en",
+ interpolation: {
+ escapeValue: false,
+ },
+ react: {
+ useSuspense: false,
+ },
+ load: "languageOnly",
+ detection: {
+ caches: ["localStorage", "sessionStorage", "cookie"],
+ },
+ backend: {
+ loadPath: joinPaths([import.meta.env.BASE_URL, `locales/{{lng}}.json`]),
+ },
+ },
+ function (err, t) {
+ dayjs.locale(i18n.language);
+ }
+ );
+
+i18n.on("languageChanged", (lng) => {
+ dayjs.locale(lng);
+});
+
+// DataPicker
+registerLocale("zh-cn", zh);
+registerLocale("ru", ru);
+registerLocale("fa", fa);
+
+export default i18n;
diff --git a/app/dashboard/src/pages/Dashboard.tsx b/app/dashboard/src/pages/Dashboard.tsx
@@ -0,0 +1,48 @@
+import { Box, VStack } from "@chakra-ui/react";
+import { CoreSettingsModal } from "components/CoreSettingsModal";
+import { DeleteUserModal } from "components/DeleteUserModal";
+import { Filters } from "components/Filters";
+import { Footer } from "components/Footer";
+import { Header } from "components/Header";
+import { HostsDialog } from "components/HostsDialog";
+import { NodesDialog } from "components/NodesModal";
+import { NodesUsage } from "components/NodesUsage";
+import { QRCodeDialog } from "components/QRCodeDialog";
+import { ResetAllUsageModal } from "components/ResetAllUsageModal";
+import { ResetUserUsageModal } from "components/ResetUserUsageModal";
+import { RevokeSubscriptionModal } from "components/RevokeSubscriptionModal";
+import { UserDialog } from "components/UserDialog";
+import { UsersTable } from "components/UsersTable";
+import { fetchInbounds, useDashboard } from "contexts/DashboardContext";
+import { FC, useEffect } from "react";
+import { Statistics } from "../components/Statistics";
+
+export const Dashboard: FC = () => {
+ useEffect(() => {
+ useDashboard.getState().refetchUsers();
+ fetchInbounds();
+ }, []);
+ return (
+ <VStack justifyContent="space-between" minH="100vh" p="6" rowGap={4}>
+ <Box w="full">
+ <Header />
+ <Statistics mt="4" />
+ <Filters />
+ <UsersTable />
+ <UserDialog />
+ <DeleteUserModal />
+ <QRCodeDialog />
+ <HostsDialog />
+ <ResetUserUsageModal />
+ <RevokeSubscriptionModal />
+ <NodesDialog />
+ <NodesUsage />
+ <ResetAllUsageModal />
+ <CoreSettingsModal />
+ </Box>
+ <Footer />
+ </VStack>
+ );
+};
+
+export default Dashboard;
diff --git a/app/dashboard/src/pages/Login.tsx b/app/dashboard/src/pages/Login.tsx
@@ -0,0 +1,148 @@
+import {
+ Alert,
+ AlertDescription,
+ AlertIcon,
+ Box,
+ Button,
+ chakra,
+ FormControl,
+ FormLabel,
+ HStack,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+import { ArrowRightOnRectangleIcon } from "@heroicons/react/24/outline";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { FC, useEffect, useState } from "react";
+import { FieldValues, useForm } from "react-hook-form";
+import { useLocation, useNavigate } from "react-router-dom";
+import { z } from "zod";
+import { Footer } from "components/Footer";
+import { Input } from "components/Input";
+import { fetch } from "service/http";
+import { removeAuthToken, setAuthToken } from "utils/authStorage";
+import { ReactComponent as Logo } from "assets/logo.svg";
+import { useTranslation } from "react-i18next";
+import { Language } from "components/Language";
+
+const schema = z.object({
+ username: z.string().min(1, "login.fieldRequired"),
+ password: z.string().min(1, "login.fieldRequired"),
+});
+
+export const LogoIcon = chakra(Logo, {
+ baseStyle: {
+ strokeWidth: "10px",
+ w: 12,
+ h: 12,
+ },
+});
+
+const LoginIcon = chakra(ArrowRightOnRectangleIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ strokeWidth: "2px",
+ },
+});
+
+export const Login: FC = () => {
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ let location = useLocation();
+ const {
+ register,
+ formState: { errors },
+ handleSubmit,
+ } = useForm({
+ resolver: zodResolver(schema),
+ });
+ useEffect(() => {
+ removeAuthToken();
+ if (location.pathname !== "/login") {
+ navigate("/login", { replace: true });
+ }
+ }, []);
+ const login = (values: FieldValues) => {
+ setError("");
+ const formData = new FormData();
+ formData.append("username", values.username);
+ formData.append("password", values.password);
+ formData.append("grant_type", "password");
+ setLoading(true);
+ fetch("/admin/token", { method: "post", body: formData })
+ .then(({ access_token: token }) => {
+ setAuthToken(token);
+ navigate("/");
+ })
+ .catch((err) => {
+ setError(err.response._data.detail);
+ })
+ .finally(setLoading.bind(null, false));
+ };
+ return (
+ <VStack justifyContent="space-between" minH="100vh" p="6" w="full">
+ <Box w="full">
+ <HStack justifyContent="end" w="full">
+ <Language />
+ </HStack>
+ <HStack w="full" justifyContent="center" alignItems="center">
+ <Box w="full" maxW="340px" mt="6">
+ <VStack alignItems="center" w="full">
+ <LogoIcon />
+ <Text fontSize="2xl" fontWeight="semibold">
+ {t("login.loginYourAccount")}
+ </Text>
+ <Text color="gray.600" _dark={{ color: "gray.400" }}>
+ {t("login.welcomeBack")}
+ </Text>
+ </VStack>
+ <Box w="full" maxW="300px" m="auto" pt="4">
+ <form onSubmit={handleSubmit(login)}>
+ <VStack mt={4} rowGap={2}>
+ <FormControl>
+ <Input
+ w="full"
+ placeholder={t("username")}
+ {...register("username")}
+ error={t(errors?.username?.message as string)}
+ />
+ </FormControl>
+ <FormControl>
+ <Input
+ w="full"
+ type="password"
+ placeholder={t("password")}
+ {...register("password")}
+ error={t(errors?.password?.message as string)}
+ />
+ </FormControl>
+ {error && (
+ <Alert status="error" rounded="md">
+ <AlertIcon />
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+ <Button
+ isLoading={loading}
+ type="submit"
+ w="full"
+ colorScheme="primary"
+ >
+ {<LoginIcon marginRight={1} />}
+ {t("login")}
+ </Button>
+ </VStack>
+ </form>
+ </Box>
+ </Box>
+ </HStack>
+ </Box>
+ <Footer />
+ </VStack>
+ );
+};
+
+export default Login;
diff --git a/app/dashboard/src/service/http.ts b/app/dashboard/src/service/http.ts
@@ -0,0 +1,22 @@
+import { FetchOptions, $fetch as ohMyFetch } from "ohmyfetch";
+import { getAuthToken } from "utils/authStorage";
+
+export const $fetch = ohMyFetch.create({
+ baseURL: import.meta.env.VITE_BASE_API,
+});
+
+export const fetcher = <T = any>(
+ url: string,
+ ops: FetchOptions<"json"> = {}
+) => {
+ const token = getAuthToken();
+ if (token) {
+ ops["headers"] = {
+ ...(ops?.headers || {}),
+ Authorization: `Bearer ${getAuthToken()}`,
+ };
+ }
+ return $fetch<T>(url, ops);
+};
+
+export const fetch = fetcher;
diff --git a/app/dashboard/src/types/ProxyHosts.ts b/app/dashboard/src/types/ProxyHosts.ts
@@ -0,0 +1 @@
+export type ProxyHostSecurity = "inbound_default" | "none" | "tls"
diff --git a/app/dashboard/src/types/Service.ts b/app/dashboard/src/types/Service.ts
@@ -0,0 +1,7 @@
+
+export type Service = {
+ id: number;
+ name: string;
+ users: number[];
+ inbounds: number[];
+}
diff --git a/app/dashboard/src/types/User.ts b/app/dashboard/src/types/User.ts
@@ -0,0 +1,62 @@
+export type Status =
+ | "active"
+ | "disabled"
+ | "limited"
+ | "expired"
+ | "on_hold"
+ | "error"
+ | "connecting"
+ | "connected";
+export type ProxyKeys = ("vmess" | "vless" | "trojan" | "shadowsocks")[];
+export type ProxyType = {
+ vmess?: {
+ id?: string;
+ };
+ vless?: {
+ id?: string;
+ flow?: string;
+ };
+ trojan?: {
+ password?: string;
+ };
+ shadowsocks?: {
+ password?: string;
+ method?: string;
+ };
+};
+
+export type DataLimitResetStrategy =
+ | "no_reset"
+ | "day"
+ | "week"
+ | "month"
+ | "year";
+
+type serviceId = number;
+
+export type User = {
+ expire: number | null;
+ data_limit: number | null;
+ data_limit_reset_strategy: DataLimitResetStrategy;
+ lifetime_used_traffic: number;
+ username: string;
+ used_traffic: number;
+ status: Status;
+ links: string[];
+ subscription_url: string;
+ services: serviceId[];
+ note: string;
+ online_at: string;
+};
+
+export type UserCreate = Pick<
+
+ User,
+ | "services"
+ | "expire"
+ | "data_limit"
+ | "data_limit_reset_strategy"
+ | "username"
+ | "status"
+ | "note"
+>;
diff --git a/app/dashboard/src/utils/authStorage.ts b/app/dashboard/src/utils/authStorage.ts
@@ -0,0 +1,11 @@
+export const getAuthToken = () => {
+ return localStorage.getItem("token");
+};
+
+export const setAuthToken = (token: string) => {
+ localStorage.setItem("token", token);
+};
+
+export const removeAuthToken = () => {
+ localStorage.removeItem("token");
+};
diff --git a/app/dashboard/src/utils/color.ts b/app/dashboard/src/utils/color.ts
@@ -0,0 +1,49 @@
+export function generateDistinctColors(numColors: number) {
+ const hueStep = 360 / numColors;
+ const saturation = 90;
+ const lightness = 47;
+ const colors = [];
+
+ for (let i = 0; i < numColors; i++) {
+ const hue = ((i * hueStep) % 360) + 140;
+ const color = hslToHex(hue, saturation, lightness);
+ colors.push(color);
+ }
+
+ return colors;
+}
+
+function hslToHex(h: number, s: number, l: number) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+
+ let r, g, b;
+
+ if (s === 0) {
+ r = g = b = l;
+ } else {
+ const hueToRgb = (p: number, q: number, t: number) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+
+ r = Math.round(hueToRgb(p, q, h + 1 / 3) * 255);
+ g = Math.round(hueToRgb(p, q, h) * 255);
+ b = Math.round(hueToRgb(p, q, h - 1 / 3) * 255);
+ }
+
+ const toHex = (c: number) => {
+ const hex = c.toString(16);
+ return hex.length === 1 ? "0" + hex : hex;
+ };
+
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
diff --git a/app/dashboard/src/utils/dateFormatter.ts b/app/dashboard/src/utils/dateFormatter.ts
@@ -0,0 +1,61 @@
+import dayjs from "dayjs";
+
+export const relativeExpiryDate = (expiryDate: number | null | undefined) => {
+ let dateInfo = { status: "", time: "" };
+ if (expiryDate) {
+ if (
+ dayjs(expiryDate * 1000)
+ .utc()
+ .isAfter(dayjs().utc())
+ ) {
+ dateInfo.status = "expires";
+ } else {
+ dateInfo.status = "expired";
+ }
+ const durationSlots: string[] = [];
+ const duration = dayjs.duration(
+ dayjs(expiryDate * 1000)
+ .utc()
+ .diff(dayjs())
+ );
+ if (duration.years() != 0) {
+ durationSlots.push(
+ Math.abs(duration.years()) +
+ " year" +
+ (Math.abs(duration.years()) != 1 ? "s" : "")
+ );
+ }
+ if (duration.months() != 0) {
+ durationSlots.push(
+ Math.abs(duration.months()) +
+ " month" +
+ (Math.abs(duration.months()) != 1 ? "s" : "")
+ );
+ }
+ if (duration.days() != 0) {
+ durationSlots.push(
+ Math.abs(duration.days()) +
+ " day" +
+ (Math.abs(duration.days()) != 1 ? "s" : "")
+ );
+ }
+ if (durationSlots.length === 0) {
+ if (duration.hours() != 0) {
+ durationSlots.push(
+ Math.abs(duration.hours()) +
+ " hour" +
+ (Math.abs(duration.hours()) != 1 ? "s" : "")
+ );
+ }
+ if (duration.minutes() != 0) {
+ durationSlots.push(
+ Math.abs(duration.minutes()) +
+ " min" +
+ (Math.abs(duration.minutes()) != 1 ? "s" : "")
+ );
+ }
+ }
+ dateInfo.time = durationSlots.join(", ");
+ }
+ return dateInfo;
+};
diff --git a/app/dashboard/src/utils/formatByte.tsx b/app/dashboard/src/utils/formatByte.tsx
@@ -0,0 +1,16 @@
+export function formatBytes(bytes: number, decimals = 2, asArray = false) {
+ if (!+bytes) return "0 B";
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ if (!asArray)
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
+ else return [parseFloat((bytes / Math.pow(k, i)).toFixed(dm)), sizes[i]];
+}
+
+export const numberWithCommas = (x: number) => {
+ if (x !== null) return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
diff --git a/app/dashboard/src/utils/react-query.ts b/app/dashboard/src/utils/react-query.ts
@@ -0,0 +1,3 @@
+import { QueryClient } from "react-query";
+
+export const queryClient = new QueryClient();
diff --git a/app/dashboard/src/utils/themeColor.ts b/app/dashboard/src/utils/themeColor.ts
@@ -0,0 +1,6 @@
+import { ColorMode } from "@chakra-ui/react";
+
+export const updateThemeColor = (colorMode: ColorMode) => {
+ const el = document.querySelector('meta[name="theme-color"]');
+ el?.setAttribute('content', colorMode == "dark" ? "#1A202C" : "#3B81F6");
+};
diff --git a/app/dashboard/src/utils/toastHandler.ts b/app/dashboard/src/utils/toastHandler.ts
@@ -0,0 +1,48 @@
+import { CreateToastFnReturn } from "@chakra-ui/react";
+import { UseFormReturn } from "react-hook-form";
+
+export const generateErrorMessage = (
+ e: any,
+ toast: CreateToastFnReturn,
+ form?: UseFormReturn<any>
+) => {
+ if (e.response && e.response._data) {
+ if (typeof e.response._data.detail === "string")
+ return toast({
+ title: e.response._data.detail,
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+ if (typeof e.response._data.detail === "object")
+ if (form) {
+ Object.keys(e.response._data.detail).forEach((errorKey) =>
+ form.setError(errorKey, {
+ message: e.response._data.detail[errorKey],
+ })
+ );
+ return;
+ }
+ }
+ return toast({
+ title: "Something went wrong!",
+ status: "error",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+};
+
+export const generateSuccessMessage = (
+ message: string,
+ toast: CreateToastFnReturn
+) => {
+ return toast({
+ title: message,
+ status: "success",
+ isClosable: true,
+ position: "top",
+ duration: 3000,
+ });
+};
diff --git a/app/dashboard/src/utils/userPreferenceStorage.ts b/app/dashboard/src/utils/userPreferenceStorage.ts
@@ -0,0 +1,12 @@
+const NUM_USERS_PER_PAGE_LOCAL_STORAGE_KEY = "marzban-num-users-per-page";
+const NUM_USERS_PER_PAGE_DEFAULT = 10;
+export const getUsersPerPageLimitSize = () => {
+ const numUsersPerPage =
+ localStorage.getItem(NUM_USERS_PER_PAGE_LOCAL_STORAGE_KEY) ||
+ NUM_USERS_PER_PAGE_DEFAULT.toString(); // this catches `null` values
+ return parseInt(numUsersPerPage) || NUM_USERS_PER_PAGE_DEFAULT; // this catches NaN values
+};
+
+export const setUsersPerPageLimitSize = (value: string) => {
+ return localStorage.setItem(NUM_USERS_PER_PAGE_LOCAL_STORAGE_KEY, value);
+};
diff --git a/app/dashboard/src/vite-env.d.ts b/app/dashboard/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="vite/client" />
+/// <reference types="vite-plugin-svgr/client" />
diff --git a/app/dashboard/tsconfig.json b/app/dashboard/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "baseUrl": "./src"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
diff --git a/app/dashboard/tsconfig.node.json b/app/dashboard/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/app/dashboard/vite.config.ts b/app/dashboard/vite.config.ts
@@ -0,0 +1,18 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig, splitVendorChunkPlugin } from "vite";
+import svgr from "vite-plugin-svgr";
+import { visualizer } from "rollup-plugin-visualizer";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ tsconfigPaths(),
+ react({
+ include: "**/*.tsx",
+ }),
+ svgr(),
+ visualizer(),
+ splitVendorChunkPlugin(),
+ ],
+});
diff --git a/app/db/__init__.py b/app/db/__init__.py
@@ -0,0 +1,77 @@
+from sqlalchemy.exc import SQLAlchemyError
+from sqlalchemy.orm import Session
+from sqlalchemy.schema import MetaData
+
+from .base import Base, SessionLocal, engine # noqa
+
+# meta_obj = MetaData()
+
+class GetDB: # Context Manager
+ def __init__(self):
+ self.db = SessionLocal()
+
+ def __enter__(self):
+ return self.db
+
+ def __exit__(self, _, exc_value, traceback):
+ if isinstance(exc_value, SQLAlchemyError):
+ self.db.rollback() # rollback on exception
+
+ self.db.close()
+
+
+def get_db(): # Dependency
+ with GetDB() as db:
+ yield db
+
+
+from .crud import (create_admin, create_notification_reminder, # noqa
+ create_user, delete_notification_reminder, get_admin,
+ get_admins, get_jwt_secret_key, get_notification_reminder,
+ assure_node_inbounds, get_system_usage,
+ get_tls_certificate, get_user, get_user_by_id, get_users,
+ get_users_count, remove_admin, remove_user, revoke_user_sub,
+ set_owner, update_admin, update_user, update_user_status,
+ update_user_sub, start_user_expire)
+
+from .models import JWT, System, User # noqa
+
+# meta_obj.create_all(engine)
+
+__all__ = [
+ "get_or_create_inbound",
+ "get_user",
+ "get_user_by_id",
+ "get_users",
+ "get_users_count",
+ "create_user",
+ "remove_user",
+ "update_user",
+ "update_user_status",
+ "start_user_expire",
+ "update_user_sub",
+ "revoke_user_sub",
+ "set_owner",
+ "get_system_usage",
+ "get_jwt_secret_key",
+ "get_tls_certificate",
+ "get_admin",
+ "create_admin",
+ "update_admin",
+ "remove_admin",
+ "get_admins",
+
+ "create_notification_reminder",
+ "get_notification_reminder",
+ "delete_notification_reminder",
+
+ "GetDB",
+ "get_db",
+
+ "User",
+ "System",
+ "JWT",
+
+ "Base",
+ "Session",
+]
diff --git a/app/db/base.py b/app/db/base.py
@@ -0,0 +1,24 @@
+from config import SQLALCHEMY_DATABASE_URL
+from sqlalchemy import create_engine
+from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+IS_SQLITE = SQLALCHEMY_DATABASE_URL.startswith('sqlite')
+
+if IS_SQLITE:
+ engine = create_engine(
+ SQLALCHEMY_DATABASE_URL,
+ connect_args={"check_same_thread": False},
+ #echo=True
+ )
+else:
+ engine = create_engine(
+ SQLALCHEMY_DATABASE_URL,
+ pool_size=10,
+ max_overflow=30,
+ pool_recycle=3600,
+ pool_timeout=10
+ )
+
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+Base = declarative_base()
diff --git a/app/db/crud.py b/app/db/crud.py
@@ -0,0 +1,659 @@
+from datetime import datetime
+from enum import Enum
+from typing import Dict, List, Optional, Tuple, Union
+
+from sqlalchemy import and_, delete
+from sqlalchemy.orm import Session
+
+from app.db.models import (JWT, TLS, Admin, Node, NodeUsage, NodeUserUsage,
+ NotificationReminder, #Proxy,
+ InboundHost, Service,
+ Inbound, ProxyTypes, System, User,
+ UserUsageResetLogs)
+from app.models.admin import AdminCreate, AdminModify, AdminPartialModify
+from app.models.node import (NodeCreate, NodeModify, NodeStatus,
+ NodeUsageResponse)
+from app.models.proxy import InboundHost as InboundHostModify
+from app.models.user import (ReminderType, UserCreate,
+ UserDataLimitResetStrategy, UserModify,
+ UserResponse, UserStatus, UserUsageResponse)
+from app.models.service import Service as ServiceModify
+# from app.models.user_template import UserTemplateCreate, UserTemplateModify
+from app.utils.helpers import (calculate_expiration_days,
+ calculate_usage_percent)
+from app.utils.notification import Notification
+from config import NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT
+
+
+def add_default_hosts(db: Session, inbounds: List[Inbound]):
+ hosts = [InboundHost(remark="🚀 Marz ({USERNAME}) [{PROTOCOL} - {TRANSPORT}]", address="{SERVER_IP}", inbound=i) for i in inbounds]
+ db.add_all(hosts)
+ db.commit()
+
+
+def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
+ existing_inbounds = set((i.protocol, i.tag) for i in db.query(Inbound).filter(Inbound.node_id == node_id).all())
+ inbounds_set = set((i.protocol, i.tag) for i in inbounds)
+ # difference = [ i for i in existing_inbounds if (i.protocol, i.tag) not in existing_inbounds ]
+ if not inbounds_set.difference(existing_inbounds) and not existing_inbounds.difference(inbounds_set):
+ return
+ stmt = (
+ delete(Inbound)
+ .where( Inbound.node_id == node_id )
+ .execution_options(synchronize_session="fetch")
+ )
+ db.execute(stmt)
+ new_inbounds = [Inbound(tag=i.tag, protocol=i.protocol, node_id=node_id) for i in inbounds]
+ db.add_all(new_inbounds)
+ db.flush()
+ for i in new_inbounds:
+ db.refresh(i)
+ add_default_hosts(db, new_inbounds)
+ db.commit()
+
+
+def get_node_users(db: Session, statuses: List[UserStatus], node_id: Optional[int]):
+ query = db.query(User.id, User.username, User.key, Inbound.protocol, Inbound.tag).distinct(
+ ).join(Inbound.services).join(Service.users).filter(Inbound.node_id == node_id)
+ if statuses:
+ query = query.filter(User.status.in_(statuses))
+ return query.all()
+
+def get_user_hosts(db: Session, user_id: int):
+ return db.query(InboundHost).distinct().join(User.services).join(Service.inbounds).join(Inbound.hosts).filter( User.id == user_id ).all()
+
+def get_all_hosts(db: Session) -> Dict[int, InboundHost]:
+ all_inbounds = db.query(Inbound).all()
+ return {i.id: i.hosts for i in all_inbounds}
+
+def get_inbound_hosts(db: Session, inbound_id: int) -> List[InboundHost]:
+ return db.query(InboundHost).filter(InboundHost.inbound_id == inbound_id).all()
+
+def get_all_inbounds(db: Session):
+ return db.query(Inbound).all()
+
+"""
+def get_or_create_inbound(db: Session, inbound_tag: Inbound):
+ inbound = db.query(ProxyInbound).filter(ProxyInbound.tag == inbound_tag).first()
+ if not inbound:
+ inbound = ProxyInbound(tag=inbound_tag)
+ db.add(inbound)
+ db.commit()
+ add_default_host(db, inbound)
+ db.refresh(inbound)
+
+ return inbound
+
+
+def get_hosts(db: Session, inbound: Inbound):
+ inbound = get_or_create_inbound(db, inbound_tag)
+ return inbound.hosts
+
+"""
+
+def add_host(db: Session, inbound: Inbound, host: InboundHostModify):
+ # inbound = get_or_create_inbound(db, inbound_tag)
+ inbound.hosts.append(
+ InboundHost(
+ remark=host.remark,
+ address=host.address,
+ port=host.port,
+ path=host.path,
+ sni=host.sni,
+ host=host.host,
+ inbound=inbound,
+ security=host.security,
+ alpn=host.alpn,
+ fingerprint=host.fingerprint
+ )
+ )
+ db.commit()
+ db.refresh(inbound)
+ return inbound.hosts
+
+
+def update_hosts(db: Session, inbound_tag: str, modified_hosts: List[InboundHostModify]):
+ inbound = get_or_create_inbound(db, inbound_tag)
+ inbound.hosts = [
+ InboundHost(
+ remark=host.remark,
+ address=host.address,
+ port=host.port,
+ path=host.path,
+ sni=host.sni,
+ host=host.host,
+ inbound=inbound,
+ security=host.security,
+ alpn=host.alpn,
+ fingerprint=host.fingerprint,
+ allowinsecure=host.allowinsecure,
+ is_disabled=host.is_disabled
+ ) for host in modified_hosts
+ ]
+ db.commit()
+ db.refresh(inbound)
+ return inbound.hosts
+
+
+def get_user(db: Session, username: str):
+ return db.query(User).filter(User.username == username).first()
+
+
+def get_user_by_id(db: Session, user_id: int):
+ return db.query(User).filter(User.id == user_id).first()
+
+
+UsersSortingOptions = Enum('UsersSortingOptions', {
+ 'username': User.username.asc(),
+ 'used_traffic': User.used_traffic.asc(),
+ 'data_limit': User.data_limit.asc(),
+ 'expire': User.expire.asc(),
+ 'created_at': User.created_at.asc(),
+ '-username': User.username.desc(),
+ '-used_traffic': User.used_traffic.desc(),
+ '-data_limit': User.data_limit.desc(),
+ '-expire': User.expire.desc(),
+ '-created_at': User.created_at.desc(),
+})
+
+
+def get_users(db: Session,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ usernames: Optional[List[str]] = None,
+ status: Optional[Union[UserStatus, list]] = None,
+ sort: Optional[List[UsersSortingOptions]] = None,
+ admin: Optional[Admin] = None,
+ reset_strategy: Optional[Union[UserDataLimitResetStrategy, list]] = None,
+ return_with_count: bool = False) -> Union[List[User], Tuple[List[User], int]]:
+ query = db.query(User)
+
+ if usernames:
+ if len(usernames) == 1:
+ query = query.filter(User.username.ilike(f"%{usernames[0]}%"))
+ else:
+ query = query.filter(User.username.in_(usernames))
+
+ if status:
+ if isinstance(status, list):
+ query = query.filter(User.status.in_(status))
+ else:
+ query = query.filter(User.status == status)
+
+ if reset_strategy:
+ if isinstance(reset_strategy, list):
+ query = query.filter(User.data_limit_reset_strategy.in_(reset_strategy))
+ else:
+ query = query.filter(User.data_limit_reset_strategy == reset_strategy)
+
+ if admin:
+ query = query.filter(User.admin == admin)
+
+ # count it before applying limit and offset
+ if return_with_count:
+ count = query.count()
+
+ if sort:
+ query = query.order_by(*(opt.value for opt in sort))
+
+ if offset:
+ query = query.offset(offset)
+ if limit:
+ query = query.limit(limit)
+
+ if return_with_count:
+ return query.all(), count
+
+ return query.all()
+
+
+def get_user_usages(db: Session, dbuser: User, start: datetime, end: datetime,
+ ) -> List[UserUsageResponse]:
+ usages = {}
+
+ usages[0] = UserUsageResponse( # Main Core
+ node_id=None,
+ node_name="Master",
+ used_traffic=0
+ )
+ for node in db.query(Node).all():
+
+ usages[node.id] = UserUsageResponse(
+ node_id=node.id,
+ node_name=node.name,
+ used_traffic=0
+ )
+
+ cond = and_(NodeUserUsage.user_id == dbuser.id,
+ NodeUserUsage.created_at >= start,
+ NodeUserUsage.created_at <= end)
+
+ for v in db.query(NodeUserUsage).filter(cond):
+ try:
+ usages[v.node_id or 0].used_traffic += v.used_traffic
+ except KeyError:
+ pass
+
+ return list(usages.values())
+
+
+def get_users_count(db: Session, status: UserStatus = None, admin: Admin = None):
+ query = db.query(User.id)
+ if admin:
+ query = query.filter(User.admin == admin)
+ if status:
+ query = query.filter(User.status == status)
+ return query.count()
+
+
+def create_user(db: Session, user: UserCreate, admin: Admin = None):
+ """
+ excluded_inbounds_tags = user.excluded_inbounds
+ proxies = []
+ for proxy_type, settings in user.proxies.items():
+ excluded_inbounds = [
+ get_or_create_inbound(db, tag) for tag in excluded_inbounds_tags[proxy_type]
+ ]
+ proxies.append(
+ Proxy(type=proxy_type.value,
+ settings=settings.dict(no_obj=True),
+ excluded_inbounds=excluded_inbounds)
+ )
+ """
+
+ dbuser = User(
+ username=user.username,
+ # proxies=proxies,
+ key=user.key,
+ services=db.query(Service).filter(Service.id.in_(user.services)).all(),# user.services,
+ status=user.status,
+ data_limit=(user.data_limit or None),
+ expire=(user.expire or None),
+ admin=admin,
+ data_limit_reset_strategy=user.data_limit_reset_strategy,
+ note=user.note,
+ on_hold_expire_duration=(user.on_hold_expire_duration or None),
+ on_hold_timeout=(user.on_hold_timeout or None),
+ )
+ db.add(dbuser)
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def remove_user(db: Session, dbuser: User):
+ db.delete(dbuser)
+ # db.query(User).filter_by(id=user_id).delete()
+ db.commit()
+
+
+def update_user(db: Session, dbuser: User, modify: UserModify):
+ if modify.status is not None:
+ dbuser.status = modify.status
+
+ if modify.data_limit is not None:
+ dbuser.data_limit = (modify.data_limit or None)
+ if dbuser.status not in (UserStatus.expired, UserStatus.disabled):
+ if not dbuser.data_limit or dbuser.used_traffic < dbuser.data_limit:
+ if dbuser.status != UserStatus.on_hold:
+ dbuser.status = UserStatus.active
+
+ if not dbuser.data_limit or (calculate_usage_percent(
+ dbuser.used_traffic, dbuser.data_limit) < NOTIFY_REACHED_USAGE_PERCENT):
+ delete_notification_reminder_by_type(db, dbuser.id, ReminderType.data_usage)
+ else:
+ dbuser.status = UserStatus.limited
+
+ if modify.expire is not None:
+ dbuser.expire = (modify.expire or None)
+ if dbuser.status in (UserStatus.active, UserStatus.expired):
+ if not dbuser.expire or dbuser.expire > datetime.utcnow().timestamp():
+ dbuser.status = UserStatus.active
+ if not dbuser.expire or (calculate_expiration_days(
+ dbuser.expire) > NOTIFY_DAYS_LEFT):
+ delete_notification_reminder_by_type(db, dbuser.id, ReminderType.expiration_date)
+ else:
+ dbuser.status = UserStatus.expired
+
+ if modify.note is not None:
+ dbuser.note = modify.note or None
+
+ if modify.data_limit_reset_strategy is not None:
+ dbuser.data_limit_reset_strategy = modify.data_limit_reset_strategy.value
+
+ if modify.on_hold_timeout is not None:
+ dbuser.on_hold_timeout = modify.on_hold_timeout
+
+ if modify.on_hold_expire_duration is not None:
+ dbuser.on_hold_expire_duration = modify.on_hold_expire_duration
+
+ if modify.services is not None:
+ dbuser.services = db.query(Service).filter(Service.id.in_(modify.services)).all()
+ dbuser.edit_at = datetime.utcnow()
+
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def reset_user_data_usage(db: Session, dbuser: User):
+ usage_log = UserUsageResetLogs(
+ user=dbuser,
+ used_traffic_at_reset=dbuser.used_traffic,
+ )
+ db.add(usage_log)
+
+ dbuser.used_traffic = 0
+ dbuser.node_usages.clear()
+ if dbuser.status not in (UserStatus.expired or UserStatus.disabled):
+ dbuser.status = UserStatus.active.value
+ db.add(dbuser)
+
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def revoke_user_sub(db: Session, dbuser: User):
+ dbuser.sub_revoked_at = datetime.utcnow()
+
+ user = UserResponse.model_validate(dbuser)
+ for proxy_type, settings in user.proxies.copy().items():
+ settings.revoke()
+ user.proxies[proxy_type] = settings
+ dbuser = update_user(db, dbuser, user)
+
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def update_user_sub(db: Session, dbuser: User, user_agent: str):
+ dbuser.sub_updated_at = datetime.utcnow()
+ dbuser.sub_last_user_agent = user_agent
+
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def reset_all_users_data_usage(db: Session, admin: Optional[Admin] = None):
+ query = db.query(User)
+
+ if admin:
+ query = query.filter(User.admin == admin)
+
+ for dbuser in query.all():
+ dbuser.used_traffic = 0
+ if dbuser.status not in [UserStatus.on_hold, UserStatus.expired, UserStatus.disabled]:
+ dbuser.status = UserStatus.active
+ dbuser.usage_logs.clear()
+ dbuser.node_usages.clear()
+ db.add(dbuser)
+
+ db.commit()
+
+
+def update_user_status(db: Session, dbuser: User, status: UserStatus):
+ dbuser.status = status
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def set_owner(db: Session, dbuser: User, admin: Admin):
+ dbuser.admin = admin
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def start_user_expire(db: Session, dbuser: User):
+
+ expire = int(datetime.utcnow().timestamp()) + dbuser.on_hold_expire_duration
+ dbuser.expire = expire
+ db.commit()
+ db.refresh(dbuser)
+ return dbuser
+
+
+def get_system_usage(db: Session):
+ return db.query(System).first()
+
+
+def get_jwt_secret_key(db: Session):
+ return db.query(JWT).first().secret_key
+
+
+def get_tls_certificate(db: Session):
+ return db.query(TLS).first()
+
+
+def get_admin(db: Session, username: str):
+ return db.query(Admin).filter(Admin.username == username).first()
+
+
+def create_admin(db: Session, admin: AdminCreate):
+ dbadmin = Admin(
+ username=admin.username,
+ hashed_password=admin.hashed_password,
+ is_sudo=admin.is_sudo
+ )
+ db.add(dbadmin)
+ db.commit()
+ db.refresh(dbadmin)
+ return dbadmin
+
+
+def update_admin(db: Session, dbadmin: Admin, modified_admin: AdminModify):
+ dbadmin.is_sudo = modified_admin.is_sudo
+ if dbadmin.hashed_password != modified_admin.hashed_password:
+ dbadmin.hashed_password = modified_admin.hashed_password
+ dbadmin.password_reset_at = datetime.utcnow()
+ db.commit()
+ db.refresh(dbadmin)
+ return dbadmin
+
+
+def partial_update_admin(db: Session, dbadmin: Admin, modified_admin: AdminPartialModify):
+ if modified_admin.is_sudo is not None:
+ dbadmin.is_sudo = modified_admin.is_sudo
+ if modified_admin.password is not None and dbadmin.hashed_password != modified_admin.hashed_password:
+ dbadmin.hashed_password = modified_admin.hashed_password
+ dbadmin.password_reset_at = datetime.utcnow()
+
+ db.commit()
+ db.refresh(dbadmin)
+ return dbadmin
+
+
+def remove_admin(db: Session, dbadmin: Admin):
+ db.delete(dbadmin)
+ db.commit()
+ return dbadmin
+
+
+def get_admins(db: Session,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ username: Optional[str] = None):
+ query = db.query(Admin)
+ if username:
+ query = query.filter(Admin.username.ilike(f'%{username}%'))
+ if offset:
+ query = query.offset(offset)
+ if limit:
+ query = query.limit(limit)
+ return query.all()
+
+
+def create_service(db: Session, service: Service) -> Service:
+
+ #for _, i in service.inbounds.items():
+ # inbound_tags.extend(i)
+ dbservice = Service(
+ name=service.name,
+ inbounds=db.query(Inbound).filter(Inbound.id.in_(service.inbounds)).all(),
+ users=db.query(User).filter(User.id.in_(service.users)).all()
+ )
+ db.add(dbservice)
+ db.commit()
+ db.refresh(dbservice)
+ return dbservice
+
+
+def get_service(db: Session, service_name) -> Service:
+ return db.query(Service).filter( Service.name == service_name ).first()
+
+def get_services(db: Session) -> List[Service]:
+ return db.query(Service).all()
+
+def get_node(db: Session, name: str):
+ return db.query(Node).filter(Node.name == name).first()
+
+
+def get_node_by_id(db: Session, node_id: int):
+ return db.query(Node).filter(Node.id == node_id).first()
+
+
+def get_nodes(db: Session,
+ status: Optional[Union[NodeStatus, list]] = None,
+ enabled: bool = None):
+ query = db.query(Node)
+
+ if status:
+ if isinstance(status, list):
+ query = query.filter(Node.status.in_(status))
+ else:
+ query = query.filter(Node.status == status)
+
+ if enabled:
+ query = query.filter(Node.status != NodeStatus.disabled)
+
+ return query.all()
+
+
+def get_nodes_usage(db: Session, start: datetime, end: datetime) -> List[NodeUsageResponse]:
+ usages = {}
+
+ usages[0] = NodeUsageResponse( # Main Core
+ node_id=None,
+ node_name="Master",
+ uplink=0,
+ downlink=0
+ )
+ for node in db.query(Node).all():
+ usages[node.id] = NodeUsageResponse(
+ node_id=node.id,
+ node_name=node.name,
+ uplink=0,
+ downlink=0
+ )
+
+ cond = and_(NodeUsage.created_at >= start, NodeUsage.created_at <= end)
+
+ for v in db.query(NodeUsage).filter(cond):
+ try:
+ usages[v.node_id or 0].uplink += v.uplink
+ usages[v.node_id or 0].downlink += v.downlink
+ except KeyError:
+ pass
+
+ return list(usages.values())
+
+
+def create_node(db: Session, node: NodeCreate):
+ dbnode = Node(name=node.name,
+ address=node.address,
+ port=node.port,
+ api_port=node.api_port)
+
+ db.add(dbnode)
+ db.commit()
+ db.refresh(dbnode)
+ return dbnode
+
+
+def remove_node(db: Session, dbnode: Node):
+ db.delete(dbnode)
+ db.commit()
+ return dbnode
+
+
+def update_node(db: Session, dbnode: Node, modify: NodeModify):
+ if modify.name is not None:
+ dbnode.name = modify.name
+
+ if modify.address is not None:
+ dbnode.address = modify.address
+
+ if modify.port is not None:
+ dbnode.port = modify.port
+
+ if modify.api_port is not None:
+ dbnode.api_port = modify.api_port
+
+ if modify.status is NodeStatus.disabled:
+ dbnode.status = modify.status
+ dbnode.xray_version = None
+ dbnode.message = None
+ else:
+ dbnode.status = NodeStatus.connecting
+
+ if modify.usage_coefficient:
+ dbnode.usage_coefficient = modify.usage_coefficient
+
+ db.commit()
+ db.refresh(dbnode)
+ return dbnode
+
+
+def update_node_status(db: Session, dbnode: Node, status: NodeStatus, message: str = None, version: str = None):
+ dbnode.status = status
+ dbnode.message = message
+ dbnode.xray_version = version
+ dbnode.last_status_change = datetime.utcnow()
+ db.commit()
+ db.refresh(dbnode)
+ return dbnode
+
+
+def create_notification_reminder(
+ db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int) -> NotificationReminder:
+ reminder = NotificationReminder(type=reminder_type, expires_at=expires_at, user_id=user_id)
+ db.add(reminder)
+ db.commit()
+ db.refresh(reminder)
+ return reminder
+
+
+def get_notification_reminder(
+ db: Session, user_id: int, reminder_type: ReminderType,
+) -> Union[NotificationReminder, None]:
+ reminder = db.query(NotificationReminder).filter(
+ NotificationReminder.user_id == user_id).filter(
+ NotificationReminder.type == reminder_type).first()
+ if reminder is None:
+ return
+ if reminder.expires_at and reminder.expires_at < datetime.utcnow():
+ db.delete(reminder)
+ db.commit()
+ return
+ return reminder
+
+
+def delete_notification_reminder_by_type(db: Session, user_id: int, reminder_type: ReminderType) -> None:
+ """Deletes notification reminder filtered by user_id and type if exists"""
+ stmt = delete(NotificationReminder).where(
+ NotificationReminder.user_id == user_id,
+ NotificationReminder.type == reminder_type,
+ )
+ db.execute(stmt)
+ db.commit()
+ return
+
+
+def delete_notification_reminder(db: Session, dbreminder: NotificationReminder) -> None:
+ db.delete(dbreminder)
+ db.commit()
+ return
diff --git a/app/db/migrations/README b/app/db/migrations/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/app/db/migrations/env.py b/app/db/migrations/env.py
@@ -0,0 +1,82 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+from app.db.base import Base
+from config import SQLALCHEMY_DATABASE_URL
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL)
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/app/db/migrations/script.py.mako b/app/db/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/app/db/migrations/versions/20faa9f18c0a_init.py b/app/db/migrations/versions/20faa9f18c0a_init.py
@@ -0,0 +1,230 @@
+"""Initiate Marzneshin database
+
+Revision ID: 20faa9f18c0a
+Revises:
+Create Date: 2024-01-15 09:48:24.808505
+
+"""
+import os
+from alembic import op
+import sqlalchemy as sa
+from app.utils.crypto import generate_certificate
+
+# revision identifiers, used by Alembic.
+revision = '20faa9f18c0a'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('admins',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=32), nullable=True),
+ sa.Column('hashed_password', sa.String(length=128), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('is_sudo', sa.Boolean(), nullable=True),
+ sa.Column('password_reset_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_admins_id'), 'admins', ['id'], unique=False)
+ op.create_index(op.f('ix_admins_username'), 'admins', ['username'], unique=True)
+
+ jwttable = op.create_table('jwt',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('secret_key', sa.String(length=64), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.bulk_insert(jwttable, [{"id": 1, "secret_key": os.urandom(32).hex()}])
+
+ op.create_table('nodes',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=256, collation='NOCASE'), nullable=True),
+ sa.Column('address', sa.String(length=256), nullable=False),
+ sa.Column('port', sa.Integer(), nullable=False),
+ sa.Column('api_port', sa.Integer(), nullable=False),
+ sa.Column('xray_version', sa.String(length=32), nullable=True),
+ sa.Column('status', sa.Enum('connected', 'connecting', 'error', 'disabled', name='nodestatus'), nullable=False),
+ sa.Column('last_status_change', sa.DateTime(), nullable=True),
+ sa.Column('message', sa.String(length=1024), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('uplink', sa.BigInteger(), nullable=True),
+ sa.Column('downlink', sa.BigInteger(), nullable=True),
+ sa.Column('usage_coefficient', sa.Float(), server_default=sa.text('(1.0)'), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('name')
+ )
+ op.create_index(op.f('ix_nodes_id'), 'nodes', ['id'], unique=False)
+ op.create_table('services',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=64), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_services_id'), 'services', ['id'], unique=False)
+ op.create_index(op.f('ix_services_name'), 'services', ['name'], unique=False)
+
+ systable = op.create_table('system',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('uplink', sa.BigInteger(), nullable=True),
+ sa.Column('downlink', sa.BigInteger(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.bulk_insert(systable, [{"id": 1, "uplink": 0, "downlink": 0}])
+ op.create_index(op.f('ix_system_id'), 'system', ['id'], unique=False)
+
+ tlstable = op.create_table('tls',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('key', sa.String(length=4096), nullable=False),
+ sa.Column('certificate', sa.String(length=2048), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ tls = generate_certificate()
+ op.bulk_insert(tlstable, [{"id": 1, "key": tls['key'], "certificate": tls['cert']}])
+
+ op.create_table('inbounds',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('protocol', sa.String(), nullable=True),
+ sa.Column('tag', sa.String(length=256), nullable=False),
+ sa.Column('node_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_inbounds_node_id'), 'inbounds', ['node_id'], unique=False)
+ op.create_index(op.f('ix_inbounds_tag'), 'inbounds', ['tag'], unique=False)
+ op.create_table('node_usages',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=False),
+ sa.Column('node_id', sa.Integer(), nullable=True),
+ sa.Column('uplink', sa.BigInteger(), nullable=True),
+ sa.Column('downlink', sa.BigInteger(), nullable=True),
+ sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('created_at', 'node_id')
+ )
+ op.create_index(op.f('ix_node_usages_id'), 'node_usages', ['id'], unique=False)
+ op.create_table('users',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=32, collation='NOCASE'), nullable=True),
+ sa.Column('key', sa.String(length=64), nullable=True),
+ sa.Column('status', sa.Enum('active', 'disabled', 'limited', 'expired', 'on_hold', name='userstatus'), nullable=False),
+ sa.Column('used_traffic', sa.BigInteger(), nullable=True),
+ sa.Column('data_limit', sa.BigInteger(), nullable=True),
+ sa.Column('data_limit_reset_strategy', sa.Enum('no_reset', 'day', 'week', 'month', 'year', name='userdatalimitresetstrategy'), nullable=False),
+ sa.Column('ip_limit', sa.Integer(), nullable=False),
+ sa.Column('settings', sa.String(), nullable=True),
+ sa.Column('expire', sa.Integer(), nullable=True),
+ sa.Column('admin_id', sa.Integer(), nullable=True),
+ sa.Column('sub_revoked_at', sa.DateTime(), nullable=True),
+ sa.Column('sub_updated_at', sa.DateTime(), nullable=True),
+ sa.Column('sub_last_user_agent', sa.String(length=512), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('note', sa.String(length=500), nullable=True),
+ sa.Column('online_at', sa.DateTime(), nullable=True),
+ sa.Column('on_hold_expire_duration', sa.BigInteger(), nullable=True),
+ sa.Column('on_hold_timeout', sa.DateTime(), nullable=True),
+ sa.Column('edit_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['admin_id'], ['admins.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
+ op.create_index(op.f('ix_users_key'), 'users', ['key'], unique=True)
+ op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
+ op.create_table('hosts',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('remark', sa.String(length=256), nullable=False),
+ sa.Column('address', sa.String(length=256), nullable=False),
+ sa.Column('port', sa.Integer(), nullable=True),
+ sa.Column('path', sa.String(length=256), nullable=True),
+ sa.Column('sni', sa.String(length=256), nullable=True),
+ sa.Column('host', sa.String(length=256), nullable=True),
+ sa.Column('security', sa.Enum('inbound_default', 'none', 'tls', name='inboundhostsecurity'), nullable=False),
+ sa.Column('alpn', sa.Enum('none', 'h2', 'http/1.1', 'h2,http/1.1', name='proxyhostalpn'), server_default='none', nullable=False),
+ sa.Column('fingerprint', sa.Enum('none', 'chrome', 'firefox', 'safari', 'ios', 'android', 'edge', '360', 'qq', 'random', 'randomized', name='proxyhostfingerprint'), server_default='none', nullable=False),
+ sa.Column('inbound_id', sa.Integer(), nullable=False),
+ sa.Column('allowinsecure', sa.Boolean(), nullable=True),
+ sa.Column('is_disabled', sa.Boolean(), nullable=True),
+ sa.ForeignKeyConstraint(['inbound_id'], ['inbounds.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('inbounds_services',
+ sa.Column('inbound_id', sa.Integer(), nullable=False),
+ sa.Column('service_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['inbound_id'], ['inbounds.id'], ),
+ sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
+ sa.PrimaryKeyConstraint('inbound_id', 'service_id')
+ )
+ op.create_table('node_user_usages',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=True),
+ sa.Column('node_id', sa.Integer(), nullable=True),
+ sa.Column('used_traffic', sa.BigInteger(), nullable=True),
+ sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('created_at', 'user_id', 'node_id')
+ )
+ op.create_index(op.f('ix_node_user_usages_id'), 'node_user_usages', ['id'], unique=False)
+ op.create_table('notification_reminders',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=True),
+ sa.Column('type', sa.Enum('expiration_date', 'data_usage', name='remindertype'), nullable=False),
+ sa.Column('expires_at', sa.DateTime(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_notification_reminders_id'), 'notification_reminders', ['id'], unique=False)
+ op.create_table('user_usage_logs',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=True),
+ sa.Column('used_traffic_at_reset', sa.BigInteger(), nullable=False),
+ sa.Column('reset_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_user_usage_logs_id'), 'user_usage_logs', ['id'], unique=False)
+ op.create_table('users_services',
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('service_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('user_id', 'service_id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('users_services')
+ op.drop_index(op.f('ix_user_usage_logs_id'), table_name='user_usage_logs')
+ op.drop_table('user_usage_logs')
+ op.drop_index(op.f('ix_notification_reminders_id'), table_name='notification_reminders')
+ op.drop_table('notification_reminders')
+ op.drop_index(op.f('ix_node_user_usages_id'), table_name='node_user_usages')
+ op.drop_table('node_user_usages')
+ op.drop_table('inbounds_services')
+ op.drop_table('hosts')
+ op.drop_index(op.f('ix_users_username'), table_name='users')
+ op.drop_index(op.f('ix_users_key'), table_name='users')
+ op.drop_index(op.f('ix_users_id'), table_name='users')
+ op.drop_table('users')
+ op.drop_index(op.f('ix_node_usages_id'), table_name='node_usages')
+ op.drop_table('node_usages')
+ op.drop_index(op.f('ix_inbounds_tag'), table_name='inbounds')
+ op.drop_index(op.f('ix_inbounds_node_id'), table_name='inbounds')
+ op.drop_table('inbounds')
+ op.drop_table('tls')
+ op.drop_index(op.f('ix_system_id'), table_name='system')
+ op.drop_table('system')
+ op.drop_index(op.f('ix_services_name'), table_name='services')
+ op.drop_index(op.f('ix_services_id'), table_name='services')
+ op.drop_table('services')
+ op.drop_index(op.f('ix_nodes_id'), table_name='nodes')
+ op.drop_table('nodes')
+ op.drop_table('jwt')
+ op.drop_index(op.f('ix_admins_username'), table_name='admins')
+ op.drop_index(op.f('ix_admins_id'), table_name='admins')
+ op.drop_table('admins')
+ # ### end Alembic commands ###
diff --git a/app/db/models.py b/app/db/models.py
@@ -0,0 +1,263 @@
+import os
+from datetime import datetime
+
+from sqlalchemy import (BigInteger, Boolean, Column, DateTime, Enum,
+ Float, ForeignKey, Integer, String, Table,
+ UniqueConstraint)
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql.expression import text
+
+from app import xray
+from app.db.base import Base
+from app.models.node import NodeStatus
+from app.models.proxy import (InboundHostALPN, InboundHostFingerprint,
+ InboundHostSecurity, ProxyTypes)
+from app.models.user import (ReminderType, UserDataLimitResetStrategy,
+ UserStatus)
+
+
+class Admin(Base):
+ __tablename__ = "admins"
+
+ id = Column(Integer, primary_key=True, index=True)
+ username = Column(String(32), unique=True, index=True)
+ hashed_password = Column(String(128))
+ users = relationship("User", back_populates="admin")
+ created_at = Column(DateTime, default=datetime.utcnow)
+ is_sudo = Column(Boolean, default=False)
+ password_reset_at = Column(DateTime, nullable=True)
+
+
+inbounds_services = Table(
+ "inbounds_services",
+ Base.metadata,
+ Column("inbound_id", ForeignKey("inbounds.id"), primary_key=True),
+ Column("service_id", ForeignKey("services.id"), primary_key=True),
+)
+
+users_services = Table(
+ "users_services",
+ Base.metadata,
+ Column("user_id", ForeignKey("users.id"), primary_key=True),
+ Column("service_id", ForeignKey("services.id"), primary_key=True),
+)
+
+
+class Service(Base):
+ __tablename__ = "services"
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(64), index=True)
+ # modifications = Column(String(1024), nullable=True, default=None)
+ users = relationship("User", secondary=users_services, back_populates="services")
+ inbounds = relationship("Inbound", secondary=inbounds_services, back_populates="services")
+
+
+class User(Base):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+ username = Column(String(32, collation='NOCASE'), unique=True, index=True)
+ key = Column(String(64), unique=True, index=True)
+ services = relationship("Service", secondary=users_services, back_populates="users", lazy="joined")
+ inbounds = relationship("Inbound", secondary="join(users_services, inbounds_services, inbounds_services.c.service_id == users_services.c.service_id).join(Inbound, Inbound.id == inbounds_services.c.inbound_id)", viewonly=True, distinct_target_key=True, lazy="joined")
+ # proxies = relationship("Proxy", back_populates="user", cascade="all, delete-orphan")
+ status = Column(Enum(UserStatus), nullable=False, default=UserStatus.active)
+ used_traffic = Column(BigInteger, default=0)
+ node_usages = relationship("NodeUserUsage", back_populates="user", cascade="all,delete,delete-orphan", lazy="joined")
+ notification_reminders = relationship("NotificationReminder", back_populates="user", cascade="all,delete,delete-orphan")
+ data_limit = Column(BigInteger, nullable=True)
+ data_limit_reset_strategy = Column(
+ Enum(UserDataLimitResetStrategy),
+ nullable=False,
+ default=UserDataLimitResetStrategy.no_reset,
+ )
+ ip_limit = Column(Integer, nullable=False, default=-1)
+ settings = Column(String, nullable=True, default=None)
+ usage_logs = relationship("UserUsageResetLogs", back_populates="user", lazy="joined")
+ expire = Column(Integer, nullable=True)
+ admin_id = Column(Integer, ForeignKey("admins.id"))
+ admin = relationship("Admin", back_populates="users")
+ sub_revoked_at = Column(DateTime, nullable=True, default=None)
+ sub_updated_at = Column(DateTime, nullable=True, default=None)
+ sub_last_user_agent = Column(String(512), nullable=True, default=None)
+ created_at = Column(DateTime, default=datetime.utcnow)
+ note = Column(String(500), nullable=True, default=None)
+ online_at = Column(DateTime, nullable=True, default=None)
+ on_hold_expire_duration = Column(BigInteger, nullable=True, default=None)
+ on_hold_timeout = Column(DateTime, nullable=True, default=None)
+ edit_at = Column(DateTime, nullable=True, default=None)
+
+ @property
+ def lifetime_used_traffic(self):
+ return (
+ sum([log.used_traffic_at_reset for log in self.usage_logs])
+ + self.used_traffic
+ )
+
+ @property
+ def last_traffic_reset_time(self):
+ return self.usage_logs[-1].reset_at if self.usage_logs else self.created_at
+ """
+ @property
+ def inbounds(self):
+ _ = {}
+ for proxy in self.proxies:
+ _[proxy.type] = []
+ excluded_tags = [i.tag for i in proxy.excluded_inbounds]
+ for inbound in xray.config.inbounds_by_protocol.get(proxy.type, []):
+ if inbound["tag"] not in excluded_tags:
+ _[proxy.type].append(inbound["tag"])
+
+ return _
+ """
+
+
+
+class UserUsageResetLogs(Base):
+ __tablename__ = "user_usage_logs"
+
+ id = Column(Integer, primary_key=True, index=True)
+ user_id = Column(Integer, ForeignKey("users.id"))
+ user = relationship("User", back_populates="usage_logs")
+ used_traffic_at_reset = Column(BigInteger, nullable=False)
+ reset_at = Column(DateTime, default=datetime.utcnow)
+
+
+
+class Inbound(Base):
+ __tablename__ = "inbounds"
+
+ id = Column(Integer, primary_key=True)
+ protocol = Column(Enum(ProxyTypes))
+ tag = Column(String(256), nullable=False, index=True)
+ node_id = Column(Integer, ForeignKey("nodes.id"), index=True)
+ node = relationship("Node", back_populates="inbounds")
+ services = relationship("Service", secondary=inbounds_services, back_populates="inbounds")
+ hosts = relationship(
+ "InboundHost", back_populates="inbound", cascade="all, delete, delete-orphan"
+ )
+
+
+class InboundHost(Base):
+ __tablename__ = "hosts"
+ # __table_args__ = (
+ # UniqueConstraint('inbound_tag', 'remark'),
+ # )
+
+ id = Column(Integer, primary_key=True)
+ remark = Column(String(256), unique=False, nullable=False)
+ address = Column(String(256), unique=False, nullable=False)
+ port = Column(Integer, nullable=True)
+ path = Column(String(256), unique=False, nullable=True)
+ sni = Column(String(256), unique=False, nullable=True)
+ host = Column(String(256), unique=False, nullable=True)
+ security = Column(
+ Enum(InboundHostSecurity),
+ unique=False,
+ nullable=False,
+ default=InboundHostSecurity.inbound_default,
+ )
+ alpn = Column(
+ Enum(InboundHostALPN),
+ unique=False,
+ nullable=False,
+ default=InboundHostSecurity.none,
+ server_default=InboundHostSecurity.none.name
+ )
+ fingerprint = Column(
+ Enum(InboundHostFingerprint),
+ unique=False,
+ nullable=False,
+ default=InboundHostSecurity.none,
+ server_default=InboundHostSecurity.none.name
+ )
+
+ inbound_id = Column(Integer, ForeignKey("inbounds.id"), nullable=False)
+ inbound = relationship("Inbound", back_populates="hosts")
+ allowinsecure = Column(Boolean, nullable=True)
+ is_disabled = Column(Boolean, nullable=True, default=False)
+
+
+class System(Base):
+ __tablename__ = "system"
+
+ id = Column(Integer, primary_key=True, index=True)
+ uplink = Column(BigInteger, default=0)
+ downlink = Column(BigInteger, default=0)
+
+
+class JWT(Base):
+ __tablename__ = "jwt"
+
+ id = Column(Integer, primary_key=True)
+ secret_key = Column(
+ String(64), nullable=False, default=lambda: os.urandom(32).hex()
+ )
+
+
+class TLS(Base):
+ __tablename__ = "tls"
+
+ id = Column(Integer, primary_key=True)
+ key = Column(String(4096), nullable=False)
+ certificate = Column(String(2048), nullable=False)
+
+
+class Node(Base):
+ __tablename__ = "nodes"
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(256, collation='NOCASE'), unique=True)
+ address = Column(String(256), unique=False, nullable=False)
+ port = Column(Integer, unique=False, nullable=False)
+ api_port = Column(Integer, unique=False, nullable=False)
+ xray_version = Column(String(32), nullable=True)
+ inbounds = relationship("Inbound", back_populates="node")
+ status = Column(Enum(NodeStatus), nullable=False, default=NodeStatus.connecting)
+ last_status_change = Column(DateTime, default=datetime.utcnow)
+ message = Column(String(1024), nullable=True)
+ created_at = Column(DateTime, default=datetime.utcnow)
+ uplink = Column(BigInteger, default=0)
+ downlink = Column(BigInteger, default=0)
+ user_usages = relationship("NodeUserUsage", back_populates="node", cascade="all, delete, delete-orphan")
+ usages = relationship("NodeUsage", back_populates="node", cascade="all, delete, delete-orphan")
+ usage_coefficient = Column(Float, nullable=False, server_default=text("1.0"), default=1)
+
+
+class NodeUserUsage(Base):
+ __tablename__ = "node_user_usages"
+ __table_args__ = (
+ UniqueConstraint('created_at', 'user_id', 'node_id'),
+ )
+
+ id = Column(Integer, primary_key=True, index=True)
+ created_at = Column(DateTime, unique=False, nullable=False) # one hour per record
+ user_id = Column(Integer, ForeignKey("users.id"))
+ user = relationship("User", back_populates="node_usages")
+ node_id = Column(Integer, ForeignKey("nodes.id"))
+ node = relationship("Node", back_populates="user_usages")
+ used_traffic = Column(BigInteger, default=0)
+
+
+class NodeUsage(Base):
+ __tablename__ = "node_usages"
+ __table_args__ = (
+ UniqueConstraint('created_at', 'node_id'),
+ )
+
+ id = Column(Integer, primary_key=True, index=True)
+ created_at = Column(DateTime, unique=False, nullable=False) # one hour per record
+ node_id = Column(Integer, ForeignKey("nodes.id"))
+ node = relationship("Node", back_populates="usages")
+ uplink = Column(BigInteger, default=0)
+ downlink = Column(BigInteger, default=0)
+
+
+class NotificationReminder(Base):
+ __tablename__ = "notification_reminders"
+
+ id = Column(Integer, primary_key=True, index=True)
+ user_id = Column(Integer, ForeignKey("users.id"))
+ user = relationship("User", back_populates="notification_reminders")
+ type = Column(Enum(ReminderType), nullable=False)
+ expires_at = Column(DateTime, nullable=True)
+ created_at = Column(DateTime, default=datetime.utcnow)
diff --git a/app/discord/__init__.py b/app/discord/__init__.py
@@ -0,0 +1,21 @@
+handler_names = ["report"]
+
+from .handlers.report import ( # noqa
+ send_webhook,
+ report_new_user,
+ report_user_modification,
+ report_user_deletion,
+ report_status_change,
+ report_user_usage_reset,
+ report_user_subscription_revoked
+)
+
+__all__ = [
+ "send_webhook",
+ "report_new_user",
+ "report_user_modification",
+ "report_user_deletion",
+ "report_status_change",
+ "report_user_usage_reset",
+ "report_user_subscription_revoked"
+]
diff --git a/app/discord/handlers/report.py b/app/discord/handlers/report.py
@@ -0,0 +1,133 @@
+import requests
+from datetime import datetime
+from app.utils.system import readable_size
+from telebot.formatting import escape_html
+from app import logger
+from config import DISCORD_WEBHOOK_URL
+
+
+def send_webhook(json_data):
+ if DISCORD_WEBHOOK_URL:
+ result = requests.post(DISCORD_WEBHOOK_URL, json=json_data)
+
+ try:
+ result.raise_for_status()
+ except requests.exceptions.HTTPError as err:
+ logger.error(err)
+ else:
+ logger.debug("Discord payload delivered successfully, code {}.".format(result.status_code))
+
+def report_status_change(username: str, status: str):
+ _status = {
+ 'active': '**:white_check_mark: Activated**',
+ 'disabled': '**:x: Disabled**',
+ 'limited': '**:low_battery: #Limited**',
+ 'expired': '**:clock5: #Expired**'
+ }
+ _status_color = {
+ 'active': int("9ae6b4", 16),
+ 'disabled': int("424b59", 16),
+ 'limited': int("f8a7a8", 16),
+ 'expired': int("fbd38d", 16)
+ }
+ statusChange = {
+ "content": "",
+ "embeds": [
+ {
+ "description": f"{_status[status]}\n----------------------\n**Username:** {username}",
+ "color": _status_color[status]
+ }
+ ],
+ }
+ send_webhook(statusChange)
+
+def report_new_user(user_id: int, username: str, by: str, expire_date: int, data_limit: int, proxies: list):
+
+ data_limit=readable_size(data_limit) if data_limit else "Unlimited"
+ expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never"
+ proxies="" if not proxies else ", ".join([escape_html(proxy) for proxy in proxies])
+
+ reportNewUser = {
+ 'content': '',
+ 'embeds': [
+ {
+ "title": ":new: Created",
+ "description": f"**Username:** {username}\n**Traffic Limit:** {data_limit}\n**Expire Date:** {expire_date}\n**Proxies:** {proxies}",
+ "footer": {
+ "text": f"By {by}"
+ },
+ "color": int("00ff00", 16)
+ }
+ ]
+ }
+ send_webhook(reportNewUser)
+
+def report_user_modification(username: str, expire_date: int, data_limit: int, proxies: list, by: str):
+
+ data_limit=readable_size(data_limit) if data_limit else "Unlimited"
+ expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never"
+ proxies="" if not proxies else ", ".join([escape_html(proxy) for proxy in proxies])
+ protocols = proxies
+
+ reportUserModification = {
+ 'content': '',
+ 'embeds': [
+ {
+ 'title': ':pencil2: Modified',
+ 'description': f'**Username:** {username}\n**Traffic Limit:** {data_limit}\n**Expire Date:** {expire_date}\n**Protocols:** {protocols}',
+ 'footer': {
+ 'text': f'By {by}'
+ },
+ 'color': int("00ffff", 16)
+ }
+ ]
+ }
+ send_webhook(reportUserModification)
+
+def report_user_deletion(username: str, by: str):
+ userDeletion = {
+ 'content': '',
+ 'embeds': [
+ {
+ 'title': ':wastebasket: Deleted',
+ 'description': f'**Username: **{username}',
+ 'footer': {
+ 'text': f'By {by}'
+ },
+ 'color': int("ff0000", 16)
+ }
+ ]
+ }
+ send_webhook(userDeletion)
+
+def report_user_usage_reset(username: str, by: str):
+ userUsageReset = {
+ 'content': '',
+ 'embeds': [
+ {
+ 'title': ':repeat: Reset',
+ 'description': f'**Username:** {username}',
+ 'footer': {
+ 'text': f'By {by}'
+ },
+ 'color': int('00ffff', 16)
+ }
+ ]
+ }
+ send_webhook(userUsageReset)
+
+def report_user_subscription_revoked(username: str, by: str):
+ subscriptionRevoked = {
+ 'content': '',
+ 'embeds': [
+ {
+ 'title': ':repeat: Revoked',
+ 'description': f'**Username:** {username}',
+ 'footer': {
+ 'text': f'By {by}'
+ },
+ 'color': int('ff0000', 16)
+ }
+ ]
+ }
+ send_webhook(subscriptionRevoked)
diff --git a/app/jobs/0_start_xray.py b/app/jobs/0_start_xray.py
@@ -0,0 +1,26 @@
+import traceback
+import asyncio
+
+from app import app, logger, scheduler, xray
+
+
+async def core_health_check():
+ while True:
+ if not xray.core.started:
+ await xray.core.restart(xray.configs[0].include_db_users(node_id=0))
+ await asyncio.sleep(15)
+
+
+@app.on_event("startup")
+async def app_startup():
+ logger.info('Starting Xray core')
+ try:
+ await xray.core.start(xray.configs[0].include_db_users(node_id=0))
+ except Exception:
+ traceback.print_exc()
+ asyncio.get_event_loop().create_task(core_health_check())
+
+
+@app.on_event("shutdown")
+async def app_shutdown():
+ await xray.core.stop()
diff --git a/app/jobs/1_start_nodes.py b/app/jobs/1_start_nodes.py
@@ -0,0 +1,41 @@
+import json
+
+from app import app, logger, scheduler, xray
+from app.db import GetDB, crud
+from app.models.node import NodeStatus
+from xray_api import exc as xray_exc
+
+
+async def nodes_health_check():
+ for node_id, node in list(xray.nodes.items()):
+ if node.started and await node.is_healthy():
+ try:
+ await node.api.get_sys_stats()
+ except (ConnectionError, xray_exc.ConnectionError, xray_exc.UnknownError):
+ await xray.operations.restart_node(node_id, xray.configs[node_id].include_db_users(node_id))
+
+ if not await node.is_healthy():
+ await xray.operations.start_node(node_id, xray.configs[node_id].include_db_users(node_id))
+
+
+@app.on_event("startup")
+async def app_startup():
+ with GetDB() as db:
+ dbnodes = crud.get_nodes(db=db, enabled=True)
+ node_ids = [dbnode.id for dbnode in dbnodes]
+ for dbnode in dbnodes:
+ crud.update_node_status(db, dbnode, NodeStatus.connecting)
+
+ for node_id in node_ids:
+ await xray.operations.start_node(node_id, xray.configs[node_id].include_db_users(node_id))
+
+ scheduler.add_job(nodes_health_check, 'interval', seconds=20, coalesce=True, max_instances=1)
+
+
+@app.on_event("shutdown")
+async def app_shutdown():
+ for node in list(xray.nodes.values()):
+ try:
+ await node.stop()
+ except Exception:
+ pass
diff --git a/app/jobs/__init__.py b/app/jobs/__init__.py
@@ -0,0 +1,13 @@
+import glob
+import importlib.util
+from os.path import basename, dirname, join
+
+modules = glob.glob(join(dirname(__file__), "*.py"))
+
+for file in modules:
+ name = basename(file).replace('.py', '')
+ if name.startswith('_'):
+ continue
+
+ spec = importlib.util.spec_from_file_location(name, file)
+ spec.loader.exec_module(importlib.util.module_from_spec(spec))
diff --git a/app/jobs/record_usages.py b/app/jobs/record_usages.py
@@ -0,0 +1,202 @@
+import asyncio
+from collections import defaultdict
+# from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime
+from operator import attrgetter
+from typing import Union
+
+from pymysql.err import OperationalError
+from sqlalchemy import and_, bindparam, insert, select, sql, update
+
+from app import scheduler, xray
+from app.db import GetDB
+from app.db.models import NodeUsage, NodeUserUsage, System, User
+from config import DISABLE_RECORDING_NODE_USAGE
+from xray_api import XRay as XRayAPI
+from xray_api import exc as xray_exc
+
+
+def safe_execute(db, stmt, params=None):
+ if db.bind.name == 'mysql':
+ if isinstance(stmt, sql.dml.Insert):
+ stmt = stmt.prefix_with('IGNORE')
+
+ tries = 0
+ done = False
+ while not done:
+ try:
+ db.execute(stmt, params)
+ db.commit()
+ done = True
+ except OperationalError as err:
+ if err.args[0] == 1213 and tries < 3: # Deadlock
+ db.rollback()
+ tries += 1
+ continue
+ raise err
+
+ else:
+ db.execute(stmt, params)
+ db.commit()
+
+
+def record_user_stats(params: list, node_id: Union[int, None],
+ consumption_factor: int = 1):
+ if not params:
+ return
+
+ created_at = datetime.fromisoformat(datetime.utcnow().strftime('%Y-%m-%dT%H:00:00'))
+
+ with GetDB() as db:
+ # make user usage row if doesn't exist
+ select_stmt = select(NodeUserUsage.user_id) \
+ .where(and_(NodeUserUsage.node_id == node_id, NodeUserUsage.created_at == created_at))
+ existings = [r[0] for r in db.execute(select_stmt).fetchall()]
+ uids_to_insert = set()
+
+ for p in params:
+ uid = int(p['uid'])
+ if uid in existings:
+ continue
+ uids_to_insert.add(uid)
+
+ if uids_to_insert:
+ stmt = insert(NodeUserUsage).values(
+ user_id=bindparam('uid'),
+ created_at=created_at,
+ node_id=node_id,
+ used_traffic=0
+ )
+ safe_execute(db, stmt, [{'uid': uid} for uid in uids_to_insert])
+
+ # record
+ stmt = update(NodeUserUsage) \
+ .values(used_traffic=NodeUserUsage.used_traffic + bindparam('value') * consumption_factor) \
+ .where(and_(NodeUserUsage.user_id == bindparam('uid'),
+ NodeUserUsage.node_id == node_id,
+ NodeUserUsage.created_at == created_at))
+ safe_execute(db, stmt, params)
+
+
+def record_node_stats(params: dict, node_id: Union[int, None]):
+ if not params:
+ return
+
+ created_at = datetime.fromisoformat(datetime.utcnow().strftime('%Y-%m-%dT%H:00:00'))
+
+ with GetDB() as db:
+
+ # make node usage row if doesn't exist
+ select_stmt = select(NodeUsage.node_id). \
+ where(and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at))
+ notfound = db.execute(select_stmt).first() is None
+ if notfound:
+ stmt = insert(NodeUsage).values(created_at=created_at, node_id=node_id, uplink=0, downlink=0)
+ safe_execute(db, stmt)
+
+ # record
+ stmt = update(NodeUsage). \
+ values(uplink=NodeUsage.uplink + bindparam('up'), downlink=NodeUsage.downlink + bindparam('down')). \
+ where(and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at))
+
+ safe_execute(db, stmt, params)
+
+
+async def get_users_stats(api: XRayAPI):
+ try:
+ params = defaultdict(int)
+ for stat in filter(attrgetter('value'), await api.get_users_stats(reset=True)):
+ params[stat.name.split('.', 1)[0]] += stat.value
+ params = list({"uid": uid, "value": value} for uid, value in params.items())
+ return params
+ except (xray_exc.ConnectionError, xray_exc.UnknownError):
+ return []
+
+
+async def get_outbounds_stats(api: XRayAPI):
+ try:
+ params = [{"up": stat.value, "down": 0} if stat.link == "uplink" else {"up": 0, "down": stat.value}
+ for stat in filter(attrgetter('value'), await api.get_outbounds_stats(reset=True))]
+ return params
+ except (xray_exc.ConnectionError, xray_exc.UnknownError):
+ return []
+
+
+async def record_user_usages():
+ api_instances = [(None, xray.api)]
+ usage_coefficient = {None: 1} # default usage coefficient for the main api instance
+
+ for node_id, node in list(xray.nodes.items()):
+ if node.started and await node.is_healthy():
+ api_instances.append((node_id, node.api))
+ usage_coefficient[node_id] = node.usage_coefficient # fetch the usage coefficient
+
+ results = await asyncio.gather(
+ *[get_users_stats(api[1]) for api in api_instances]
+ )
+ api_params = {api_instances[i][0]: results[i] for i in range(len(api_instances))}
+
+ users_usage = defaultdict(int)
+ for node_id, params in api_params.items():
+ coefficient = usage_coefficient.get(node_id, 1) # get the usage coefficient for the node
+ for param in params:
+ users_usage[param['uid']] += param['value'] * coefficient # apply the usage coefficient
+
+ users_usage = list({"uid": uid, "value": value} for uid, value in users_usage.items())
+ if not users_usage:
+ return
+
+ # record users usage
+ with GetDB() as db:
+ stmt = update(User). \
+ where(User.id == bindparam('uid')). \
+ values(
+ used_traffic=User.used_traffic + bindparam('value'),
+ online_at=datetime.utcnow()
+ )
+
+ safe_execute(db, stmt, users_usage)
+
+ if DISABLE_RECORDING_NODE_USAGE:
+ return
+
+ for node_id, params in api_params.items():
+ record_user_stats(params, node_id, usage_coefficient[node_id])
+
+
+async def record_node_usages():
+ api_instances = [(None, xray.api)]
+ for node_id, node in list(xray.nodes.items()):
+ if node.started and await node.is_healthy():
+ api_instances.append((node_id, node.api))
+ results = await asyncio.gather(
+ *[get_outbounds_stats(api[1]) for api in api_instances]
+ )
+ api_params = {api_instances[i][0]: results[i] for i in range(len(api_instances))}
+
+ total_up = 0
+ total_down = 0
+ for node_id, params in api_params.items():
+ for param in params:
+ total_up += param['up']
+ total_down += param['down']
+ if not (total_up or total_down):
+ return
+
+ # record nodes usage
+ with GetDB() as db:
+ stmt = update(System).values(
+ uplink=System.uplink + total_up,
+ downlink=System.downlink + total_down
+ )
+ safe_execute(db, stmt)
+
+ if DISABLE_RECORDING_NODE_USAGE:
+ return
+
+ for node_id, params in api_params.items():
+ record_node_stats(params, node_id)
+
+
+scheduler.add_job(record_user_usages, 'interval', coalesce=True, seconds=30)
+scheduler.add_job(record_node_usages, 'interval', coalesce=True, seconds=10)
diff --git a/app/jobs/reset_user_data_usage.py b/app/jobs/reset_user_data_usage.py
@@ -0,0 +1,38 @@
+from datetime import datetime
+
+from app import logger, scheduler, xray
+from app.db import crud, GetDB, get_users
+from app.models.user import UserDataLimitResetStrategy, UserStatus
+
+reset_strategy_to_days = {
+ UserDataLimitResetStrategy.day.value: 1,
+ UserDataLimitResetStrategy.week.value: 7,
+ UserDataLimitResetStrategy.month.value: 30,
+ UserDataLimitResetStrategy.year.value: 365,
+}
+
+
+def reset_user_data_usage():
+ now = datetime.utcnow()
+ with GetDB() as db:
+ for user in get_users(db, reset_strategy=[
+ UserDataLimitResetStrategy.day.value,
+ UserDataLimitResetStrategy.week.value,
+ UserDataLimitResetStrategy.month.value,
+ UserDataLimitResetStrategy.year.value,
+ ]):
+ last_reset_time = user.last_traffic_reset_time
+ num_days_to_reset = reset_strategy_to_days[user.data_limit_reset_strategy]
+
+ if not (now - last_reset_time).days >= num_days_to_reset:
+ continue
+
+ crud.reset_user_data_usage(db, user)
+ # make user active if limited on usage reset
+ if user.status == UserStatus.limited:
+ xray.operations.add_user(user)
+
+ logger.info(f"User data usage reset for User \"{user.username}\"")
+
+
+scheduler.add_job(reset_user_data_usage, 'interval', coalesce=True, hours=1)
diff --git a/app/jobs/review_users.py b/app/jobs/review_users.py
@@ -0,0 +1,101 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from sqlalchemy.orm import Session
+
+from app import logger, scheduler, xray
+from app.db import (GetDB, get_notification_reminder, get_users,
+ start_user_expire, update_user_status)
+from app.models.user import ReminderType, UserResponse, UserStatus
+from app.utils import report
+# from app.utils.concurrency import GetBG
+from app.utils.helpers import (calculate_expiration_days,
+ calculate_usage_percent)
+from config import (NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT,
+ WEBHOOK_ADDRESS)
+
+if TYPE_CHECKING:
+ from app.db.models import User
+
+
+def add_notification_reminders(db: Session, user: "User", now: datetime = datetime.utcnow()) -> None:
+ if user.data_limit:
+ usage_percent = calculate_usage_percent(
+ user.used_traffic, user.data_limit)
+ if (usage_percent >= NOTIFY_REACHED_USAGE_PERCENT) and (not get_notification_reminder(db, user.id, ReminderType.data_usage)):
+ report.data_usage_percent_reached(
+ db, usage_percent, UserResponse.model_validate(user),
+ user.id, user.expire)
+
+ if user.expire and ((now - user.created_at).days >= NOTIFY_DAYS_LEFT):
+ expire_days = calculate_expiration_days(user.expire)
+ if (expire_days <= NOTIFY_DAYS_LEFT) and (not get_notification_reminder(db, user.id, ReminderType.expiration_date)):
+ report.expire_days_reached(
+ db, expire_days, UserResponse.model_validate(user),
+ user.id, user.expire)
+
+
+async def review():
+ now = datetime.utcnow()
+ with GetDB() as db: #, GetBG() as bg:
+ for user in get_users(db, status=UserStatus.active):
+
+ limited = user.data_limit and user.used_traffic >= user.data_limit
+ expired = user.expire and user.expire <= now.timestamp()
+ if limited:
+ status = UserStatus.limited
+ elif expired:
+ status = UserStatus.expired
+ else:
+ if WEBHOOK_ADDRESS:
+ add_notification_reminders(db, user, now)
+ continue
+
+ await xray.operations.remove_user(user)
+ update_user_status(db, user, status)
+
+ #bg.add_task(
+ # report.status_change, user.username, status, UserResponse.model_validate(user)
+ #)
+
+ logger.info(f"User \"{user.username}\" status changed to {status}")
+
+ for user in get_users(db, status=UserStatus.on_hold):
+
+ if user.edit_at:
+ base_time = datetime.timestamp(user.edit_at)
+ else:
+ base_time = datetime.timestamp(user.created_at)
+
+ # Check if the user is online After or at 'base_time'
+ if user.online_at and base_time <= datetime.timestamp(user.online_at):
+ status = UserStatus.active
+
+ elif user.on_hold_timeout and (datetime.timestamp(user.on_hold_timeout) <= (now.timestamp())):
+ # If the user didn't connect within the timeout period, change status to "Active"
+ status = UserStatus.active
+
+ else:
+ continue
+
+ update_user_status(db, user, status)
+ start_user_expire(db, user)
+ #bg.add_task(report.status_change, user.username, status,
+ # UserResponse.model_validate(user))
+
+ logger.info(f"User \"{user.username}\" status changed to {status}")
+
+
+ active = user.expire and user.expire >= now.timestamp()
+ if active:
+ status = UserStatus.active
+ else:
+ continue
+
+ update_user_status(db, user, status)
+ await xray.operations.add_user(user)
+
+ logger.info(f"User \"{user.username}\" status fixed.")
+
+
+scheduler.add_job(review, 'interval', seconds=10, coalesce=True, max_instances=1)
diff --git a/app/jobs/send_notifications.py b/app/jobs/send_notifications.py
@@ -0,0 +1,82 @@
+from datetime import datetime as dt
+from datetime import timedelta as td
+from typing import Any, Dict, List
+
+from fastapi.encoders import jsonable_encoder
+from requests import Session
+
+import config
+from app import app, logger, scheduler
+from app.db import GetDB
+from app.db.models import NotificationReminder
+from app.utils.notification import queue
+
+session = Session()
+
+headers = {"x-webhook-secret": config.WEBHOOK_SECRET} if config.WEBHOOK_SECRET else None
+
+
+async def send(data: List[Dict[Any, Any]]) -> bool:
+ """Send the notification to the webhook address provided by config.WEBHOOK_ADDRESS
+
+ Args:
+ data (List[Dict[Any, Any]]): list of json encoded notifications
+
+ Returns:
+ bool: returns True if an ok response received
+ """
+ try:
+ logger.debug(f"Sending {len(data)} webhook updates to {config.WEBHOOK_ADDRESS}")
+
+ r = await aiohttp.request("post", config.WEBHOOK_ADDRESS, json=data, headers=headers)
+ if r.ok:
+ return True
+ logger.error(r)
+ except Exception as err:
+ logger.error(err)
+ return False
+
+
+async def send_notifications():
+ if not queue:
+ return
+
+ notifications_to_send = list()
+ try:
+ while (notification := queue.popleft()):
+ if (notification.tries > config.NUMBER_OF_RECURRENT_NOTIFICATIONS):
+ continue
+ if notification.send_at > dt.utcnow().timestamp():
+ queue.append(notification) # add it to the queue again for the next check
+ continue
+ notifications_to_send.append(notification)
+ except IndexError: # if the queue is empty
+ pass
+
+ if not notifications_to_send:
+ return
+ if not await send([jsonable_encoder(notif) for notif in notifications_to_send]):
+ for notification in notifications_to_send:
+ if (notification.tries + 1) > config.NUMBER_OF_RECURRENT_NOTIFICATIONS:
+ continue
+ notification.tries += 1
+ notification.send_at = ( # schedule notification for n seconds later
+ dt.utcnow() + td(seconds=config.RECURRENT_NOTIFICATIONS_TIMEOUT)).timestamp()
+ queue.append(notification)
+
+
+def delete_expired_reminders() -> None:
+ with GetDB() as db:
+ db.query(NotificationReminder).filter(NotificationReminder.expires_at < dt.utcnow()).delete()
+ db.commit()
+
+
+if config.WEBHOOK_ADDRESS:
+ @app.on_event("shutdown")
+ async def app_shutdown():
+ logger.info("Sending pending notifications before shutdown...")
+ await send_notifications()
+
+ logger.info("Send webhook job started")
+ scheduler.add_job(send_notifications, "interval", seconds=30, replace_existing=True)
+ scheduler.add_job(delete_expired_reminders, "interval", hours=2, start_date=dt.utcnow() + td(minutes=1))
diff --git a/app/models/admin.py b/app/models/admin.py
@@ -0,0 +1,88 @@
+from typing import Optional
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from passlib.context import CryptContext
+from pydantic import ConfigDict, BaseModel
+
+from app.db import Session, crud, get_db
+from app.utils.jwt import get_admin_payload
+from config import SUDOERS
+
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/token") # Admin view url
+
+
+class Token(BaseModel):
+ access_token: str
+ token_type: str = "bearer"
+
+
+class Admin(BaseModel):
+ username: str
+ is_sudo: bool
+ model_config = ConfigDict(from_attributes=True)
+
+ @classmethod
+ def get_admin(cls, token: str, db: Session):
+ payload = get_admin_payload(token)
+ if not payload:
+ return
+
+ if payload['username'] in SUDOERS and payload['is_sudo'] is True:
+ return cls(username=payload['username'], is_sudo=True)
+
+ dbadmin = crud.get_admin(db, payload['username'])
+ if not dbadmin:
+ return
+
+ if dbadmin.password_reset_at:
+ if not payload.get("created_at"):
+ return
+ if dbadmin.password_reset_at > payload.get("created_at"):
+ return
+
+ return cls.from_orm(dbadmin)
+
+ @classmethod
+ def get_current(cls,
+ db: Session = Depends(get_db),
+ token: str = Depends(oauth2_scheme)):
+ admin = cls.get_admin(token, db)
+ if not admin:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return admin
+
+
+class AdminCreate(Admin):
+ password: str
+
+ @property
+ def hashed_password(self):
+ return pwd_context.hash(self.password)
+
+
+class AdminModify(BaseModel):
+ password: str
+ is_sudo: bool
+
+ @property
+ def hashed_password(self):
+ return pwd_context.hash(self.password)
+
+
+class AdminPartialModify(AdminModify):
+ __annotations__ = {k: Optional[v] for k, v in AdminModify.__annotations__.items()}
+
+
+class AdminInDB(Admin):
+ username: str
+ hashed_password: str
+
+ def verify_password(self, plain_password):
+ return pwd_context.verify(plain_password, self.hashed_password)
diff --git a/app/models/core.py b/app/models/core.py
@@ -0,0 +1,7 @@
+from pydantic import BaseModel
+
+
+class CoreStats(BaseModel):
+ version: str
+ started: bool
+ logs_websocket: str
diff --git a/app/models/node.py b/app/models/node.py
@@ -0,0 +1,81 @@
+from enum import Enum
+from typing import List, Optional
+
+from pydantic import ConfigDict, BaseModel, Field
+
+
+class NodeStatus(str, Enum):
+ connected = "connected"
+ connecting = "connecting"
+ error = "error"
+ disabled = "disabled"
+
+
+class NodeSettings(BaseModel):
+ min_node_version: str = "v0.2.0"
+ certificate: str
+
+
+class NodeBase(BaseModel):
+ id: int
+ name: str
+ address: str
+ port: int = 62050
+ api_port: int = 62051
+ usage_coefficient: float = Field(gt=0, default=1.0)
+ model_config = ConfigDict(from_attributes=True)
+
+from app.models.proxy import InboundBase
+
+class Node(NodeBase):
+ inbounds: Optional[List[InboundBase]] = None
+
+
+class NodeCreate(Node):
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "name": "DE node",
+ "address": "192.168.1.1",
+ "port": 62050,
+ "api_port": 62051,
+ "add_as_new_host": True,
+ "usage_coefficient": 1
+ }
+ })
+
+
+class NodeModify(Node):
+ name: Optional[str] = Field(None, nullable=True)
+ address: Optional[str] = Field(None, nullable=True)
+ port: Optional[int] = Field(None, nullable=True)
+ api_port: Optional[int] = Field(None, nullable=True)
+ status: Optional[NodeStatus] = Field(None, nullable=True)
+ usage_coefficient: Optional[float] = Field(None, nullable=True)
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "name": "DE node",
+ "address": "192.168.1.1",
+ "port": 62050,
+ "api_port": 62051,
+ "status": "disabled",
+ "usage_coefficient": 1.0
+ }
+ })
+
+
+class NodeResponse(Node):
+ xray_version: Optional[str] = None
+ status: NodeStatus
+ message: Optional[str] = None
+ model_config = ConfigDict(from_attributes=True)
+
+
+class NodeUsageResponse(BaseModel):
+ node_id: Optional[int] = None
+ node_name: str
+ uplink: int
+ downlink: int
+
+
+class NodesUsageResponse(BaseModel):
+ usages: List[NodeUsageResponse]
diff --git a/app/models/proxy.py b/app/models/proxy.py
@@ -0,0 +1,186 @@
+import json
+from enum import Enum
+from typing import Optional, Union, List
+# from uuid import UUID, uuid4
+
+from pydantic import ConfigDict, BaseModel, Field, validator
+
+
+# from app.utils.system import random_password
+from xray_api.types.account import (
+ ShadowsocksAccount,
+ ShadowsocksMethods,
+ TrojanAccount,
+ VLESSAccount,
+ VMessAccount,
+ XTLSFlows,
+)
+
+
+class ProxyTypes(str, Enum):
+ # proxy_type = protocol
+
+ VMess = "vmess"
+ VLESS = "vless"
+ Trojan = "trojan"
+ Shadowsocks = "shadowsocks"
+
+ @property
+ def account_model(self):
+ if self == self.VMess:
+ return VMessAccount
+ if self == self.VLESS:
+ return VLESSAccount
+ if self == self.Trojan:
+ return TrojanAccount
+ if self == self.Shadowsocks:
+ return ShadowsocksAccount
+
+ @property
+ def settings_model(self):
+ if self == self.VMess:
+ return VMessSettings
+ if self == self.VLESS:
+ return VLESSSettings
+ if self == self.Trojan:
+ return TrojanSettings
+ if self == self.Shadowsocks:
+ return ShadowsocksSettings
+
+
+class ProxySettings(BaseModel):
+ @classmethod
+ def from_dict(cls, proxy_type: ProxyTypes, _dict: dict):
+ return ProxyTypes(proxy_type).settings_model.parse_obj(_dict)
+
+ def dict(self, *, no_obj=False, **kwargs):
+ if no_obj:
+ return json.loads(self.json())
+ return super().dict(**kwargs)
+
+
+class VMessSettings(ProxySettings):
+ id: str = Field(nullable=False)
+
+
+
+class VLESSSettings(ProxySettings):
+ id: str = Field(nullable=False)
+ flow: XTLSFlows = XTLSFlows.NONE
+
+
+
+class TrojanSettings(ProxySettings):
+ password: str = Field(nullable=False)
+
+
+
+class ShadowsocksSettings(ProxySettings):
+ password: str = Field(nullable=False)
+ method: ShadowsocksMethods = ShadowsocksMethods.CHACHA20_POLY1305
+
+
+
+class InboundHostSecurity(str, Enum):
+ inbound_default = "inbound_default"
+ none = "none"
+ tls = "tls"
+
+
+InboundHostALPN = Enum(
+ "ProxyHostALPN",
+ {
+ "none": "",
+ "h2": "h2",
+ "http/1.1": "http/1.1",
+ "h2,http/1.1": "h2,http/1.1",
+ },
+)
+
+
+InboundHostFingerprint = Enum(
+ "ProxyHostFingerprint",
+ {
+ "none": "",
+ "chrome": "chrome",
+ "firefox": "firefox",
+ "safari": "safari",
+ "ios": "ios",
+ "android": "android",
+ "edge": "edge",
+ "360": "360",
+ "qq": "qq",
+ "random": "random",
+ "randomized": "randomized",
+ },
+)
+
+
+class FormatVariables(dict):
+ def __missing__(self, key):
+ return key.join("{}")
+
+
+class InboundHost(BaseModel):
+ remark: str
+ address: str
+ port: Optional[int] = Field(None, nullable=True)
+ sni: Optional[str] = Field(None, nullable=True)
+ host: Optional[str] = Field(None, nullable=True)
+ path: Optional[str] = Field(None, nullable=True)
+ security: InboundHostSecurity = InboundHostSecurity.inbound_default
+ alpn: InboundHostALPN = InboundHostALPN.none
+ fingerprint: InboundHostFingerprint = InboundHostFingerprint.none
+ allowinsecure: Union[bool, None] = None
+ is_disabled: Union[bool, None] = None
+ model_config = ConfigDict(from_attributes=True)
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ @validator("remark", pre=False, always=True)
+ def validate_remark(cls, v):
+ try:
+ v.format_map(FormatVariables())
+ except ValueError as exc:
+ raise ValueError("Invalid formatting variables")
+
+ return v
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ @validator("address", pre=False, always=True)
+ def validate_address(cls, v):
+ try:
+ v.format_map(FormatVariables())
+ except ValueError as exc:
+ raise ValueError("Invalid formatting variables")
+
+ return v
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ @validator("path", pre=False, always=True)
+ def validate_path(cls, v):
+ if not v:
+ return v
+ try:
+ v.format_map(FormatVariables())
+ except ValueError:
+ raise ValueError("Invalid formatting variables")
+
+ return v
+
+
+class InboundBase(BaseModel):
+ id: Optional[int] = Field(None)
+ tag: str = Field()
+ protocol: ProxyTypes = Field()
+ node_id: Optional[int] = Field(None)
+ model_config = ConfigDict(from_attributes=True)
+
+from app.models.service import ServiceBase
+from app.models.node import NodeBase
+
+class Inbound(InboundBase):
+ services: Optional[List[ServiceBase]] = None
+ node: Optional[NodeBase] = None
diff --git a/app/models/service.py b/app/models/service.py
@@ -0,0 +1,63 @@
+from typing import Dict, List, Optional, Union
+
+from pydantic import ConfigDict, BaseModel, Field, validator
+
+# from app import xray
+
+
+class ServiceBase(BaseModel):
+ id: Optional[int] = None
+ name: Optional[str] = Field(None, nullable=True)
+ model_config = ConfigDict(from_attributes=True)
+
+from app.models.proxy import InboundBase
+from app.models.user import UserBase
+
+class Service(ServiceBase):
+ users: List[UserBase] = Field([])
+ inbounds: List[InboundBase] = Field([])
+
+
+class ServiceCreate(Service):
+ users: List[int] = Field([])
+ inbounds: List[int] = Field([])
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "name": "my service 1",
+ "inbounds": [1, 5, 9],
+ "users": [1, 2, 3]
+ }
+ })
+
+
+class ServiceModify(Service):
+ id: int
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "id": 2,
+ "name": "my service 2",
+ "inbounds": [1, 2, 3],
+ "users": [1, 2, 3],
+ }
+ })
+
+
+class ServiceResponse(Service):
+ id: int
+ users: List[UserBase] = Field([])
+ inbounds: List[InboundBase] = Field([])
+ """
+ @validator("inbounds", pre=True)
+ def validate_inbounds(cls, v):
+ final = {}
+ inbound_tags = [i.tag for i in v]
+ for protocol, inbounds in xray.config.inbounds_by_protocol.items():
+ for inbound in inbounds:
+ if inbound["tag"] in inbound_tags:
+ if protocol in final:
+ final[protocol].append(inbound["tag"])
+ else:
+ final[protocol] = [inbound["tag"]]
+ return final
+ """
+ model_config = ConfigDict(from_attributes=True)
diff --git a/app/models/system.py b/app/models/system.py
@@ -0,0 +1,15 @@
+from pydantic import BaseModel
+
+
+class SystemStats(BaseModel):
+ version: str
+ mem_total: int
+ mem_used: int
+ cpu_cores: int
+ cpu_usage: float
+ total_user: int
+ users_active: int
+ incoming_bandwidth: int
+ outgoing_bandwidth: int
+ incoming_bandwidth_speed: int
+ outgoing_bandwidth_speed: int
diff --git a/app/models/user.py b/app/models/user.py
@@ -0,0 +1,215 @@
+import re
+from datetime import datetime
+from enum import Enum
+from typing import Dict, List, Optional, Union
+import random
+import secrets
+
+from pydantic import field_validator, ConfigDict, BaseModel, Field, validator, computed_field, ValidationInfo
+
+from app import xray
+from app.models.proxy import ProxySettings, ProxyTypes, InboundBase
+from app.utils.jwt import create_subscription_token
+
+from config import XRAY_SUBSCRIPTION_PATH, XRAY_SUBSCRIPTION_URL_PREFIX
+
+USERNAME_REGEXP = re.compile(r"^(?=\w{3,32}\b)[a-zA-Z0-9-_@.]+(?:_[a-zA-Z0-9-_@.]+)*$")
+
+
+class ReminderType(str, Enum):
+ expiration_date = "expiration_date"
+ data_usage = "data_usage"
+
+
+class UserStatus(str, Enum):
+ active = "active"
+ disabled = "disabled"
+ limited = "limited"
+ expired = "expired"
+ on_hold = "on_hold"
+
+from app.utils.share import generate_v2ray_links
+
+class UserStatusModify(str, Enum):
+ active = "active"
+ disabled = "disabled"
+ on_hold = "on_hold"
+
+
+class UserStatusCreate(str, Enum):
+ active = "active"
+ on_hold = "on_hold"
+
+
+class UserDataLimitResetStrategy(str, Enum):
+ no_reset = "no_reset"
+ day = "day"
+ week = "week"
+ month = "month"
+ year = "year"
+
+
+class UserBase(BaseModel):
+ # proxies: Dict[ProxyTypes, ProxySettings] = {}
+ username: str
+ expire: Optional[int] = Field(None, nullable=True)
+ key: str = Field(default_factory=lambda: secrets.token_hex(16))
+ data_limit: Optional[int] = Field(
+ ge=0, default=None, description="data_limit can be 0 or greater"
+ )
+ data_limit_reset_strategy: UserDataLimitResetStrategy = (
+ UserDataLimitResetStrategy.no_reset
+ )
+ # inbounds: Dict[ProxyTypes, List[str]] = {}
+ note: Optional[str] = Field(None, nullable=True)
+ sub_updated_at: Optional[datetime] = Field(None, nullable=True)
+ sub_last_user_agent: Optional[str] = Field(None, nullable=True)
+ online_at: Optional[datetime] = Field(None, nullable=True)
+ on_hold_expire_duration: Optional[int] = Field(None, nullable=True)
+ on_hold_timeout: Optional[Union[datetime, None]] = Field(None, nullable=True)
+
+
+ @field_validator("username")
+ @classmethod
+ def validate_username(cls, v: str):
+ if not USERNAME_REGEXP.match(v):
+ raise ValueError(
+ "Username only can be 3 to 32 characters and contain a-z, 0-9, and underscores in between."
+ )
+ return v
+
+ @field_validator("note")
+ @classmethod
+ def validate_note(cls, v: str):
+ if v and len(v) > 500:
+ raise ValueError("User's note can be a maximum of 500 character")
+ return v
+
+ @field_validator("on_hold_expire_duration", "on_hold_timeout", check_fields=False)
+ @classmethod
+ def validate_timeout(cls, v: Optional[int]):
+ # Check if expire is 0 or None and timeout is not 0 or None
+ if (v in (0, None)):
+ return None
+ return v
+ model_config = ConfigDict(from_attributes=True)
+
+from app.models.service import ServiceBase
+
+class User(UserBase):
+ pass
+
+
+class UserCreate(User):
+ status: UserStatusCreate = Field(None, validate_default=True)
+ services: List[int] = Field([])
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "username": "user1234",
+ "services": [1, 2, 3],
+ "expire": 0,
+ "data_limit": 0,
+ "data_limit_reset_strategy": "no_reset",
+ "status": "active",
+ "note": "",
+ "on_hold_timeout": "2023-11-03T20:30:00",
+ "on_hold_expire_duration": 0,
+ }
+ })
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ #@validator("status", pre=True, always=True)
+ #def validate_status(cls, value):
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ @field_validator("status", mode="before")
+ def validate_status(cls, status: str, info: ValidationInfo) -> str:
+ if not status or status not in UserStatusCreate.__members__:
+ return UserStatusCreate.active # Set to the default if not valid
+ on_hold_expire = info.data.get("on_hold_expire_duration")
+ expire = info.data.get("expire")
+ if status == UserStatusCreate.on_hold:
+ if (on_hold_expire == 0 or on_hold_expire is None):
+ raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.")
+ if expire:
+ raise ValueError("User cannot be on hold with specified expire.")
+ return status
+
+
+class UserModify(User):
+ status: UserStatusModify = None
+ services: List[int] = []
+ data_limit_reset_strategy: UserDataLimitResetStrategy = None
+ model_config = ConfigDict(json_schema_extra={
+ "example": {
+ "username": "mammad1234",
+ "services": [1, 2, 3],
+ "expire": 0,
+ "data_limit": 0,
+ "data_limit_reset_strategy": "no_reset",
+ "status": "active",
+ "note": "",
+ "on_hold_timeout": "2023-11-03T20:30:00",
+ "on_hold_expire_duration": 0,
+ }
+ })
+
+
+ # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
+ # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
+ @validator("status", pre=True, always=True)
+ def validate_status(cls, status, values):
+ on_hold_expire = values.get("on_hold_expire_duration")
+ expire = values.get("expire")
+ if status == UserStatusCreate.on_hold:
+ if not on_hold_expire:
+ raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.")
+ if expire:
+ raise ValueError("User cannot be on hold with specified expire.")
+ return status
+
+
+class UserResponse(User):
+ id: int
+ status: UserStatus
+ used_traffic: int
+ lifetime_used_traffic: int = 0
+ created_at: datetime
+ inbounds: List[InboundBase] = Field([])
+ services: List[ServiceBase] = Field([])
+ # links: List[str] = [] # Field(default_factory=lamba x: )
+ # subscription_url: str = ""
+ model_config = ConfigDict(from_attributes=True)
+
+ @computed_field
+ @property
+ def links(self) -> List[str]:
+ return generate_v2ray_links(
+ inbounds=self.inbounds, key=self.key, extra_data=self.dict(exclude={'subscription_url', 'links'})
+ )
+
+ @computed_field
+ @property
+ def subscription_url(self) -> str:
+ salt = secrets.token_hex(8)
+ url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt)
+ token = create_subscription_token(self.username)
+ return f"{url_prefix}/{XRAY_SUBSCRIPTION_PATH}/{token}"
+
+
+class UsersResponse(BaseModel):
+ total: int
+ users: List[UserResponse]
+
+
+class UserUsageResponse(BaseModel):
+ node_id: Union[int, None] = None
+ node_name: str
+ used_traffic: int
+
+
+class UserUsagesResponse(BaseModel):
+ username: str
+ usages: List[UserUsageResponse]
diff --git a/app/telegram/__init__.py b/app/telegram/__init__.py
@@ -0,0 +1,52 @@
+import importlib.util
+import asyncio
+from os.path import dirname
+from config import TELEGRAM_API_TOKEN, TELEGRAM_PROXY_URL
+from app import app
+from telebot.async_telebot import AsyncTeleBot
+from telebot import asyncio_helper #, logger
+
+# import logging
+
+# logger.setLevel(logging.DEBUG)
+
+bot = None
+if TELEGRAM_API_TOKEN:
+ asyncio_helper.proxy = TELEGRAM_PROXY_URL
+ bot = AsyncTeleBot(TELEGRAM_API_TOKEN)
+
+handler_names = ["admin", "report", "user"]
+
+@app.on_event("startup")
+async def start_bot():
+ if bot:
+ handler_dir = dirname(__file__) + "/handlers/"
+ for name in handler_names:
+ spec = importlib.util.spec_from_file_location(name, f"{handler_dir}{name}.py")
+ spec.loader.exec_module(importlib.util.module_from_spec(spec))
+
+ from app.telegram import utils # setup custom handlers
+ utils.setup()
+ asyncio.get_running_loop().create_task(bot.polling())
+
+
+from .handlers.report import ( # noqa
+ report,
+ report_new_user,
+ report_user_modification,
+ report_user_deletion,
+ report_status_change,
+ report_user_usage_reset,
+ report_user_subscription_revoked
+)
+
+__all__ = [
+ "bot",
+ "report",
+ "report_new_user",
+ "report_user_modification",
+ "report_user_deletion",
+ "report_status_change",
+ "report_user_usage_reset",
+ "report_user_subscription_revoked"
+]
diff --git a/app/telegram/handlers/admin.py b/app/telegram/handlers/admin.py
@@ -0,0 +1,1980 @@
+import io
+import math
+import re
+import random
+import string
+import os
+from datetime import datetime
+
+import qrcode
+import sqlalchemy
+from dateutil.relativedelta import relativedelta
+from telebot import types
+from telebot.util import user_link
+from telebot.asyncio_handler_backends import State, StatesGroup
+
+from app import xray
+from app.db import GetDB, crud
+from app.models.user import (UserCreate, UserModify, UserResponse, UserStatus,
+ UserStatusModify)
+# from app.models.user_template import UserTemplateResponse
+from app.models.proxy import ProxyTypes
+from app.telegram import bot
+from app.telegram.utils.custom_filters import (cb_query_equals,
+ cb_query_startswith)
+from app.telegram.utils.keyboard import BotKeyboard
+from app.utils.store import MemoryStorage
+from app.utils.system import cpu_usage, memory_usage, readable_size
+
+try:
+ from app.utils.system import realtime_bandwith as realtime_bandwidth
+except ImportError:
+ from app.utils.system import realtime_bandwidth
+
+from config import TELEGRAM_LOGGER_CHANNEL_ID, TELEGRAM_DEFAULT_VLESS_FLOW
+
+mem_store = MemoryStorage()
+
+class botStates(StatesGroup):
+ start = State()
+ add_data = State() # statesgroup should contain states
+ add_time = State()
+ edit_user_data_limit = State()
+ edit_user_expire = State()
+ edit_note = State()
+ add_user_from_template = State()
+ add_user_username = State()
+ add_user_data_limit = State()
+ add_user_expire = State()
+
+
+def get_system_info():
+ mem = memory_usage()
+ cpu = cpu_usage()
+ with GetDB() as db:
+ bandwidth = crud.get_system_usage(db)
+ total_users = crud.get_users_count(db)
+ users_active = crud.get_users_count(db, UserStatus.active)
+ return """\
+🎛 *CPU Cores*: `{cpu_cores}`
+🖥 *CPU Usage*: `{cpu_percent}%`
+➖➖➖➖➖➖➖
+📊 *Total Memory*: `{total_memory}`
+📈 *In Use Memory*: `{used_memory}`
+📉 *Free Memory*: `{free_memory}`
+➖➖➖➖➖➖➖
+⬇️ *Download Usage*: `{down_bandwidth}`
+⬆️ *Upload Usage*: `{up_bandwidth}`
+↕️ *Total Usage*: `{total_bandwidth}`
+➖➖➖➖➖➖➖
+👥 *Total Users*: `{total_users}`
+🟢 *Active Users*: `{active_users}`
+🔴 *Deactivate Users*: `{deactivate_users}`
+➖➖➖➖➖➖➖
+⏫ *Upload Speed*: `{up_speed}`
+⏬ *Download Speed*: `{down_speed}`
+""".format(
+ cpu_cores=cpu.cores,
+ cpu_percent=cpu.percent,
+ total_memory=readable_size(mem.total),
+ used_memory=readable_size(mem.used),
+ free_memory=readable_size(mem.free),
+ total_bandwidth=readable_size(bandwidth.uplink + bandwidth.downlink),
+ up_bandwidth=readable_size(bandwidth.uplink),
+ down_bandwidth=readable_size(bandwidth.downlink),
+ total_users=total_users,
+ active_users=users_active,
+ deactivate_users=total_users - users_active,
+ up_speed=readable_size(realtime_bandwidth().outgoing_bytes),
+ down_speed=readable_size(realtime_bandwidth().incoming_bytes)
+ )
+
+
+def schedule_delete_message(chat_id, *message_ids: int) -> None:
+ messages: list[int] = mem_store.get(f"{chat_id}:messages_to_delete", [])
+ for mid in message_ids:
+ messages.append(mid)
+ mem_store.set(f"{chat_id}:messages_to_delete", messages)
+
+
+async def cleanup_messages(chat_id: int) -> None:
+ messages: list[int] = mem_store.get(f"{chat_id}:messages_to_delete", [])
+ for message_id in messages:
+ try: await bot.delete_message(chat_id, message_id)
+ except: pass
+ mem_store.set(f"{chat_id}:messages_to_delete", [])
+
+
+@bot.message_handler(commands=['start', 'help'], is_admin=True)
+async def help_command(message: types.Message):
+ await cleanup_messages(message.chat.id)
+ # bot.clear_step_handler_by_chat_id(message.chat.id)
+ await bot.delete_state(message.from_user.id, message.chat.id)
+ return await bot.reply_to(message, """
+{user_link} Welcome to Marzban Telegram-Bot Admin Panel.
+Here you can manage your users and proxies.
+To get started, use the buttons below.
+""".format(
+ user_link=user_link(message.from_user)
+ ), parse_mode="html", reply_markup=BotKeyboard.main_menu())
+
+
+@bot.callback_query_handler(cb_query_equals('system'), is_admin=True)
+async def system_command(call: types.CallbackQuery):
+ return await bot.edit_message_text(
+ get_system_info(),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="MarkdownV2",
+ reply_markup=BotKeyboard.main_menu()
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('restart'), is_admin=True)
+async def restart_command(call: types.CallbackQuery):
+ await bot.edit_message_text(
+ '⚠️ Are you sure? This will restart Xray core.',
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.confirm_action(action='restart')
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('delete:'), is_admin=True)
+async def delete_user_command(call: types.CallbackQuery):
+ username = call.data.split(':')[1]
+ await bot.edit_message_text(
+ f'⚠️ Are you sure? This will delete user `{username}`.',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(
+ action='delete', username=username)
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith("suspend:"), is_admin=True)
+async def suspend_user_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will suspend user `{username}`.",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(
+ action="suspend", username=username),
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith("activate:"), is_admin=True)
+async def activate_user_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will activate user `{username}`.",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(
+ action="activate", username=username),
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith("reset_usage:"), is_admin=True)
+async def reset_usage_user_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will Reset Usage of user `{username}`.",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(
+ action="reset_usage", username=username),
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('edit_all'), is_admin=True)
+async def edit_all_command(call: types.CallbackQuery):
+ with GetDB() as db:
+ total_users = crud.get_users_count(db)
+ active_users = crud.get_users_count(db, UserStatus.active)
+ disabled_users = crud.get_users_count(db, UserStatus.disabled)
+ expired_users = crud.get_users_count(db, UserStatus.expired)
+ limited_users = crud.get_users_count(db, UserStatus.limited)
+ text = f'''
+👥 *Total Users*: `{total_users}`
+✅ *Active Users*: `{active_users}`
+❌ *Disabled Users*: `{disabled_users}`
+🕰 *Expired Users*: `{expired_users}`
+🪫 *Limited Users*: `{limited_users}`'''
+ return await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.edit_all_menu()
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('delete_expired'), is_admin=True)
+async def delete_expired_command(call: types.CallbackQuery):
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will *DELETE All Expired Users*‼️",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(action="delete_expired"))
+
+
+@bot.callback_query_handler(cb_query_equals('delete_limited'), is_admin=True)
+async def delete_limited_command(call: types.CallbackQuery):
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will *DELETE All Limited Users*‼️",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(action="delete_limited"))
+
+
+@bot.callback_query_handler(cb_query_equals('add_data'), is_admin=True)
+async def add_data_command(call: types.CallbackQuery):
+ msg = await bot.edit_message_text(
+ f"🔋 Enter Data Limit to increase or decrease (GB):",
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.inline_cancel_action())
+ schedule_delete_message(call.message.chat.id, call.message.id)
+ schedule_delete_message(call.message.chat.id, msg.id)
+ # return bot.register_next_step_handler(call.message, add_data_step)
+ await bot.set_state(call.from_user.id, botStates.add_data, call.message.chat.id)
+
+
+@bot.message_handler(state=botStates.add_data)
+async def add_data_step(message):
+ try:
+ data_limit = float(message.text)
+ if not data_limit:
+ raise ValueError
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number and not zero.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_data_step)
+ schedule_delete_message(message.chat.id, message.message_id)
+ msg = await bot.send_message(
+ message.chat.id,
+ f"⚠️ Are you sure? this will change Data limit of all users according to <b>"
+ f"{'+' if data_limit > 0 else '-'}{readable_size(abs(data_limit *1024*1024*1024))}</b>",
+ parse_mode="html",
+ reply_markup=BotKeyboard.confirm_action('add_data', data_limit))
+ await bot.delete_state(message.from_user.id, message.chat.id)
+ await cleanup_messages(message.chat.id)
+ schedule_delete_message(message.chat.id, msg.id)
+
+
+@bot.callback_query_handler(cb_query_equals('add_time'), is_admin=True)
+async def add_time_command(call: types.CallbackQuery):
+ msg = await bot.edit_message_text(
+ f"📅 Enter Days to increase or decrease expiry:",
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.inline_cancel_action())
+ schedule_delete_message(call.message.chat.id, call.message.id)
+ schedule_delete_message(call.message.chat.id, msg.id)
+ # return bot.register_next_step_handler(call.message, add_time_step)
+ await bot.set_state(call.from_user.id, botStates.add_time, call.message.chat.id)
+
+
+@bot.message_handler(state=botStates.add_time)
+async def add_time_step(message):
+ try:
+ days = int(message.text)
+ if not days:
+ raise ValueError
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Days must be as a number and not zero.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_time_step)
+ schedule_delete_message(message.chat.id, message.message_id)
+ msg = await bot.send_message(
+ message.chat.id,
+ f"⚠️ Are you sure? this will change Expiry Time of all users according to <b>{days} Days</b>",
+ parse_mode="html",
+ reply_markup=BotKeyboard.confirm_action('add_time', days))
+ await cleanup_messages(message.chat.id)
+ schedule_delete_message(message.chat.id, msg.id)
+
+
+@bot.callback_query_handler(cb_query_startswith("inbound"), is_admin=True)
+async def inbound_command(call: types.CallbackQuery):
+ await bot.edit_message_text(
+ f"Select inbound to *{call.data[8:].title()}* from all users",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.inbounds_menu(call.data, xray.config.inbounds_by_tag))
+
+
+@bot.callback_query_handler(cb_query_startswith("confirm_inbound"), is_admin=True)
+async def delete_expired_confirm_command(call: types.CallbackQuery):
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will *{call.data[16:].replace(':', ' ')} for All Users*‼️",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(action=call.data[8:]))
+
+
+@bot.callback_query_handler(cb_query_startswith("edit:"), is_admin=True)
+async def edit_command(call: types.CallbackQuery):
+ # bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ await bot.delete_state(call.from_user.id, call.message.chat.id)
+ username = call.data.split(":")[1]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(
+ call.id,
+ '❌ User not found.',
+ show_alert=True
+ )
+ user = UserResponse.model_validate(db_user)
+ mem_store.set(f'{call.message.chat.id}:username', username)
+ mem_store.set(f'{call.message.chat.id}:data_limit', db_user.data_limit)
+ mem_store.set(f'{call.message.chat.id}:expire_date', datetime.fromtimestamp(db_user.expire) if db_user.expire else None)
+ mem_store.set(f'{call.message.chat.id}:protocols', {protocol.value: inbounds for protocol, inbounds in db_user.inbounds.items()})
+ await bot.edit_message_text(
+ f"📝 Editing user `{username}`",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.select_protocols(
+ user.inbounds,
+ "edit",
+ username=username,
+ data_limit=db_user.data_limit,
+ expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"),
+ )
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('help_edit'), is_admin=True)
+async def help_edit_command(call: types.CallbackQuery):
+ await bot.answer_callback_query(
+ call.id,
+ text="Press the (✏️ Edit) button to edit",
+ show_alert=True
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('cancel'), is_admin=True)
+async def cancel_command(call: types.CallbackQuery):
+ # bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ await bot.delete_state(call.from_user.id, call.message.chat.id)
+ return await bot.edit_message_text(
+ get_system_info(),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="MarkdownV2",
+ reply_markup=BotKeyboard.main_menu()
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('edit_user:'), is_admin=True)
+async def edit_user_command(call: types.CallbackQuery):
+ _, username, action = call.data.split(":")
+ schedule_delete_message(call.message.chat.id, call.message.id)
+ await cleanup_messages(call.message.chat.id)
+ if action == "data":
+ msg = await bot.send_message(
+ call.message.chat.id,
+ '⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.',
+ reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}')
+ )
+ mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text)
+ await bot.delete_state(call.from_user.id, call.message.chat.id)
+ await bot.set_state(call.from_user.id, botStates.edit_user_data_limit, call.message.chat.id)
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ data["username"] = username
+ #bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ #bot.register_next_step_handler(
+ # call.message, edit_user_data_limit_step, username)
+ schedule_delete_message(call.message.chat.id, msg.message_id)
+ elif action == "expire":
+ msg = await bot.send_message(
+ call.message.chat.id,
+ '⬆️ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\n⚠️ Send 0 for never expire.',
+ reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}'))
+ mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text)
+ # bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ await bot.set_state(call.from_user.id, botStates.edit_user_expire, call.message.chat.id)
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ data["username"] = username
+ #bot.register_next_step_handler(
+ # call.message, edit_user_expire_step, username=username)
+ schedule_delete_message(call.message.chat.id, msg.message_id)
+
+
+@bot.message_handler(state=botStates.edit_user_data_limit)
+async def edit_user_data_limit_step(message: types.Message):
+ try:
+ if float(message.text) < 0:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be greater or equal to 0.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username)
+ data_limit = float(message.text) * 1024**3
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username)
+ mem_store.set(f'{message.chat.id}:data_limit', data_limit)
+ schedule_delete_message(message.chat.id, message.message_id)
+ text = mem_store.get(f"{message.chat.id}:edit_msg_text")
+ mem_store.delete(f"{message.chat.id}:edit_msg_text")
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ username = data["username"]
+ del data["username"]
+ await bot.send_message(
+ message.chat.id,
+ text or f"📝 Editing user <code>{username}</code>",
+ parse_mode="html",
+ reply_markup=BotKeyboard.select_protocols(
+ mem_store.get(f'{message.chat.id}:protocols'), "edit",
+ username=username, data_limit=data_limit, expire_date=mem_store.get(f'{message.chat.id}:expire_date')))
+ await cleanup_messages(message.chat.id)
+
+
+@bot.message_handler(state=botStates.edit_user_expire)
+async def edit_user_expire_step(message: types.Message):
+ try:
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59
+ )
+ if re.match(r'^[0-9]{1,3}(M|m|D|d)$', message.text):
+ expire_date = today
+ number_pattern = r'^[0-9]{1,3}'
+ number = int(re.findall(number_pattern, message.text)[0])
+ symbol_pattern = r'(M|m|D|d)$'
+ symbol = re.findall(symbol_pattern, message.text)[0].upper()
+ if symbol == 'M':
+ expire_date = today + relativedelta(months=number)
+ elif symbol == 'D':
+ expire_date = today + relativedelta(days=number)
+ elif message.text != '0':
+ expire_date = datetime.strptime(message.text, "%Y-%m-%d")
+ else:
+ expire_date = None
+ if expire_date and expire_date < today:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be greater than today.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username)
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)')
+ schedule_delete_message(message.chat.id, wait_msg.message_id)
+ return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username)
+
+ mem_store.set(f'{message.chat.id}:expire_date', expire_date)
+ schedule_delete_message(message.chat.id, message.message_id)
+ text = mem_store.get(f"{message.chat.id}:edit_msg_text")
+ mem_store.delete(f"{message.chat.id}:edit_msg_text")
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ username = data["username"]
+ del data["username"]
+ await bot.send_message(
+ message.chat.id,
+ text or f"📝 Editing user <code>{username}</code>",
+ parse_mode="html",
+ reply_markup=BotKeyboard.select_protocols(
+ mem_store.get(f'{message.chat.id}:protocols'), "edit",
+ username=username, data_limit=mem_store.get(f'{message.chat.id}:data_limit'), expire_date=expire_date))
+ await cleanup_messages(message.chat.id)
+
+
+@bot.callback_query_handler(cb_query_startswith('users:'), is_admin=True)
+async def users_command(call: types.CallbackQuery):
+ page = int(call.data.split(':')[1]) if len(call.data.split(':')) > 1 else 1
+ with GetDB() as db:
+ total_pages = math.ceil(crud.get_users_count(db) / 10)
+ users = crud.get_users(db, offset=(page - 1) * 10, limit=10, sort=[crud.UsersSortingOptions["-created_at"]])
+ text = """👥 Users: (Page {page}/{total_pages})
+✅ Active
+❌ Disabled
+🕰 Expired
+🪫 Limited""".format(page=page, total_pages=total_pages)
+
+ await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.user_list(
+ users, page, total_pages=total_pages)
+ )
+
+
+def get_user_info_text(
+ status: str, username: str, sub_url: str, data_limit: int = None,
+ usage: int = None, expire: int = None, note: str = None) -> str:
+ statuses = {
+ 'active': '✅',
+ 'expired': '🕰',
+ 'limited': '🪫',
+ 'disabled': '❌'}
+ text = f"""\
+┌─{statuses[status]} <b>Status:</b> <code>{status.title()}</code>
+│ └─<b>Username:</b> <code>{username}</code>
+│
+├─🔋 <b>Data limit:</b> <code>{readable_size(data_limit) if data_limit else 'Unlimited'}</code>
+│ └─<b>Data Used:</b> <code>{readable_size(usage) if usage else "-"}</code>
+│
+├─📅 <b>Expiry Date:</b> <code>{datetime.fromtimestamp(expire).date() if expire else 'Never'}</code>
+│ └─<b>Days left:</b> <code>{(datetime.fromtimestamp(expire or 0) - datetime.now()).days if expire else '-'}</code>
+│
+"""
+ if note:
+ text += f'├─📝 <b>Note:</b> <code>{note}</code>\n│\n'
+ text += f'└─🚀 <b><a href="{sub_url}">Subscription</a>:</b> <code>{sub_url}</code>'
+ return text
+
+
+def get_template_info_text(
+ id: int, data_limit: int, expire_duration: int, username_prefix: str, username_suffix: str, inbounds: dict):
+ protocols = ""
+ for p, inbounds in inbounds.items():
+ protocols += f"\n├─ <b>{p.upper()}</b>\n"
+ protocols += "├───" + ", ".join([f"<code>{i}</code>" for i in inbounds])
+ text = f"""
+📊 Template Info:
+┌ ID: <b>{id}</b>
+├ Data Limit: <b>{readable_size(data_limit) if data_limit else 'Unlimited'}</b>
+├ Expire Date: <b>{(datetime.now() + relativedelta(seconds=expire_duration)).strftime('%Y-%m-%d') if expire_duration else 'Never'}</b>
+├ Username Prefix: <b>{username_prefix if username_prefix else '🚫'}</b>
+├ Username Suffix: <b>{username_suffix if username_suffix else '🚫'}</b>
+├ Protocols: {protocols}
+ """
+ return text
+
+
+@bot.callback_query_handler(cb_query_startswith('edit_note:'), is_admin=True)
+async def edit_note_command(call: types.CallbackQuery):
+ username = call.data.split(':')[1]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, '❌ User not found.', show_alert=True)
+ schedule_delete_message(call.message.chat.id, call.message.id)
+ await cleanup_messages(call.message.chat.id)
+ msg = await bot.send_message(
+ call.message.chat.id,
+ f'<b>📝 Current Note:</b> <code>{db_user.note}</code>\n\nSend new Note for <code>{username}</code>',
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}'))
+ mem_store.set(f'{call.message.chat.id}:username', username)
+ schedule_delete_message(call.message.chat.id, msg.id)
+ await bot.set_state(call.from_user.id, botStates.edit_note, call.message.chat.id)
+ # bot.register_next_step_handler(msg, edit_note_step)
+
+@bot.message_handler(state=botStates.edit_note)
+async def edit_note_step(message: types.Message):
+ note = message.text or ''
+ if len(note) > 500:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Note can not be more than 500 characters.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, edit_note_step)
+ username = mem_store.get(f'{message.chat.id}:username')
+ if not username:
+ await cleanup_messages(message.chat.id)
+ await bot.reply_to(message, '❌ Something went wrong!\n restart bot /start')
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ last_note = db_user.note
+ db_user = crud.update_user(db, db_user, UserModify(note=note))
+ user = UserResponse.model_validate(db_user)
+ text = get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ expire=user.expire,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ note=user.note)
+ await bot.reply_to(message, text, parse_mode="html", reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': user.status,
+ 'username': user.username}, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+📝 <b>#Edit_Note #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{username}</code>
+<b>Last Note :</b> <code>{last_note}</code>
+<b>New Note :</b> <code>{note}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={message.chat.id}">{message.from_user.full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+
+
+@bot.callback_query_handler(cb_query_startswith('user:'), is_admin=True)
+async def user_command(call: types.CallbackQuery):
+ await bot.delete_state(call.from_user.id, call.message.chat.id)
+ # bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ username = call.data.split(':')[1]
+ page = int(call.data.split(':')[2]) if len(call.data.split(':')) > 2 else 1
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(
+ call.id,
+ '❌ User not found.',
+ show_alert=True
+ )
+ user = UserResponse.model_validate(db_user)
+ text = get_user_info_text(
+ status=user.status, username=username, sub_url=user.subscription_url,
+ data_limit=user.data_limit, usage=user.used_traffic, expire=user.expire, note=user.note)
+ await bot.edit_message_text(
+ text,
+ call.message.chat.id, call.message.message_id, parse_mode="HTML",
+ reply_markup=BotKeyboard.user_menu(
+ {'username': user.username, 'status': user.status},
+ page=page, note=True))
+
+
+@bot.callback_query_handler(cb_query_startswith("revoke_sub:"), is_admin=True)
+async def revoke_sub_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+ await bot.edit_message_text(
+ f"⚠️ Are you sure? This will *Revoke Subscription* link for `{username}`‼️",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="markdown",
+ reply_markup=BotKeyboard.confirm_action(action=call.data))
+
+
+@bot.callback_query_handler(cb_query_startswith("links:"), is_admin=True)
+async def links_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, "User not found!", show_alert=True)
+
+ user = UserResponse.model_validate(db_user)
+
+ text = f"<code>{user.subscription_url}</code>\n\n\n"
+ for link in user.links:
+ text += f"<code>{link}</code>\n\n"
+
+ await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.show_links(username)
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith("genqr:"), is_admin=True)
+async def genqr_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, "User not found!", show_alert=True)
+
+ user = UserResponse.model_validate(db_user)
+
+ await bot.answer_callback_query(call.id, "Generating QR code...")
+
+ for link in user.links:
+ f = io.BytesIO()
+ qr = qrcode.QRCode(border=6)
+ qr.add_data(link)
+ qr.make_image().save(f)
+ f.seek(0)
+ await bot.send_photo(
+ call.message.chat.id,
+ photo=f,
+ caption=f"<code>{link}</code>",
+ parse_mode="HTML"
+ )
+ with io.BytesIO() as f:
+ qr = qrcode.QRCode(border=6)
+ qr.add_data(user.subscription_url)
+ qr.make_image().save(f)
+ f.seek(0)
+ await bot.send_photo(
+ call.message.chat.id,
+ photo=f,
+ caption=get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ expire=user.expire
+ ),
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.subscription_page(user.subscription_url)
+ )
+ try:
+ await bot.delete_message(call.message.chat.id, call.message.message_id)
+ except:
+ pass
+
+ text = f"<code>{user.subscription_url}</code>\n\n\n"
+ for link in user.links:
+ text += f"<code>{link}</code>\n\n"
+
+ await bot.send_message(
+ call.message.chat.id,
+ text,
+ "HTML",
+ reply_markup=BotKeyboard.show_links(username)
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('template_charge:'), is_admin=True)
+async def template_charge_command(call: types.CallbackQuery):
+ _, template_id, username = call.data.split(":")
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59
+ )
+ with GetDB() as db:
+ template = crud.get_user_template(db, template_id)
+ if not template:
+ return await bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
+ template = UserTemplateResponse.model_validate(template)
+
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, "User not found!", show_alert=True)
+ user = UserResponse.model_validate(db_user)
+ if (user.data_limit and not user.expire) or (not user.data_limit and user.expire):
+ text = get_user_info_text(
+ status='active',
+ username=username,
+ sub_url=user.subscription_url,
+ expire=int(((datetime.fromtimestamp(user.expire) if user.expire else today) +
+ relativedelta(seconds=template.expire_duration)).timestamp()),
+ data_limit=(
+ user.data_limit - user.used_traffic + template.data_limit) if user.data_limit else template.data_limit,
+ usage=0, note=user.note)
+ await bot.edit_message_text(f'''\
+‼️ <b>If add template <u>Bandwidth</u> and <u>Time</u> to the user, the user will be this</b>:\n\n\
+{text}\n\n\
+<b>Add template <u>Bandwidth</u> and <u>Time</u> to user or Reset to <u>Template default</u></b>⁉️''',
+ call.message.chat.id, call.message.message_id, parse_mode='html',
+ reply_markup=BotKeyboard.charge_add_or_reset(
+ username=username, template_id=template_id))
+ elif (not user.data_limit and not user.expire) or (user.used_traffic > user.data_limit) or (now > datetime.fromtimestamp(user.expire)):
+ crud.reset_user_data_usage(db, db_user)
+ expire_date = None
+ if template.expire_duration:
+ expire_date = today + relativedelta(seconds=template.expire_duration)
+ modify = UserModify(
+ status=UserStatusModify.active,
+ expire=int(expire_date.timestamp()) if expire_date else 0,
+ data_limit=template.data_limit,
+ )
+ db_user = crud.update_user(db, db_user, modify)
+ await xray.operations.add_user(db_user)
+
+ text = get_user_info_text(
+ status='active',
+ username=username,
+ sub_url=user.subscription_url,
+ expire=int(expire_date.timestamp()),
+ data_limit=template.data_limit,
+ usage=0, note=user.note)
+ await bot.edit_message_text(
+ f'🔋 User Successfully Charged!\n\n{text}',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='html',
+ reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': 'active',
+ 'username': user.username}, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🔋 <b>#Charged #Reset #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Template :</b> <code>{template.name}</code>
+<b>Username :</b> <code>{user.username}</code>
+➖➖➖➖➖➖➖➖➖
+<u><b>Last status</b></u>
+<b>├Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>
+<b>├Expire Date :</b> <code>\
+{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>
+➖➖➖➖➖➖➖➖➖
+<u><b>New status</b></u>
+<b>├Traffic Limit :</b> <code>{readable_size(db_user.data_limit) if db_user.data_limit else "Unlimited"}</code>
+<b>├Expire Date :</b> <code>\
+{datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ else:
+ text = get_user_info_text(
+ status='active',
+ username=username,
+ sub_url=user.subscription_url,
+ expire=int(((datetime.fromtimestamp(user.expire) if user.expire else today) +
+ relativedelta(seconds=template.expire_duration)).timestamp()),
+ data_limit=(
+ user.data_limit - user.used_traffic + template.data_limit) if user.data_limit else template.data_limit,
+ usage=0, note=user.note)
+ await bot.edit_message_text(f'''\
+‼️ <b>If add template <u>Bandwidth</u> and <u>Time</u> to the user, the user will be this</b>:\n\n\
+{text}\n\n\
+<b>Add template <u>Bandwidth</u> and <u>Time</u> to user or Reset to <u>Template default</u></b>⁉️''',
+ call.message.chat.id, call.message.message_id, parse_mode='html',
+ reply_markup=BotKeyboard.charge_add_or_reset(
+ username=username, template_id=template_id))
+
+
+@bot.callback_query_handler(cb_query_startswith('charge:'), is_admin=True)
+async def charge_command(call: types.CallbackQuery):
+ username = call.data.split(":")[1]
+ with GetDB() as db:
+ templates = crud.get_user_templates(db)
+ if not templates:
+ return await bot.answer_callback_query(call.id, "You don't have any User Templates!")
+
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, "User not found!", show_alert=True)
+
+ await bot.edit_message_text(
+ f"{call.message.html_text}\n\n🔢 Select <b>User Template</b> to charge:",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='html',
+ reply_markup=BotKeyboard.templates_menu(
+ {template.name: template.id for template in templates},
+ username=username,
+ )
+ )
+
+
+@bot.callback_query_handler(cb_query_equals('template_add_user'), is_admin=True)
+async def add_user_from_template_command(call: types.CallbackQuery):
+ with GetDB() as db:
+ templates = crud.get_user_templates(db)
+ if not templates:
+ return await bot.answer_callback_query(call.id, "You don't have any User Templates!")
+
+ await bot.edit_message_text(
+ "<b>Select a Template to create user from</b>:",
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='html',
+ reply_markup=BotKeyboard.templates_menu({template.name: template.id for template in templates})
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('template_add_user:'), is_admin=True)
+async def add_user_from_template(call: types.CallbackQuery):
+ template_id = int(call.data.split(":")[1])
+ with GetDB() as db:
+ template = crud.get_user_template(db, template_id)
+ if not template:
+ return await bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
+ template = UserTemplateResponse.model_validate(template)
+
+ text = get_template_info_text(
+ template_id, data_limit=template.data_limit, expire_duration=template.expire_duration,
+ username_prefix=template.username_prefix, username_suffix=template.username_suffix,
+ inbounds=template.inbounds)
+ if template.username_prefix:
+ text += f"\n⚠️ Username will be prefixed with <code>{template.username_prefix}</code>"
+ if template.username_suffix:
+ text += f"\n⚠️ Username will be suffixed with <code>{template.username_suffix}</code>"
+
+ mem_store.set(f"{call.message.chat.id}:template_id", template.id)
+ template_msg = await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML"
+ )
+ text = '👤 Enter username:\n⚠️ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.'
+ msg = await bot.send_message(
+ call.message.chat.id,
+ text,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.random_username(template_id=template.id)
+ )
+ schedule_delete_message(call.message.chat.id, template_msg.message_id, msg.id)
+ await bot.set_state(call.from_user.id, botStates.add_user_from_template, call.message.chat.id)
+ # bot.register_next_step_handler(template_msg, add_user_from_template_username_step)
+
+
+@bot.callback_query_handler(cb_query_startswith('random'), is_admin=True)
+async def random_username(call: types.CallbackQuery):
+ # bot.clear_step_handler_by_chat_id(call.message.chat.id)
+ await bot.delete_state(call.from_user.id, call.message.chat.id)
+ template_id = int(call.data.split(":")[1] or 0)
+ mem_store.delete(f'{call.message.chat.id}:template_id')
+
+ characters = string.ascii_letters + string.digits
+ username = random.choice(characters) + ''.join(random.choices(characters, k=4)) + '_' + ''.join(random.choices(characters, k=4))
+
+ schedule_delete_message(call.message.chat.id, call.message.id)
+ await cleanup_messages(call.message.chat.id)
+
+ if not template_id:
+ msg = await bot.send_message(call.message.chat.id,
+ '⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.',
+ reply_markup=BotKeyboard.inline_cancel_action())
+ schedule_delete_message(call.message.chat.id, msg.id)
+ await bot.set_state(call.from_user.id, botStates.add_user_data_limit, call.message.chat.id)
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ data["username"] = username
+ return # bot.register_next_step_handler(call.message, add_user_data_limit_step, username=username)
+
+ with GetDB() as db:
+ template = crud.get_user_template(db, template_id)
+ if template.username_prefix:
+ username = template.username_prefix + username
+ if template.username_suffix:
+ username += template.username_suffix
+
+ template = UserTemplateResponse.model_validate(template)
+ await bot.set_state(call.from_user.id, botStates.add_user_data_limit, call.message.chat.id)
+ async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data:
+ data["username"] = username
+ mem_store.set(f"{call.message.chat.id}:username", username)
+ mem_store.set(f"{call.message.chat.id}:data_limit", template.data_limit)
+ mem_store.set(f"{call.message.chat.id}:protocols", template.inbounds)
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59)
+ expire_date = None
+ if template.expire_duration:
+ expire_date = today + relativedelta(seconds=template.expire_duration)
+ mem_store.set(f"{call.message.chat.id}:expire_date", expire_date)
+
+ text = f"📝 Creating user <code>{username}</code>\n" + get_template_info_text(
+ id=template.id, data_limit=template.data_limit, expire_duration=template.expire_duration,
+ username_prefix=template.username_prefix, username_suffix=template.username_suffix, inbounds=template.inbounds)
+
+ await bot.send_message(
+ call.message.chat.id,
+ text,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.select_protocols(
+ template.inbounds,
+ "create_from_template",
+ username=username,
+ data_limit=template.data_limit,
+ expire_date=expire_date,))
+
+@bot.message_handler(state=botStates.add_user_from_template)
+async def add_user_from_template_username_step(message: types.Message):
+ template_id = mem_store.get(f"{message.chat.id}:template_id")
+ if template_id is None:
+ return await bot.send_message(message.chat.id, "An error occured in the process! try again.")
+
+ if not message.text:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Username can not be empty.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
+
+ with GetDB() as db:
+ username = message.text
+
+ template = crud.get_user_template(db, template_id)
+ if template.username_prefix:
+ username = template.username_prefix + username
+ if template.username_suffix:
+ username += template.username_suffix
+
+ match = re.match(r"^(?=\w{3,32}\b)[a-zA-Z0-9-_@.]+(?:_[a-zA-Z0-9-_@.]+)*$", username)
+ if not match:
+ wait_msg = await bot.send_message(message.chat.id,
+ '❌ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
+
+ if len(username) < 3:
+ wait_msg = await bot.send_message(message.chat.id,
+ f"❌ Username can't be generated because is shorter than 32 characters! username: <code>{username}</code>",
+ parse_mode="HTML")
+ schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
+ elif len(username) > 32:
+ wait_msg = await bot.send_message(message.chat.id,
+ f"❌ Username can't be generated because is longer than 32 characters! username: <code>{username}</code>",
+ parse_mode="HTML")
+ schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
+
+ if crud.get_user(db, username):
+ wait_msg = await bot.send_message(message.chat.id, '❌ Username already exists.')
+ schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
+ return # bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
+ template = UserTemplateResponse.model_validate(template)
+ mem_store.set(f"{message.chat.id}:username", username)
+ mem_store.set(f"{message.chat.id}:data_limit", template.data_limit)
+ mem_store.set(f"{message.chat.id}:protocols", template.inbounds)
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59
+ )
+ expire_date = None
+ if template.expire_duration:
+ expire_date = today + relativedelta(seconds=template.expire_duration)
+ mem_store.set(f"{message.chat.id}:expire_date", expire_date)
+
+ text = f"📝 Creating user <code>{username}</code>\n" + get_template_info_text(
+ id=template.id, data_limit=template.data_limit, expire_duration=template.expire_duration,
+ username_prefix=template.username_prefix, username_suffix=template.username_suffix, inbounds=template.inbounds)
+
+ await bot.send_message(
+ message.chat.id,
+ text,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.select_protocols(
+ template.inbounds,
+ "create_from_template",
+ username=username,
+ data_limit=template.data_limit,
+ expire_date=expire_date,
+ )
+ )
+ schedule_delete_message(message.chat.id, message.id)
+ await cleanup_messages(message.chat.id)
+
+
+@bot.callback_query_handler(cb_query_equals('add_user'), is_admin=True)
+async def add_user_command(call: types.CallbackQuery):
+ try:
+ await bot.delete_message(call.message.chat.id, call.message.message_id)
+ except: # noqa
+ pass
+ username_msg = await bot.send_message(
+ call.message.chat.id,
+ '👤 Enter username:\n⚠️Username only can be 3 to 32 characters and contain a-z, A-Z 0-9, and underscores in '
+ 'between.',
+ reply_markup=BotKeyboard.random_username())
+ schedule_delete_message(call.message.chat.id, username_msg.id)
+ await bot.set_state(call.from_user.id, botStates.add_user_username, call.message.chat.id)
+ # bot.register_next_step_handler(username_msg, add_user_username_step)
+
+@bot.message_handler(state=botStates.add_user_username)
+async def add_user_username_step(message: types.Message):
+ username = message.text
+ if not username:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Username can not be empty.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_username_step)
+ if not re.match(r'^(?!.*__)(?!.*_$)\w{2,31}[a-zA-Z\d]$', username):
+ wait_msg = await bot.send_message(message.chat.id,
+ '❌ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_username_step)
+ with GetDB() as db:
+ if crud.get_user(db, username):
+ wait_msg = await bot.send_message(message.chat.id, '❌ Username already exists.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_username_step)
+ schedule_delete_message(message.chat.id, message.id)
+ await cleanup_messages(message.chat.id)
+ msg = await bot.send_message(message.chat.id,
+ '⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.',
+ reply_markup=BotKeyboard.inline_cancel_action())
+ schedule_delete_message(message.chat.id, msg.id)
+ await bot.set_state(message.from_user.id, botStates.add_user_data_limit, message.chat.id)
+ async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
+ data["username"] = username
+ # bot.register_next_step_handler(msg, add_user_data_limit_step, username=username)
+
+
+@bot.message_handler(state=botStates.add_user_data_limit)
+async def add_user_data_limit_step(message: types.Message):
+ try:
+ if float(message.text) < 0:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be greater or equal to 0.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return #bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username)
+ data_limit = float(message.text) * 1024 * 1024 * 1024
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username)
+ schedule_delete_message(message.chat.id, message.id)
+ await cleanup_messages(message.chat.id)
+ msg = await bot.send_message(message.chat.id,
+ '⬆️ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\n⚠️ Send 0 for never expire.',
+ reply_markup=BotKeyboard.inline_cancel_action())
+ schedule_delete_message(message.chat.id, msg.id)
+ await bot.set_state(message.from_user.id, botStates.add_user_expire, message.chat.id)
+ async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
+ data["data_limit"] = data_limit
+
+ # bot.register_next_step_handler(msg, add_user_expire_step, username=username, data_limit=data_limit)
+
+@bot.message_handler(state=botStates.add_user_expire)
+async def add_user_expire_step(message: types.Message):
+ try:
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59
+ )
+ if re.match(r'^[0-9]{1,3}(M|m|D|d)$', message.text):
+ expire_date = today
+ number_pattern = r'^[0-9]{1,3}'
+ number = int(re.findall(number_pattern, message.text)[0])
+ symbol_pattern = r'(M|m|D|d)$'
+ symbol = re.findall(symbol_pattern, message.text)[0].upper()
+ if symbol == 'M':
+ expire_date = today + relativedelta(months=number)
+ elif symbol == 'D':
+ expire_date = today + relativedelta(days=number)
+ elif message.text != '0':
+ expire_date = datetime.strptime(message.text, "%Y-%m-%d")
+ else:
+ expire_date = None
+ if expire_date and expire_date < today:
+ wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be greater than today.')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit)
+ except ValueError:
+ wait_msg = await bot.send_message(message.chat.id,
+ '❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)')
+ schedule_delete_message(message.chat.id, wait_msg.id)
+ schedule_delete_message(message.chat.id, message.id)
+ return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit)
+ async with bot.retrieve_data(message.from_user.id, message.chat.id) as data:
+ username, data_limit = data["username"], data["data_limit"]
+ mem_store.set(f'{message.chat.id}:username', username)
+ mem_store.set(f'{message.chat.id}:data_limit', data_limit)
+ mem_store.set(f'{message.chat.id}:expire_date', expire_date)
+
+ schedule_delete_message(message.chat.id, message.id)
+ await cleanup_messages(message.chat.id)
+ await bot.send_message(
+ message.chat.id,
+ 'Select Protocols:\nUsernames: {}\nData Limit: {}\nExpiry Date {}'.format(
+ mem_store.get(f'{message.chat.id}:username'),
+ readable_size(mem_store.get(f'{message.chat.id}:data_limit'))
+ if mem_store.get(f'{message.chat.id}:data_limit') else "Unlimited",
+ mem_store.get(f'{message.chat.id}:expire_date').strftime("%Y-%m-%d")
+ if mem_store.get(f'{message.chat.id}:expire_date') else 'Never'
+ ),
+ reply_markup=BotKeyboard.select_protocols({}, action="create")
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('select_inbound:'), is_admin=True)
+async def select_inbounds(call: types.CallbackQuery):
+ if not (username := mem_store.get(f'{call.message.chat.id}:username')):
+ return await bot.answer_callback_query(call.id, '❌ No user selected.', show_alert=True)
+ protocols: dict[str, list[str]] = mem_store.get(f'{call.message.chat.id}:protocols', {})
+ _, inbound, action = call.data.split(':')
+ for protocol, inbounds in xray.config.inbounds_by_protocol.items():
+ for i in inbounds:
+ if i['tag'] != inbound:
+ continue
+ if not inbound in protocols[protocol]:
+ protocols[protocol].append(inbound)
+ else:
+ protocols[protocol].remove(inbound)
+ if len(protocols[protocol]) < 1:
+ del protocols[protocol]
+
+ mem_store.set(f'{call.message.chat.id}:protocols', protocols)
+
+ if action in ["edit", "create_from_template"]:
+ return await bot.edit_message_text(
+ call.message.text,
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.select_protocols(
+ protocols,
+ "edit",
+ username=username,
+ data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"),
+ expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"))
+ )
+ await bot.edit_message_text(
+ call.message.text,
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.select_protocols(protocols, "create")
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('select_protocol:'), is_admin=True)
+async def select_protocols(call: types.CallbackQuery):
+ if not (username := mem_store.get(f'{call.message.chat.id}:username')):
+ return await bot.answer_callback_query(call.id, '❌ No user selected.', show_alert=True)
+ protocols: dict[str, list[str]] = mem_store.get(f'{call.message.chat.id}:protocols', {})
+ _, protocol, action = call.data.split(':')
+ if protocol in protocols:
+ del protocols[protocol]
+ else:
+ protocols.update(
+ {protocol: [inbound['tag'] for inbound in xray.config.inbounds_by_protocol[protocol]]})
+ mem_store.set(f'{call.message.chat.id}:protocols', protocols)
+
+ if action == ["edit", "create_from_template"]:
+ return await bot.edit_message_text(
+ call.message.text,
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.select_protocols(
+ protocols,
+ "edit",
+ username=username,
+ data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"),
+ expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"))
+ )
+ await bot.edit_message_text(
+ call.message.text,
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.select_protocols(protocols, action="create")
+ )
+
+
+@bot.callback_query_handler(cb_query_startswith('confirm:'), is_admin=True)
+async def confirm_user_command(call: types.CallbackQuery):
+ data = call.data.split(':')[1]
+ chat_id = call.from_user.id
+ full_name = call.from_user.full_name
+ now = datetime.now()
+ today = datetime(
+ year=now.year,
+ month=now.month,
+ day=now.day,
+ hour=23,
+ minute=59,
+ second=59)
+ if data == 'delete':
+ username = call.data.split(':')[2]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ crud.remove_user(db, db_user)
+ await xray.operations.remove_user(db_user)
+
+ await bot.edit_message_text(
+ '✅ User deleted.',
+ call.message.chat.id,
+ call.message.message_id,
+ reply_markup=BotKeyboard.main_menu()
+ )
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🗑 <b>#Deleted #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{db_user.username}</code>
+<b>Traffic Limit :</b> <code>{readable_size(db_user.data_limit) if db_user.data_limit else "Unlimited"}</code>
+<b>Expire Date :</b> <code>\
+{datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ elif data == "suspend":
+ username = call.data.split(":")[2]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ crud.update_user(db, db_user, UserModify(
+ status=UserStatusModify.disabled))
+ await xray.operations.remove_user(db_user)
+ user = UserResponse.model_validate(db_user)
+ await bot.edit_message_text(
+ get_user_info_text(
+ status='disabled',
+ username=username,
+ sub_url=user.subscription_url,
+ data_limit=db_user.data_limit,
+ usage=db_user.used_traffic,
+ expire=db_user.expire,
+ note=user.note
+ ),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='HTML',
+ reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': 'disabled',
+ 'username': db_user.username
+ }, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+❌ <b>#Disabled #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ elif data == "activate":
+ username = call.data.split(":")[2]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ crud.update_user(db, db_user, UserModify(
+ status=UserStatusModify.active))
+ await xray.operations.add_user(db_user)
+ user = UserResponse.model_validate(db_user)
+ await bot.edit_message_text(
+ get_user_info_text(
+ status='active',
+ username=username,
+ sub_url=user.subscription_url,
+ data_limit=db_user.data_limit,
+ usage=db_user.used_traffic,
+ expire=db_user.expire,
+ note=user.note
+ ),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='HTML',
+ reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': 'active',
+ 'username': db_user.username
+ }, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+✅ <b>#Activated #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ elif data == 'reset_usage':
+ username = call.data.split(":")[2]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ crud.reset_user_data_usage(db, db_user)
+ user = UserResponse.model_validate(db_user)
+ await bot.edit_message_text(
+ get_user_info_text(
+ status=user.status,
+ username=username,
+ sub_url=user.subscription_url,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ expire=user.expire,
+ note=user.note
+ ),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='HTML',
+ reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': user.status,
+ 'username': user.username
+ }, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🔁 <b>#Reset_usage #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ elif data == 'restart':
+ m = await bot.edit_message_text(
+ '🔄 Restarting XRay core...', call.message.chat.id, call.message.message_id)
+ await xray.core.restart(xray.config.include_db_users())
+ for node_id, node in list(xray.nodes.items()):
+ if await node.is_healthy():
+ await xray.operations.restart_node(node_id, xray.config.include_db_users())
+ await bot.edit_message_text(
+ '✅ XRay core restarted successfully.',
+ m.chat.id, m.message_id,
+ reply_markup=BotKeyboard.main_menu()
+ )
+
+ elif data in ['charge_add', 'charge_reset']:
+ _, _, username, template_id = call.data.split(":")
+ with GetDB() as db:
+ template = crud.get_user_template(db, template_id)
+ if not template:
+ return await bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
+ template = UserTemplateResponse.model_validate(template)
+
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, "User not found!", show_alert=True)
+ user = UserResponse.model_validate(db_user)
+
+ inbounds = template.inbounds
+ proxies = {p.type.value: p.settings for p in db_user.proxies}
+
+ for protocol in xray.config.inbounds_by_protocol:
+ if protocol in inbounds and protocol not in db_user.inbounds:
+ proxies.update({protocol: {}})
+ elif protocol in db_user.inbounds and protocol not in inbounds:
+ del proxies[protocol]
+
+ crud.reset_user_data_usage(db, db_user)
+ if data == 'charge_reset':
+ expire_date = None
+ if template.expire_duration:
+ expire_date = today + relativedelta(seconds=template.expire_duration)
+ modify = UserModify(
+ status=UserStatus.active,
+ expire=int(expire_date.timestamp()) if expire_date else 0,
+ data_limit=template.data_limit,
+ )
+ else:
+ expire_date = None
+ if template.expire_duration:
+ expire_date = (datetime.fromtimestamp(user.expire)
+ if user.expire else today) + relativedelta(seconds=template.expire_duration)
+ modify = UserModify(
+ status=UserStatus.active,
+ expire=int(expire_date.timestamp()) if expire_date else 0,
+ data_limit=(user.data_limit or 0) - user.used_traffic + template.data_limit,
+ )
+ db_user = crud.update_user(db, db_user, modify)
+ await xray.operations.add_user(db_user)
+
+ text = get_user_info_text(
+ status=db_user.status,
+ username=username,
+ sub_url=user.subscription_url,
+ expire=db_user.expire,
+ data_limit=db_user.data_limit,
+ usage=db_user.used_traffic,
+ note=db_user.note)
+
+ await bot.edit_message_text(
+ f'🔋 User Successfully Charged!\n\n{text}',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode='html',
+ reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': user.status,
+ 'username': user.username
+ }, note=True))
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🔋 <b>#Charged #{data.split('_')[1].title()} #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Template :</b> <code>{template.name}</code>
+<b>Username :</b> <code>{user.username}</code>
+➖➖➖➖➖➖➖➖➖
+<u><b>Last status</b></u>
+<b>├Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>
+<b>├Expire Date :</b> <code>\
+{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>
+➖➖➖➖➖➖➖➖➖
+<u><b>New status</b></u>
+<b>├Traffic Limit :</b> <code>{readable_size(db_user.data_limit) if db_user.data_limit else "Unlimited"}</code>
+<b>├Expire Date :</b> <code>\
+{datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>\
+'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+ elif data == 'edit_user':
+ if (username := mem_store.get(f'{call.message.chat.id}:username')) is None:
+ try:
+ await bot.delete_message(call.message.chat.id,
+ call.message.message_id)
+ except Exception:
+ pass
+ return await bot.send_message(
+ call.message.chat.id,
+ '❌ Bot reload detected. Please start over.',
+ reply_markup=BotKeyboard.main_menu()
+ )
+
+ if not mem_store.get(f'{call.message.chat.id}:protocols'):
+ return await bot.answer_callback_query(
+ call.id,
+ '❌ No inbounds selected.',
+ show_alert=True
+ )
+
+ inbounds: dict[str, list[str]] = {
+ k: v for k, v in mem_store.get(f'{call.message.chat.id}:protocols').items() if v}
+
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, text=f"User not found!", show_alert=True)
+
+ proxies = {p.type.value: p.settings for p in db_user.proxies}
+
+ for protocol in xray.config.inbounds_by_protocol:
+ if protocol in inbounds and protocol not in db_user.inbounds:
+ proxies.update({protocol: {'flow': TELEGRAM_DEFAULT_VLESS_FLOW} if
+ TELEGRAM_DEFAULT_VLESS_FLOW and protocol == ProxyTypes.VLESS else {}})
+ elif protocol in db_user.inbounds and protocol not in inbounds:
+ del proxies[protocol]
+
+ modify = UserModify(
+ expire=int(mem_store.get(f'{call.message.chat.id}:expire_date').timestamp())
+ if mem_store.get(f'{call.message.chat.id}:expire_date') else 0, data_limit=mem_store.get(
+ f"{call.message.chat.id}:data_limit"),
+ proxies=proxies, inbounds=inbounds)
+ last_user = UserResponse.model_validate(db_user)
+ db_user = crud.update_user(db, db_user, modify)
+
+ user = UserResponse.model_validate(db_user)
+
+ if user.status == UserStatus.active:
+ await xray.operations.update_user(db_user)
+ else:
+ await xray.operations.remove_user(db_user)
+
+ await bot.answer_callback_query(call.id, "✅ User updated successfully.")
+
+ text = get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ expire=user.expire,
+ note=user.note
+ )
+ await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.user_menu({
+ 'username': db_user.username,
+ 'status': db_user.status},
+ note=True)
+ )
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ tag = f'\n➖➖➖➖➖➖➖➖➖ \n<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'
+ if last_user.data_limit != user.data_limit:
+ text = f'''\
+📶 <b>#Traffic_Change #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{user.username}</code>
+<b>Last Traffic Limit :</b> <code>{readable_size(last_user.data_limit) if last_user.data_limit else "Unlimited"}</code>
+<b>New Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>{tag}'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ if last_user.expire != user.expire:
+ text = f'''\
+📅 <b>#Expiry_Change #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{user.username}</code>
+<b>Last Expire Date :</b> <code>\
+{datetime.fromtimestamp(last_user.expire).strftime('%H:%M:%S %Y-%m-%d') if last_user.expire else "Never"}</code>
+<b>New Expire Date :</b> <code>\
+{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>{tag}'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+ if list(last_user.inbounds.values())[0] != list(user.inbounds.values())[0]:
+ text = f'''\
+⚙️ <b>#Inbounds_Change #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{user.username}</code>
+<b>Last Proxies :</b> <code>{", ".join(list(last_user.inbounds.values())[0])}</code>
+<b>New Proxies :</b> <code>{", ".join(list(user.inbounds.values())[0])}</code>{tag}'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+ elif data == 'add_user':
+ if mem_store.get(f'{call.message.chat.id}:username') is None:
+ try:
+ await bot.delete_message(call.message.chat.id,
+ call.message.message_id)
+ except Exception:
+ pass
+ return await bot.send_message(
+ call.message.chat.id,
+ '❌ Bot reload detected. Please start over.',
+ reply_markup=BotKeyboard.main_menu()
+ )
+
+ if not mem_store.get(f'{call.message.chat.id}:protocols'):
+ return await bot.answer_callback_query(
+ call.id,
+ '❌ No inbounds selected.',
+ show_alert=True
+ )
+
+ inbounds: dict[str, list[str]] = {
+ k: v for k, v in mem_store.get(f'{call.message.chat.id}:protocols').items() if v}
+ proxies = {p: ({'flow': TELEGRAM_DEFAULT_VLESS_FLOW} if
+ TELEGRAM_DEFAULT_VLESS_FLOW and p == ProxyTypes.VLESS else {}) for p in inbounds}
+
+ new_user = UserCreate(
+ username=mem_store.get(f'{call.message.chat.id}:username'),
+ expire=int(mem_store.get(f'{call.message.chat.id}:expire_date').timestamp())
+ if mem_store.get(f'{call.message.chat.id}:expire_date') else None,
+ data_limit=mem_store.get(f'{call.message.chat.id}:data_limit')
+ if mem_store.get(f'{call.message.chat.id}:data_limit') else None,
+ proxies=proxies,
+ inbounds=inbounds)
+
+ for proxy_type in new_user.proxies:
+ if not xray.config.inbounds_by_protocol.get(proxy_type):
+ return await bot.answer_callback_query(
+ call.id,
+ f'❌ Protocol {proxy_type} is disabled on your server',
+ show_alert=True
+ )
+
+ try:
+ with GetDB() as db:
+ db_user = crud.create_user(db, new_user)
+ proxies = db_user.proxies
+ user = UserResponse.model_validate(db_user)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ return await bot.answer_callback_query(
+ call.id,
+ '❌ Username already exists.',
+ show_alert=True
+ )
+
+ await xray.operations.add_user(db_user)
+
+ text = get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ expire=user.expire,
+ note=user.note
+ )
+ await bot.edit_message_text(
+ text,
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.user_menu(user_info={'status': user.status, 'username': user.username}, note=True))
+
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🆕 <b>#Created #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{user.username}</code>
+<b>Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>
+<b>Expire Date :</b> <code>\
+{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>
+<b>Proxies :</b> <code>{"" if not proxies else ", ".join([proxy.type for proxy in proxies])}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+ elif data in ['delete_expired', 'delete_limited']:
+ await bot.edit_message_text(
+ '⏳ <b>In Progress...</b>',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML")
+ with GetDB() as db:
+ depleted_users = crud.get_users(
+ db, status=[UserStatus.limited if data == 'delete_limited' else UserStatus.expired])
+ file_name = f'{data[8:]}_users_{int(now.timestamp()*1000)}.txt'
+ with open(file_name, 'w') as f:
+ f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n')
+ deleted = 0
+ for user in depleted_users:
+ try:
+ crud.remove_user(db, user)
+ await xray.operations.remove_user(user)
+ deleted +=1
+ f.write(\
+f'{user.username}\
+\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\
+\t{readable_size(user.used_traffic) if user.used_traffic else 0}\
+/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\
+\t{user.status}\n')
+ except:
+ db.rollback()
+ await bot.edit_message_text(
+ f'✅ <code>{deleted}</code>/<code>{len(depleted_users)}</code> <b>{data[7:].title()} Users Deleted</b>',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.main_menu())
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🗑 <b>#Delete #{data[7:].title()} #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Count:</b> <code>{deleted}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML')
+ os.remove(file_name)
+ except:
+ pass
+ elif data == 'add_data':
+ schedule_delete_message(
+ call.message.chat.id,
+ (await bot.send_message(chat_id, '⏳ <b>In Progress...</b>', 'HTML')).id)
+ data_limit = float(call.data.split(":")[2]) * 1024 * 1024 * 1024
+ with GetDB() as db:
+ users = crud.get_users(db)
+ counter = 0
+ file_name = f'new_data_limit_users_{int(now.timestamp()*1000)}.txt'
+ with open(file_name, 'w') as f:
+ f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n')
+ for user in users:
+ try:
+ if user.data_limit and user.status not in [UserStatus.limited, UserStatus.expired]:
+ user = crud.update_user(db, user, UserModify(data_limit=(user.data_limit + data_limit)))
+ counter += 1
+ f.write(
+ f'{user.username}\
+\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\
+\t{readable_size(user.used_traffic) if user.used_traffic else 0}\
+/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\
+\t{user.status}\n')
+ except:
+ db.rollback()
+ await cleanup_messages(chat_id)
+ await bot.send_message(
+ chat_id,
+ f'✅ <b>{counter}/{len(users)} Users</b> Data Limit according to <code>{"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}</code>',
+ 'HTML',
+ reply_markup=BotKeyboard.main_menu())
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+📶 <b>#Traffic_Change #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>According to:</b> <code>{"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}</code>
+<b>Count:</b> <code>{counter}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML')
+ os.remove(file_name)
+ except:
+ pass
+
+ elif data == 'add_time':
+ schedule_delete_message(
+ call.message.chat.id,
+ (await bot.send_message(chat_id, '⏳ <b>In Progress...</b>', 'HTML')).id)
+ days = int(call.data.split(":")[2])
+ with GetDB() as db:
+ users = crud.get_users(db)
+ counter = 0
+ file_name = f'new_expiry_users_{int(now.timestamp()*1000)}.txt'
+ with open(file_name, 'w') as f:
+ f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n')
+ for user in users:
+ try:
+ if user.expire and user.status not in [UserStatus.limited, UserStatus.expired]:
+ user = crud.update_user(
+ db, user,
+ UserModify(
+ expire=int(
+ (datetime.fromtimestamp(user.expire) + relativedelta(days=days)).timestamp())))
+ counter += 1
+ f.write(
+ f'{user.username}\
+\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\
+\t{readable_size(user.used_traffic) if user.used_traffic else 0}\
+/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\
+\t{user.status}\n')
+ except:
+ db.rollback()
+ await cleanup_messages(chat_id)
+ await bot.send_message(
+ chat_id,
+ f'✅ <b>{counter}/{len(users)} Users</b> Expiry Changes according to {days} Days',
+ 'HTML',
+ reply_markup=BotKeyboard.main_menu())
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+📅 <b>#Expiry_Change #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>According to:</b> <code>{days} Days</code>
+<b>Count:</b> <code>{counter}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML')
+ os.remove(file_name)
+ except:
+ pass
+ elif data in ['inbound_add', 'inbound_remove']:
+ await bot.edit_message_text(
+ '⏳ <b>In Progress...</b>',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML")
+ inbound = call.data.split(":")[2]
+ with GetDB() as db:
+ users = crud.get_users(db)
+ unsuccessful = 0
+ for user in users:
+ inbound_tags = [j for i in user.inbounds for j in user.inbounds[i]]
+ protocol = xray.config.inbounds_by_tag[inbound]['protocol']
+ new_inbounds = user.inbounds
+ if data == 'inbound_add':
+ if inbound not in inbound_tags:
+ if protocol in list(new_inbounds.keys()):
+ new_inbounds[protocol].append(inbound)
+ else:
+ new_inbounds[protocol] = [inbound]
+ elif data == 'inbound_remove':
+ if inbound in inbound_tags:
+ if len(new_inbounds[protocol]) == 1:
+ del new_inbounds[protocol]
+ else:
+ new_inbounds[protocol].remove(inbound)
+ if (data == 'inbound_remove' and inbound in inbound_tags)\
+ or (data == 'inbound_add' and inbound not in inbound_tags):
+ proxies = {p.type.value: p.settings for p in user.proxies}
+ for protocol in xray.config.inbounds_by_protocol:
+ if protocol in new_inbounds and protocol not in user.inbounds:
+ proxies.update({protocol: {'flow': TELEGRAM_DEFAULT_VLESS_FLOW} if
+ TELEGRAM_DEFAULT_VLESS_FLOW and protocol == ProxyTypes.VLESS else {}})
+ elif protocol in user.inbounds and protocol not in new_inbounds:
+ del proxies[protocol]
+ try:
+ user = crud.update_user(db, user, UserModify(inbounds=new_inbounds, proxies=proxies))
+ if user.status == UserStatus.active:
+ await xray.operations.update_user(user)
+ except:
+ db.rollback()
+ unsuccessful += 1
+
+ await bot.edit_message_text(
+ f'✅ <b>{data[8:].title()}</b> <code>{inbound}</code> <b>Users Successfully</b>'+\
+ (f'\n Unsuccessful: <code>{unsuccessful}</code>' if unsuccessful else ''),
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.main_menu())
+
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+✏️ <b>#Modified #Inbound_{data[8:].title()} #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Inbound:</b> <code>{inbound}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+ elif data == 'revoke_sub':
+ username = call.data.split(":")[2]
+ with GetDB() as db:
+ db_user = crud.get_user(db, username)
+ if not db_user:
+ return await bot.answer_callback_query(call.id, text=f"User not found!", show_alert=True)
+ db_user = crud.revoke_user_sub(db, db_user)
+ user = UserResponse.model_validate(db_user)
+ text = get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ expire=user.expire,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ note=user.note)
+ await bot.edit_message_text(
+ f'✅ Subscription Successfully Revoked!\n\n{text}',
+ call.message.chat.id,
+ call.message.message_id,
+ parse_mode="HTML",
+ reply_markup=BotKeyboard.user_menu(user_info={'status': user.status, 'username': user.username}, note=True))
+
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ text = f'''\
+🚫 <b>#Revoke_sub #From_Bot</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username:</b> <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <a href="tg://user?id={chat_id}">{full_name}</a>'''
+ try:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML')
+ except:
+ pass
+
+
+@bot.message_handler(func=lambda message: True, is_admin=True)
+async def search(message: types.Message):
+ with GetDB() as db:
+ db_user = crud.get_user(db, message.text)
+ if not db_user:
+ return await bot.reply_to(message, '❌ User not found.')
+ user = UserResponse.model_validate(db_user)
+ text = get_user_info_text(
+ status=user.status,
+ username=user.username,
+ sub_url=user.subscription_url,
+ expire=user.expire,
+ data_limit=user.data_limit,
+ usage=user.used_traffic,
+ note=user.note)
+ return await bot.reply_to(message, text, parse_mode="html", reply_markup=BotKeyboard.user_menu(user_info={
+ 'status': user.status,
+ 'username': user.username
+ }, note=True))
diff --git a/app/telegram/handlers/report.py b/app/telegram/handlers/report.py
@@ -0,0 +1,135 @@
+import datetime
+
+from app import logger
+from app.telegram import bot
+from telebot.apihelper import ApiTelegramException
+from datetime import datetime
+from app.telegram.utils.keyboard import BotKeyboard
+from app.utils.system import readable_size
+from config import TELEGRAM_ADMIN_ID, TELEGRAM_LOGGER_CHANNEL_ID
+from telebot.formatting import escape_html
+
+
+async def report(message: str, parse_mode="html", keyboard=None):
+ if bot and (TELEGRAM_ADMIN_ID or TELEGRAM_LOGGER_CHANNEL_ID):
+ try:
+ if TELEGRAM_LOGGER_CHANNEL_ID:
+ await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, message, parse_mode=parse_mode)
+ else:
+ for admin in TELEGRAM_ADMIN_ID:
+ await bot.send_message(admin, message, parse_mode=parse_mode, reply_markup=keyboard)
+ except ApiTelegramException as e:
+ logger.error(e)
+
+
+async def report_new_user(user_id: int, username: str, by: str, expire_date: int, data_limit: int, services: list):
+ text = '''\
+🆕 <b>#Created</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{username}</code>
+<b>Traffic Limit :</b> <code>{data_limit}</code>
+<b>Expire Date :</b> <code>{expire_date}</code>
+<b>Services :</b> <code>{services}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <b>#{by}</b>'''.format(
+ by=escape_html(by),
+ username=escape_html(username),
+ data_limit=readable_size(data_limit) if data_limit else "Unlimited",
+ expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never",
+ services="" if not services else ", ".join([s.name for s in services])
+ )
+
+ return await report(
+ text,
+ keyboard=BotKeyboard.user_menu({
+ 'username': username,
+ 'id': user_id,
+ 'status': 'active'
+ }, with_back=False)
+ )
+
+
+async def report_user_modification(username: str, expire_date: int, data_limit: int, services: list, by: str):
+ text = '''\
+✏️ <b>#Modified</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username :</b> <code>{username}</code>
+<b>Traffic Limit :</b> <code>{data_limit}</code>
+<b>Expire Date :</b> <code>{expire_date}</code>
+<b>Services :</b> <code>{services}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By :</b> <b>#{by}</b>\
+ '''.format(
+ by=escape_html(by),
+ username=escape_html(username),
+ data_limit=readable_size(data_limit) if data_limit else "Unlimited",
+ expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never",
+ services=", ".join([s.name for s in services])
+ )
+
+ return await report(text, keyboard=BotKeyboard.user_menu({
+ 'username': username,
+ 'status': 'active'
+ }, with_back=False))
+
+
+async def report_user_deletion(username: str, by: str):
+ text = '''\
+🗑 <b>#Deleted</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By</b> : <b>#{by}</b>\
+ '''.format(
+ by=escape_html(by),
+ username=escape_html(username)
+ )
+ return await report(text)
+
+
+async def report_status_change(username: str, status: str):
+ _status = {
+ 'active': '✅ <b>#Activated</b>',
+ 'disabled': '❌ <b>#Disabled</b>',
+ 'limited': '🪫 <b>#Limited</b>',
+ 'expired': '🕔 <b>#Expired</b>'
+ }
+ text = '''\
+{status}
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>\
+ '''.format(
+ username=escape_html(username),
+ status=_status[status]
+ )
+ return await report(text)
+
+
+async def report_user_usage_reset(username: str, by: str):
+ text = """
+🔁 <b>#Reset</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By</b> : <b>#{by}</b>\
+ """.format(
+ by=escape_html(by),
+ username=escape_html(username)
+ )
+
+ return await report(text)
+
+
+async def report_user_subscription_revoked(username: str, by: str):
+ text = """
+🔁 <b>#Revoked</b>
+➖➖➖➖➖➖➖➖➖
+<b>Username</b> : <code>{username}</code>
+➖➖➖➖➖➖➖➖➖
+<b>By</b> : <b>#{by}</b>\
+ """.format(
+ by=escape_html(by),
+ username=escape_html(username)
+ )
+
+ return await report(text)
diff --git a/app/telegram/handlers/user.py b/app/telegram/handlers/user.py
@@ -0,0 +1,43 @@
+from datetime import datetime
+from app.db import GetDB, crud
+from app.models.user import UserResponse
+from app.telegram import bot
+# from pytz import UTC
+from telebot.custom_filters import ChatFilter
+from telebot.util import extract_arguments
+
+from app.utils.system import readable_size
+
+bot.add_custom_filter(ChatFilter())
+
+
+@bot.message_handler(commands=['usage'])
+def usage_command(message):
+ username = extract_arguments(message.text)
+ if not username:
+ return bot.reply_to(message, 'Usage: `/usage <username>`', parse_mode='MarkdownV2')
+
+ with GetDB() as db:
+ dbuser = crud.get_user(db, username)
+
+ if not dbuser:
+ return bot.reply_to(message, "No user found with this username")
+ user = UserResponse.model_validate(dbuser)
+
+ statuses = {
+ 'active': '✅',
+ 'expired': '🕰',
+ 'limited': '📵',
+ 'disabled': '❌'}
+
+ text = f'''\
+┌─{statuses[user.status]} <b>Status:</b> <code>{user.status.title()}</code>
+│ └─<b>Username:</b> <code>{user.username}</code>
+│
+├─🔋 <b>Data limit:</b> <code>{readable_size(user.data_limit) if user.data_limit else 'Unlimited'}</code>
+│ └─<b>Data Used:</b> <code>{readable_size(user.used_traffic) if user.used_traffic else "-"}</code>
+│
+└─📅 <b>Expiry Date:</b> <code>{datetime.fromtimestamp(user.expire).date() if user.expire else 'Never'}</code>
+ └─<b>Days left:</b> <code>{(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}</code>'''
+
+ return bot.reply_to(message, text, parse_mode='HTML')
diff --git a/app/telegram/utils/__init__.py b/app/telegram/utils/__init__.py
@@ -0,0 +1,5 @@
+from app.telegram.utils import custom_filters
+
+
+def setup() -> None:
+ custom_filters.setup()
\ No newline at end of file
diff --git a/app/telegram/utils/custom_filters.py b/app/telegram/utils/custom_filters.py
@@ -0,0 +1,32 @@
+from app.telegram import bot
+
+from telebot import types
+from telebot.asyncio_filters import AdvancedCustomFilter, StateFilter
+
+from config import TELEGRAM_ADMIN_ID
+
+
+class IsAdminFilter(AdvancedCustomFilter):
+ key = 'is_admin'
+
+ async def check(self, message, text):
+ """
+ :meta private:
+ """
+ if isinstance(message, types.CallbackQuery):
+ return message.from_user.id in TELEGRAM_ADMIN_ID
+ return message.chat.id in TELEGRAM_ADMIN_ID
+
+
+def cb_query_equals(text: str):
+ return lambda query: query.data == text
+
+
+def cb_query_startswith(text: str):
+ return lambda query: query.data.startswith(text)
+
+
+
+def setup() -> None:
+ bot.add_custom_filter(IsAdminFilter())
+ bot.add_custom_filter(StateFilter(bot))
diff --git a/app/telegram/utils/keyboard.py b/app/telegram/utils/keyboard.py
@@ -0,0 +1,342 @@
+from datetime import datetime as dt
+from itertools import islice
+from typing import Literal, Dict, List
+
+from telebot import types # noqa
+
+from app import xray
+from app.utils.system import readable_size
+
+
+def chunk_dict(data: dict, size: int = 2):
+ it = iter(data)
+ for i in range(0, len(data), size):
+ yield {k: data[k] for k in islice(it, size)}
+
+
+class BotKeyboard:
+
+ @staticmethod
+ def main_menu():
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(text='🔁 System Info', callback_data='system'),
+ types.InlineKeyboardButton(text='♻️ Restart Xray', callback_data='restart'))
+ keyboard.add(
+ types.InlineKeyboardButton(text='👥 Users', callback_data='users:1'),
+ types.InlineKeyboardButton(text='✏️ Edit All Users', callback_data='edit_all'))
+ keyboard.add(
+ types.InlineKeyboardButton(text='➕ Create User From Template', callback_data='template_add_user'))
+ keyboard.add(
+ types.InlineKeyboardButton(text='➕ Create User', callback_data='add_user'))
+ return keyboard
+
+
+ @staticmethod
+ def edit_all_menu():
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(text='🗑 Delete Expired', callback_data='delete_expired'),
+ types.InlineKeyboardButton(text='🗑 Delete Limited', callback_data='delete_limited'))
+ keyboard.add(
+ types.InlineKeyboardButton(text='🔋 Data (➕|➖)', callback_data='add_data'),
+ types.InlineKeyboardButton(text='📅 Time (➕|➖)', callback_data='add_time'))
+ keyboard.add(
+ types.InlineKeyboardButton(text='➕ Add Inbound', callback_data='inbound_add'),
+ types.InlineKeyboardButton(text='➖ Remove Inbound', callback_data='inbound_remove'))
+ keyboard.add(types.InlineKeyboardButton(text='🔙 Back', callback_data='cancel'))
+ return keyboard
+
+
+ @staticmethod
+ def inbounds_menu(action, inbounds):
+ keyboard = types.InlineKeyboardMarkup()
+ for inbound in inbounds:
+ keyboard.add(types.InlineKeyboardButton(text=inbound, callback_data=f'confirm_{action}:{inbound}'))
+ keyboard.add(types.InlineKeyboardButton(text='🔙 Back', callback_data='cancel'))
+ return keyboard
+
+
+ @staticmethod
+ def templates_menu(templates: Dict[str, int], username: str = None):
+ keyboard = types.InlineKeyboardMarkup()
+
+ for chunk in chunk_dict(templates):
+ row = []
+ for name, _id in chunk.items():
+ row.append(
+ types.InlineKeyboardButton(
+ text=name,
+ callback_data=f'template_charge:{_id}:{username}' if username else f"template_add_user:{_id}"))
+ keyboard.add(*row)
+
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔙 Back',
+ callback_data=f'user:{username}' if username else 'cancel'))
+ return keyboard
+
+
+ @staticmethod
+ def random_username(template_id: str = ''):
+ keyboard = types.InlineKeyboardMarkup()
+
+ keyboard.add(types.InlineKeyboardButton(
+ text='🔡 Random Username',
+ callback_data=f'random:{template_id}'))
+ keyboard.add(types.InlineKeyboardButton(
+ text='🔙 Cancel',
+ callback_data='cancel'))
+ return keyboard
+
+
+ @staticmethod
+ def user_menu(user_info, with_back: bool = True, page: int = 1, note: bool = False):
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='❌ Disable' if user_info['status'] == 'active' else '✅ Activate',
+ callback_data=f"{'suspend' if user_info['status'] == 'active' else 'activate'}:{user_info['username']}"
+ ),
+ types.InlineKeyboardButton(
+ text='🗑 Delete',
+ callback_data=f"delete:{user_info['username']}"
+ ),
+ )
+ if note:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🚫 Revoke Sub',
+ callback_data=f"revoke_sub:{user_info['username']}"),
+ types.InlineKeyboardButton(
+ text='✏️ Edit',
+ callback_data=f"edit:{user_info['username']}"))
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='📝 Edit Note',
+ callback_data=f"edit_note:{user_info['username']}"),
+ types.InlineKeyboardButton(
+ text='📡 Links',
+ callback_data=f"links:{user_info['username']}"))
+ else:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='📡 Links',
+ callback_data=f"links:{user_info['username']}"),
+ types.InlineKeyboardButton(
+ text='✏️ Edit',
+ callback_data=f"edit:{user_info['username']}"))
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔁 Reset usage',
+ callback_data=f"reset_usage:{user_info['username']}"
+ ),
+ types.InlineKeyboardButton(
+ text='🔋 Charge',
+ callback_data=f"charge:{user_info['username']}"
+ )
+ )
+ if with_back:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔙 Back',
+ callback_data=f'users:{page}'
+ )
+ )
+ return keyboard
+
+ @staticmethod
+ def show_links(username: str):
+ keyboard = types.InlineKeyboardMarkup()
+
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="🖼 QR code",
+ callback_data=f'genqr:{username}'
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔙 Back',
+ callback_data=f'user:{username}'
+ )
+ )
+ return keyboard
+
+
+ @staticmethod
+ def subscription_page(sub_url: str):
+ keyboard = types.InlineKeyboardMarkup()
+ if sub_url[:4] == 'http':
+ keyboard.add(types.InlineKeyboardButton(
+ text='🚀 Subscription Page',
+ url=sub_url))
+ return keyboard
+
+
+ @staticmethod
+ def confirm_action(action: str, username: str = None):
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='Yes',
+ callback_data=f"confirm:{action}:{username}"
+ ),
+ types.InlineKeyboardButton(
+ text='No',
+ callback_data=f"cancel"
+ )
+ )
+ return keyboard
+
+ @staticmethod
+ def charge_add_or_reset(username: str, template_id: int):
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔰 Add to current',
+ callback_data=f"confirm:charge_add:{username}:{template_id}"
+ ),
+ types.InlineKeyboardButton(
+ text='♻️ Reset',
+ callback_data=f"confirm:charge_reset:{username}:{template_id}"
+ ))
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="Cancel",
+ callback_data=f'user:{username}'
+ )
+ )
+ return keyboard
+
+
+ @staticmethod
+ def inline_cancel_action(callback_data: str = "cancel"):
+ keyboard = types.InlineKeyboardMarkup()
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="🔙 Cancel",
+ callback_data=callback_data
+ )
+ )
+ return keyboard
+
+ @staticmethod
+ def user_list(users: list, page: int, total_pages: int):
+ keyboard = types.InlineKeyboardMarkup()
+ if len(users) >= 2:
+ users = [p for p in users]
+ users = [users[i:i + 2] for i in range(0, len(users), 2)]
+ else:
+ users = [users]
+ for user in users:
+ row = []
+ for p in user:
+ status = {
+ 'active': '✅',
+ 'expired': '🕰',
+ 'limited': '📵',
+ 'disabled': '❌'
+ }
+ row.append(types.InlineKeyboardButton(
+ text=f"{p.username} ({status[p.status]})",
+ callback_data=f'user:{p.username}:{page}'
+ ))
+ keyboard.row(*row)
+ # if there is more than one page
+ if total_pages > 1:
+ if page > 1:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="⬅️ Previous",
+ callback_data=f'users:{page - 1}'
+ )
+ )
+ if page < total_pages:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="➡️ Next",
+ callback_data=f'users:{page + 1}'
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='🔙 Back',
+ callback_data='cancel'
+ )
+ )
+ return keyboard
+
+ @staticmethod
+ def select_protocols(selected_protocols: Dict[str, List[str]],
+ action: Literal["edit", "create", "create_from_template"],
+ username: str = None,
+ data_limit: float = None,
+ expire_date: dt = None):
+ keyboard = types.InlineKeyboardMarkup()
+
+ if action == "edit":
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="⚠️ Data Limit:",
+ callback_data=f"help_edit"
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text=f"{readable_size(data_limit) if data_limit else 'Unlimited'}",
+ callback_data=f"help_edit"
+ ),
+ types.InlineKeyboardButton(
+ text="✏️ Edit",
+ callback_data=f"edit_user:{username}:data"
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text="📅 Expire Date:",
+ callback_data=f"help_edit"
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text=f"{expire_date.strftime('%Y-%m-%d') if expire_date else 'Never'}",
+ callback_data=f"help_edit"
+ ),
+ types.InlineKeyboardButton(
+ text="✏️ Edit",
+ callback_data=f"edit_user:{username}:expire"
+ )
+ )
+
+ if action != 'create_from_template':
+ for protocol, inbounds in xray.config.inbounds_by_protocol.items():
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text=f"🌐 {protocol.upper()} {'✅' if protocol in selected_protocols else '❌'}",
+ callback_data=f'select_protocol:{protocol}:{action}'
+ )
+ )
+ if protocol in selected_protocols:
+ for inbound in inbounds:
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text=f"«{inbound['tag']}» {'✅' if inbound['tag'] in selected_protocols[protocol] else '❌'}",
+ callback_data=f'select_inbound:{inbound["tag"]}:{action}'
+ )
+ )
+
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='Done',
+ callback_data='confirm:edit_user' if action == "edit" else 'confirm:add_user'
+ )
+ )
+ keyboard.add(
+ types.InlineKeyboardButton(
+ text='Cancel',
+ callback_data=f'user:{username}' if action == "edit" else 'cancel'
+ )
+ )
+
+ return keyboard
\ No newline at end of file
diff --git a/app/templates/__init__.py b/app/templates/__init__.py
@@ -0,0 +1,21 @@
+from datetime import datetime
+from typing import Union
+
+import jinja2
+
+from config import CUSTOM_TEMPLATES_DIRECTORY
+
+from .filters import CUSTOM_FILTERS
+
+template_directories = ["app/templates"]
+if CUSTOM_TEMPLATES_DIRECTORY:
+ # User's templates have priority over default templates
+ template_directories.insert(0, CUSTOM_TEMPLATES_DIRECTORY)
+
+env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_directories))
+env.filters.update(CUSTOM_FILTERS)
+env.globals['now'] = datetime.utcnow
+
+
+def render_template(template: str, context: Union[dict, None] = None) -> str:
+ return env.get_template(template).render(context or {})
diff --git a/app/templates/clash/default.yml b/app/templates/clash/default.yml
@@ -0,0 +1,13 @@
+mode: Global
+port: 7890
+
+{{ conf | except("proxy-groups", "port", "mode") | yaml }}
+
+proxy-groups:
+- name: '♻️ Automatic'
+ type: 'url-test'
+ url: 'http://www.gstatic.com/generate_204'
+ interval: 300
+ proxies:
+ {{ proxy_remarks | yaml | indent(2) }}
+{{ conf.get("proxy-groups", []) | yaml }}
diff --git a/app/templates/filters.py b/app/templates/filters.py
@@ -0,0 +1,41 @@
+import os
+from datetime import datetime
+
+import yaml
+
+from app.utils.system import readable_size
+
+
+def to_yaml(obj):
+ if not obj:
+ return ""
+
+ return yaml.dump(obj, allow_unicode=True, indent=2)
+
+
+def exclude_keys(obj, *target_keys):
+ return {key: val for key, val in obj.items() if key not in target_keys}
+
+
+def only_keys(obj, *target_keys):
+ return {key: val for key, val in obj.items() if key in target_keys}
+
+
+def datetimeformat(dt):
+ if isinstance(dt, int):
+ dt = datetime.fromtimestamp(dt)
+ formatted_datetime = dt.strftime('%Y-%m-%d %H:%M:%S')
+ return formatted_datetime
+
+
+def env_override(value, key):
+ return os.getenv(key, value)
+
+
+CUSTOM_FILTERS = {
+ "yaml": to_yaml,
+ "except": exclude_keys,
+ "only": only_keys,
+ "datetime": datetimeformat,
+ "bytesformat": readable_size
+}
diff --git a/app/templates/home/index.html b/app/templates/home/index.html
@@ -0,0 +1,1942 @@
+<style>
+ html,
+ body {
+ height: 100%;
+ }
+
+ body {
+ overflow: hidden;
+ margin: 0;
+ perspective: 80em;
+ perspective-origin: 50% 0%;
+ background: black;
+ color: cyan;
+ }
+
+ .scene {
+ transform: translate(-15em, 15em) rotateX(75deg) rotate(22.5deg);
+ }
+
+ .scene,
+ .scene * {
+ box-sizing: border-box;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform-style: preserve-3d;
+ }
+
+ .assembly:nth-child(1) {
+ z-index: -1;
+ transform: translate(0, -5em);
+ animation: slide1 1s ease-in-out infinite;
+ }
+
+ @keyframes slide1 {
+ to {
+ transform: translate(3em, -5em);
+ }
+ }
+
+ .assembly:nth-child(1) .roller {
+ transform: rotateY(90deg) translateZ(1.5em) rotateY(-270deg);
+ animation: inherit;
+ animation-name: roll1a;
+ }
+
+ .assembly:nth-child(1) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-90deg) translateZ(-1.5em) rotateY(-90deg);
+ animation-name: roll1b;
+ }
+
+ @keyframes roll1a {
+ to {
+ transform: rotateY(-90deg) translateZ(1.5em) rotateY(-270deg);
+ }
+ }
+
+ @keyframes roll1b {
+ to {
+ transform: rotateY(90deg) translateZ(-1.5em) rotateY(-90deg);
+ }
+ }
+
+ .assembly:nth-child(1) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(1) .side:nth-child(1) {
+ transform: rotateY(0deg);
+ }
+
+ .assembly:nth-child(1) .side:nth-child(2) {
+ transform: rotateY(180deg);
+ }
+
+ .assembly:nth-child(2) {
+ z-index: -2;
+ transform: translate(0, -10em);
+ animation: slide2 1s ease-in-out infinite;
+ }
+
+ @keyframes slide2 {
+ to {
+ transform: translate(3em, -10em);
+ }
+ }
+
+ .assembly:nth-child(2) .roller {
+ transform: rotateY(60deg) translateZ(1.73205em) rotateY(-120deg);
+ animation: inherit;
+ animation-name: roll2a;
+ }
+
+ .assembly:nth-child(2) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-60deg) translateZ(-1.73205em) rotateY(-60deg);
+ animation-name: roll2b;
+ }
+
+ @keyframes roll2a {
+ to {
+ transform: rotateY(-60deg) translateZ(1.73205em) rotateY(-120deg);
+ }
+ }
+
+ @keyframes roll2b {
+ to {
+ transform: rotateY(60deg) translateZ(-1.73205em) rotateY(-60deg);
+ }
+ }
+
+ .assembly:nth-child(2) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(2) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(0.86603em);
+ }
+
+ .assembly:nth-child(2) .side:nth-child(2) {
+ transform: rotateY(120deg) translateZ(0.86603em);
+ }
+
+ .assembly:nth-child(2) .side:nth-child(3) {
+ transform: rotateY(240deg) translateZ(0.86603em);
+ }
+
+ .assembly:nth-child(3) {
+ z-index: -3;
+ transform: translate(0, -15em);
+ animation: slide3 1s ease-in-out infinite;
+ }
+
+ @keyframes slide3 {
+ to {
+ transform: translate(3em, -15em);
+ }
+ }
+
+ .assembly:nth-child(3) .roller {
+ transform: rotateY(45deg) translateZ(2.12132em) rotateY(-135deg);
+ animation: inherit;
+ animation-name: roll3a;
+ }
+
+ .assembly:nth-child(3) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-45deg) translateZ(-2.12132em) rotateY(-45deg);
+ animation-name: roll3b;
+ }
+
+ @keyframes roll3a {
+ to {
+ transform: rotateY(-45deg) translateZ(2.12132em) rotateY(-135deg);
+ }
+ }
+
+ @keyframes roll3b {
+ to {
+ transform: rotateY(45deg) translateZ(-2.12132em) rotateY(-45deg);
+ }
+ }
+
+ .assembly:nth-child(3) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(3) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(1.5em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(1):before {
+ height: 1.5em;
+ left: .125em;
+ width: calc(3em - .125em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(2) {
+ transform: rotateY(90deg) translateZ(1.5em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(2):before {
+ height: 1.5em;
+ left: .125em;
+ width: calc(3em - .125em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(3) {
+ transform: rotateY(180deg) translateZ(1.5em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(3):before {
+ height: 1.5em;
+ left: .125em;
+ width: calc(3em - .125em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(4) {
+ transform: rotateY(270deg) translateZ(1.5em);
+ }
+
+ .assembly:nth-child(3) .side:nth-child(4):before {
+ height: 1.5em;
+ left: .125em;
+ width: calc(3em - .125em);
+ }
+
+ .assembly:nth-child(4) {
+ z-index: -4;
+ transform: translate(0, -20em);
+ animation: slide4 1s ease-in-out infinite;
+ }
+
+ @keyframes slide4 {
+ to {
+ transform: translate(3em, -20em);
+ }
+ }
+
+ .assembly:nth-child(4) .roller {
+ transform: rotateY(36deg) translateZ(2.55195em) rotateY(-72deg);
+ animation: inherit;
+ animation-name: roll4a;
+ }
+
+ .assembly:nth-child(4) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-36deg) translateZ(-2.55195em) rotateY(-36deg);
+ animation-name: roll4b;
+ }
+
+ @keyframes roll4a {
+ to {
+ transform: rotateY(-36deg) translateZ(2.55195em) rotateY(-72deg);
+ }
+ }
+
+ @keyframes roll4b {
+ to {
+ transform: rotateY(36deg) translateZ(-2.55195em) rotateY(-36deg);
+ }
+ }
+
+ .assembly:nth-child(4) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(4) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(2.06457em);
+ }
+
+ .assembly:nth-child(4) .side:nth-child(1):before {
+ height: 2.06457em;
+ }
+
+ .assembly:nth-child(4) .side:nth-child(2) {
+ transform: rotateY(72deg) translateZ(2.06457em);
+ }
+
+ .assembly:nth-child(4) .side:nth-child(2):before {
+ height: 2.06457em;
+ }
+
+ .assembly:nth-child(4) .side:nth-child(3) {
+ transform: rotateY(144deg) translateZ(2.06457em);
+ }
+
+ .assembly:nth-child(4) .side:nth-child(3):before {
+ height: 2.06457em;
+ }
+
+ .assembly:nth-child(4) .side:nth-child(4) {
+ transform: rotateY(216deg) translateZ(2.06457em);
+ }
+
+ .assembly:nth-child(4) .side:nth-child(4):before {
+ height: 2.06457em;
+ }
+
+ .assembly:nth-child(4) .side:nth-child(5) {
+ transform: rotateY(288deg) translateZ(2.06457em);
+ }
+
+ .assembly:nth-child(4) .side:nth-child(5):before {
+ height: 2.06457em;
+ }
+
+ .assembly:nth-child(5) {
+ z-index: -5;
+ transform: translate(0, -25em);
+ animation: slide5 1s ease-in-out infinite;
+ }
+
+ @keyframes slide5 {
+ to {
+ transform: translate(3em, -25em);
+ }
+ }
+
+ .assembly:nth-child(5) .roller {
+ transform: rotateY(30deg) translateZ(3em) rotateY(-90deg);
+ animation: inherit;
+ animation-name: roll5a;
+ }
+
+ .assembly:nth-child(5) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-30deg) translateZ(-3em) rotateY(-30deg);
+ animation-name: roll5b;
+ }
+
+ @keyframes roll5a {
+ to {
+ transform: rotateY(-30deg) translateZ(3em) rotateY(-90deg);
+ }
+ }
+
+ @keyframes roll5b {
+ to {
+ transform: rotateY(30deg) translateZ(-3em) rotateY(-30deg);
+ }
+ }
+
+ .assembly:nth-child(5) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(1):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(2) {
+ transform: rotateY(60deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(2):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(3) {
+ transform: rotateY(120deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(3):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(4) {
+ transform: rotateY(180deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(4):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(5) {
+ transform: rotateY(240deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(5):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(5) .side:nth-child(6) {
+ transform: rotateY(300deg) translateZ(2.59808em);
+ }
+
+ .assembly:nth-child(5) .side:nth-child(6):before {
+ height: 2.59808em;
+ }
+
+ .assembly:nth-child(6) {
+ z-index: -6;
+ transform: translate(0, -30em);
+ animation: slide6 1s ease-in-out infinite;
+ }
+
+ @keyframes slide6 {
+ to {
+ transform: translate(3em, -30em);
+ }
+ }
+
+ .assembly:nth-child(6) .roller {
+ transform: rotateY(25.71429deg) translateZ(3.45715em) rotateY(-51.42857deg);
+ animation: inherit;
+ animation-name: roll6a;
+ }
+
+ .assembly:nth-child(6) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-25.71429deg) translateZ(-3.45715em) rotateY(-25.71429deg);
+ animation-name: roll6b;
+ }
+
+ @keyframes roll6a {
+ to {
+ transform: rotateY(-25.71429deg) translateZ(3.45715em) rotateY(-51.42857deg);
+ }
+ }
+
+ @keyframes roll6b {
+ to {
+ transform: rotateY(25.71429deg) translateZ(-3.45715em) rotateY(-25.71429deg);
+ }
+ }
+
+ .assembly:nth-child(6) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(1):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(2) {
+ transform: rotateY(51.42857deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(2):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(3) {
+ transform: rotateY(102.85714deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(3):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(4) {
+ transform: rotateY(154.28571deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(4):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(5) {
+ transform: rotateY(205.71429deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(5):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(6) {
+ transform: rotateY(257.14286deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(6):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(6) .side:nth-child(7) {
+ transform: rotateY(308.57143deg) translateZ(3.11478em);
+ }
+
+ .assembly:nth-child(6) .side:nth-child(7):before {
+ height: 3.11478em;
+ }
+
+ .assembly:nth-child(7) {
+ z-index: -7;
+ transform: translate(0, -35em);
+ animation: slide7 1s ease-in-out infinite;
+ }
+
+ @keyframes slide7 {
+ to {
+ transform: translate(3em, -35em);
+ }
+ }
+
+ .assembly:nth-child(7) .roller {
+ transform: rotateY(22.5deg) translateZ(3.91969em) rotateY(-67.5deg);
+ animation: inherit;
+ animation-name: roll7a;
+ }
+
+ .assembly:nth-child(7) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-22.5deg) translateZ(-3.91969em) rotateY(-22.5deg);
+ animation-name: roll7b;
+ }
+
+ @keyframes roll7a {
+ to {
+ transform: rotateY(-22.5deg) translateZ(3.91969em) rotateY(-67.5deg);
+ }
+ }
+
+ @keyframes roll7b {
+ to {
+ transform: rotateY(22.5deg) translateZ(-3.91969em) rotateY(-22.5deg);
+ }
+ }
+
+ .assembly:nth-child(7) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(1):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(2) {
+ transform: rotateY(45deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(2):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(3) {
+ transform: rotateY(90deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(3):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(4) {
+ transform: rotateY(135deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(4):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(5) {
+ transform: rotateY(180deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(5):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(6) {
+ transform: rotateY(225deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(6):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(7) {
+ transform: rotateY(270deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(7):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(7) .side:nth-child(8) {
+ transform: rotateY(315deg) translateZ(3.62132em);
+ }
+
+ .assembly:nth-child(7) .side:nth-child(8):before {
+ height: 3.62132em;
+ }
+
+ .assembly:nth-child(8) {
+ z-index: -8;
+ transform: translate(0, -40em);
+ animation: slide8 1s ease-in-out infinite;
+ }
+
+ @keyframes slide8 {
+ to {
+ transform: translate(3em, -40em);
+ }
+ }
+
+ .assembly:nth-child(8) .roller {
+ transform: rotateY(20deg) translateZ(4.38571em) rotateY(-40deg);
+ animation: inherit;
+ animation-name: roll8a;
+ }
+
+ .assembly:nth-child(8) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-20deg) translateZ(-4.38571em) rotateY(-20deg);
+ animation-name: roll8b;
+ }
+
+ @keyframes roll8a {
+ to {
+ transform: rotateY(-20deg) translateZ(4.38571em) rotateY(-40deg);
+ }
+ }
+
+ @keyframes roll8b {
+ to {
+ transform: rotateY(20deg) translateZ(-4.38571em) rotateY(-20deg);
+ }
+ }
+
+ .assembly:nth-child(8) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(1):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(2) {
+ transform: rotateY(40deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(2):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(3) {
+ transform: rotateY(80deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(3):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(4) {
+ transform: rotateY(120deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(4):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(5) {
+ transform: rotateY(160deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(5):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(6) {
+ transform: rotateY(200deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(6):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(7) {
+ transform: rotateY(240deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(7):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(8) {
+ transform: rotateY(280deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(8):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(8) .side:nth-child(9) {
+ transform: rotateY(320deg) translateZ(4.12122em);
+ }
+
+ .assembly:nth-child(8) .side:nth-child(9):before {
+ height: 4.12122em;
+ }
+
+ .assembly:nth-child(9) {
+ z-index: -9;
+ transform: translate(0, -45em);
+ animation: slide9 1s ease-in-out infinite;
+ }
+
+ @keyframes slide9 {
+ to {
+ transform: translate(3em, -45em);
+ }
+ }
+
+ .assembly:nth-child(9) .roller {
+ transform: rotateY(18deg) translateZ(4.8541em) rotateY(-54deg);
+ animation: inherit;
+ animation-name: roll9a;
+ }
+
+ .assembly:nth-child(9) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-18deg) translateZ(-4.8541em) rotateY(-18deg);
+ animation-name: roll9b;
+ }
+
+ @keyframes roll9a {
+ to {
+ transform: rotateY(-18deg) translateZ(4.8541em) rotateY(-54deg);
+ }
+ }
+
+ @keyframes roll9b {
+ to {
+ transform: rotateY(18deg) translateZ(-4.8541em) rotateY(-18deg);
+ }
+ }
+
+ .assembly:nth-child(9) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(1):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(2) {
+ transform: rotateY(36deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(2):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(3) {
+ transform: rotateY(72deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(3):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(4) {
+ transform: rotateY(108deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(4):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(5) {
+ transform: rotateY(144deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(5):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(6) {
+ transform: rotateY(180deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(6):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(7) {
+ transform: rotateY(216deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(7):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(8) {
+ transform: rotateY(252deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(8):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(9) {
+ transform: rotateY(288deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(9):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(9) .side:nth-child(10) {
+ transform: rotateY(324deg) translateZ(4.61653em);
+ }
+
+ .assembly:nth-child(9) .side:nth-child(10):before {
+ height: 4.61653em;
+ }
+
+ .assembly:nth-child(10) {
+ z-index: -10;
+ transform: translate(0, -50em);
+ animation: slide10 1s ease-in-out infinite;
+ }
+
+ @keyframes slide10 {
+ to {
+ transform: translate(3em, -50em);
+ }
+ }
+
+ .assembly:nth-child(10) .roller {
+ transform: rotateY(16.36364deg) translateZ(5.3242em) rotateY(-32.72727deg);
+ animation: inherit;
+ animation-name: roll10a;
+ }
+
+ .assembly:nth-child(10) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-16.36364deg) translateZ(-5.3242em) rotateY(-16.36364deg);
+ animation-name: roll10b;
+ }
+
+ @keyframes roll10a {
+ to {
+ transform: rotateY(-16.36364deg) translateZ(5.3242em) rotateY(-32.72727deg);
+ }
+ }
+
+ @keyframes roll10b {
+ to {
+ transform: rotateY(16.36364deg) translateZ(-5.3242em) rotateY(-16.36364deg);
+ }
+ }
+
+ .assembly:nth-child(10) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(1):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(2) {
+ transform: rotateY(32.72727deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(2):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(3) {
+ transform: rotateY(65.45455deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(3):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(4) {
+ transform: rotateY(98.18182deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(4):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(5) {
+ transform: rotateY(130.90909deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(5):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(6) {
+ transform: rotateY(163.63636deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(6):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(7) {
+ transform: rotateY(196.36364deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(7):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(8) {
+ transform: rotateY(229.09091deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(8):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(9) {
+ transform: rotateY(261.81818deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(9):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(10) {
+ transform: rotateY(294.54545deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(10):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(10) .side:nth-child(11) {
+ transform: rotateY(327.27273deg) translateZ(5.10853em);
+ }
+
+ .assembly:nth-child(10) .side:nth-child(11):before {
+ height: 5.10853em;
+ }
+
+ .assembly:nth-child(11) {
+ z-index: -11;
+ transform: translate(0, -55em);
+ animation: slide11 1s ease-in-out infinite;
+ }
+
+ @keyframes slide11 {
+ to {
+ transform: translate(3em, -55em);
+ }
+ }
+
+ .assembly:nth-child(11) .roller {
+ transform: rotateY(15deg) translateZ(5.79555em) rotateY(-45deg);
+ animation: inherit;
+ animation-name: roll11a;
+ }
+
+ .assembly:nth-child(11) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-15deg) translateZ(-5.79555em) rotateY(-15deg);
+ animation-name: roll11b;
+ }
+
+ @keyframes roll11a {
+ to {
+ transform: rotateY(-15deg) translateZ(5.79555em) rotateY(-45deg);
+ }
+ }
+
+ @keyframes roll11b {
+ to {
+ transform: rotateY(15deg) translateZ(-5.79555em) rotateY(-15deg);
+ }
+ }
+
+ .assembly:nth-child(11) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(1):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(2) {
+ transform: rotateY(30deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(2):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(3) {
+ transform: rotateY(60deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(3):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(4) {
+ transform: rotateY(90deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(4):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(5) {
+ transform: rotateY(120deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(5):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(6) {
+ transform: rotateY(150deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(6):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(7) {
+ transform: rotateY(180deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(7):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(8) {
+ transform: rotateY(210deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(8):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(9) {
+ transform: rotateY(240deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(9):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(10) {
+ transform: rotateY(270deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(10):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(11) {
+ transform: rotateY(300deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(11):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(11) .side:nth-child(12) {
+ transform: rotateY(330deg) translateZ(5.59808em);
+ }
+
+ .assembly:nth-child(11) .side:nth-child(12):before {
+ height: 5.59808em;
+ }
+
+ .assembly:nth-child(12) {
+ z-index: -12;
+ transform: translate(0, -60em);
+ animation: slide12 1s ease-in-out infinite;
+ }
+
+ @keyframes slide12 {
+ to {
+ transform: translate(3em, -60em);
+ }
+ }
+
+ .assembly:nth-child(12) .roller {
+ transform: rotateY(13.84615deg) translateZ(6.26787em) rotateY(-27.69231deg);
+ animation: inherit;
+ animation-name: roll12a;
+ }
+
+ .assembly:nth-child(12) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-13.84615deg) translateZ(-6.26787em) rotateY(-13.84615deg);
+ animation-name: roll12b;
+ }
+
+ @keyframes roll12a {
+ to {
+ transform: rotateY(-13.84615deg) translateZ(6.26787em) rotateY(-27.69231deg);
+ }
+ }
+
+ @keyframes roll12b {
+ to {
+ transform: rotateY(13.84615deg) translateZ(-6.26787em) rotateY(-13.84615deg);
+ }
+ }
+
+ .assembly:nth-child(12) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(1):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(2) {
+ transform: rotateY(27.69231deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(2):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(3) {
+ transform: rotateY(55.38462deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(3):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(4) {
+ transform: rotateY(83.07692deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(4):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(5) {
+ transform: rotateY(110.76923deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(5):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(6) {
+ transform: rotateY(138.46154deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(6):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(7) {
+ transform: rotateY(166.15385deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(7):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(8) {
+ transform: rotateY(193.84615deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(8):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(9) {
+ transform: rotateY(221.53846deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(9):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(10) {
+ transform: rotateY(249.23077deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(10):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(11) {
+ transform: rotateY(276.92308deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(11):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(12) {
+ transform: rotateY(304.61538deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(12):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(12) .side:nth-child(13) {
+ transform: rotateY(332.30769deg) translateZ(6.08574em);
+ }
+
+ .assembly:nth-child(12) .side:nth-child(13):before {
+ height: 6.08574em;
+ }
+
+ .assembly:nth-child(13) {
+ z-index: -13;
+ transform: translate(0, -65em);
+ animation: slide13 1s ease-in-out infinite;
+ }
+
+ @keyframes slide13 {
+ to {
+ transform: translate(3em, -65em);
+ }
+ }
+
+ .assembly:nth-child(13) .roller {
+ transform: rotateY(12.85714deg) translateZ(6.74094em) rotateY(-38.57143deg);
+ animation: inherit;
+ animation-name: roll13a;
+ }
+
+ .assembly:nth-child(13) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-12.85714deg) translateZ(-6.74094em) rotateY(-12.85714deg);
+ animation-name: roll13b;
+ }
+
+ @keyframes roll13a {
+ to {
+ transform: rotateY(-12.85714deg) translateZ(6.74094em) rotateY(-38.57143deg);
+ }
+ }
+
+ @keyframes roll13b {
+ to {
+ transform: rotateY(12.85714deg) translateZ(-6.74094em) rotateY(-12.85714deg);
+ }
+ }
+
+ .assembly:nth-child(13) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(1):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(2) {
+ transform: rotateY(25.71429deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(2):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(3) {
+ transform: rotateY(51.42857deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(3):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(4) {
+ transform: rotateY(77.14286deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(4):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(5) {
+ transform: rotateY(102.85714deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(5):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(6) {
+ transform: rotateY(128.57143deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(6):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(7) {
+ transform: rotateY(154.28571deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(7):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(8) {
+ transform: rotateY(180deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(8):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(9) {
+ transform: rotateY(205.71429deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(9):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(10) {
+ transform: rotateY(231.42857deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(10):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(11) {
+ transform: rotateY(257.14286deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(11):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(12) {
+ transform: rotateY(282.85714deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(12):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(13) {
+ transform: rotateY(308.57143deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(13):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(13) .side:nth-child(14) {
+ transform: rotateY(334.28571deg) translateZ(6.57193em);
+ }
+
+ .assembly:nth-child(13) .side:nth-child(14):before {
+ height: 6.57193em;
+ }
+
+ .assembly:nth-child(14) {
+ z-index: -14;
+ transform: translate(0, -70em);
+ animation: slide14 1s ease-in-out infinite;
+ }
+
+ @keyframes slide14 {
+ to {
+ transform: translate(3em, -70em);
+ }
+ }
+
+ .assembly:nth-child(14) .roller {
+ transform: rotateY(12deg) translateZ(7.2146em) rotateY(-24deg);
+ animation: inherit;
+ animation-name: roll14a;
+ }
+
+ .assembly:nth-child(14) .roller:last-child {
+ z-index: -1;
+ margin: 0 12em;
+ transform: rotateY(-12deg) translateZ(-7.2146em) rotateY(-12deg);
+ animation-name: roll14b;
+ }
+
+ @keyframes roll14a {
+ to {
+ transform: rotateY(-12deg) translateZ(7.2146em) rotateY(-24deg);
+ }
+ }
+
+ @keyframes roll14b {
+ to {
+ transform: rotateY(12deg) translateZ(-7.2146em) rotateY(-12deg);
+ }
+ }
+
+ .assembly:nth-child(14) .side {
+ margin: -2em -1.5em;
+ width: 3em;
+ height: 4em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(1) {
+ transform: rotateY(0deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(1):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(2) {
+ transform: rotateY(24deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(2):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(3) {
+ transform: rotateY(48deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(3):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(4) {
+ transform: rotateY(72deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(4):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(5) {
+ transform: rotateY(96deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(5):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(6) {
+ transform: rotateY(120deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(6):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(7) {
+ transform: rotateY(144deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(7):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(8) {
+ transform: rotateY(168deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(8):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(9) {
+ transform: rotateY(192deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(9):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(10) {
+ transform: rotateY(216deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(10):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(11) {
+ transform: rotateY(240deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(11):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(12) {
+ transform: rotateY(264deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(12):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(13) {
+ transform: rotateY(288deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(13):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(14) {
+ transform: rotateY(312deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(14):before {
+ height: 7.05695em;
+ }
+
+ .assembly:nth-child(14) .side:nth-child(15) {
+ transform: rotateY(336deg) translateZ(7.05695em);
+ }
+
+ .assembly:nth-child(14) .side:nth-child(15):before {
+ height: 7.05695em;
+ }
+
+ .strip {
+ margin: -2em -1px;
+ width: 12em;
+ height: 4em;
+ border: solid 0.125em cyan;
+ background: linear-gradient(90deg, #00ffff 0.125em, rgba(0, 0, 0, 0) 0%) black;
+ background-size: 3em 100%;
+ }
+
+ .side:before,
+ .side:after {
+ box-sizing: border-box;
+ position: absolute;
+ left: 0;
+ width: inherit;
+ backface-visibility: hidden;
+ content: '';
+ }
+
+ .side:before {
+ top: 100%;
+ height: .125em;
+ transform-origin: 50% 0;
+ transform: rotateX(-90deg);
+ border-top: solid 0.125em cyan;
+ background: black;
+ }
+
+ .side:after {
+ height: inherit;
+ border: solid 0.125em cyan;
+ background: black;
+ }
+</style>
+<div class='scene'>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+ <div class='assembly'>
+ <div class='strip'></div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ <div class='roller'>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ <div class='side'></div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/app/templates/singbox/default.json b/app/templates/singbox/default.json
@@ -0,0 +1,100 @@
+{
+ "log": {
+ "level": "warn",
+ "timestamp": true
+ },
+ "dns": {
+ "servers": [
+ {
+ "tag": "dns_proxy",
+ "address": "1.1.1.1",
+ "address_resolver": "dns_direct",
+ "strategy": "prefer_ipv4",
+ "detour": "Internet"
+ },
+ {
+ "tag": "dns_direct",
+ "address": "local",
+ "strategy": "prefer_ipv4",
+ "detour": "direct"
+ },
+ {
+ "tag": "dns_block",
+ "address": "rcode://success"
+ }
+ ],
+ "rules": [
+ {
+ "geosite": "private",
+ "server": "dns_direct"
+ },
+ {
+ "outbound": "any",
+ "server": "dns_direct"
+ }
+ ],
+ "fakeip": {},
+ "independent_cache": true
+ },
+ "inbounds": [
+ {
+ "type": "tun",
+ "mtu": 1400,
+ "inet4_address": "172.19.0.1/30",
+ "inet6_address": "fdfe:dcba:9876::1/126",
+ "auto_route": true,
+ "strict_route": true,
+ "endpoint_independent_nat": true,
+ "stack": "mixed",
+ "sniff": true
+ }
+ ],
+ "outbounds": [
+ {
+ "type": "selector",
+ "tag": "Internet",
+ "outbounds": null
+ },
+ {
+ "type": "urltest",
+ "tag": "Best Latency",
+ "outbounds": null,
+ "url": "https://www.gstatic.com/generate_204",
+ "interval": "1m0s"
+ },
+ {
+ "type": "direct",
+ "tag": "direct"
+ },
+ {
+ "type": "block",
+ "tag": "block"
+ },
+ {
+ "type": "dns",
+ "tag": "dns-out"
+ }
+ ],
+ "route": {
+ "geoip": {
+ "download_url": "https://github.com/soffchen/sing-geoip/releases/latest/download/geoip.db",
+ "download_detour": "direct"
+ },
+ "geosite": {
+ "download_url": "https://github.com/soffchen/sing-geosite/releases/latest/download/geosite.db",
+ "download_detour": "direct"
+ },
+ "rules": [
+ {
+ "protocol": "dns",
+ "outbound": "dns-out"
+ },
+ {
+ "geoip": "private",
+ "outbound": "direct"
+ }
+ ],
+ "auto_detect_interface": true
+ },
+ "experimental": {}
+}
diff --git a/app/templates/subscription/index.html b/app/templates/subscription/index.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Subscription Information</title>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ padding: 20px;
+ }
+
+ h1 {
+ margin-top: 0;
+ }
+
+ .link-input {
+ margin-bottom: 10px;
+ }
+
+ .copy-button {
+ margin-left: 10px;
+ }
+
+ .status {
+ display: inline-block;
+ padding: 3px 8px;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 16px;
+ line-height: 1;
+ }
+
+ .active {
+ background-color: #4CAF50;
+ color: white;
+ }
+
+ .limited {
+ background-color: #F44336;
+ color: white;
+ }
+
+ .expired {
+ background-color: #FF9800;
+ color: white;
+ }
+
+ .disabled {
+ background-color: #9E9E9E;
+ color: white;
+ }
+
+ .qr-popup {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: white;
+ padding: 10px 25px 25px 25px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ display: none;
+ z-index: 9999;
+ }
+
+ .qr-close-button {
+ text-align: right;
+ margin-bottom: 5px;
+ margin-right: -15px;
+ }
+ </style>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
+</head>
+
+<body>
+ <h1>User Information</h1>
+ <p>Username: {{ user.username }}</p>
+ <p>Status: <span
+ class="status {% if user.status.value == 'active' %}active{% elif user.status.value == 'limited' %}limited{% elif user.status.value == 'expired' %}expired{% elif user.status.value == 'disabled' %}disabled{% endif %}">{{
+ user.status.value }}</span></p>
+ <p>Data Limit: {% if not user.data_limit %}∞{% else %}{{ user.data_limit | bytesformat }}{% endif %}</p>
+ <p>Data Used: {{ user.used_traffic | bytesformat }}{% if user.data_limit_reset_strategy != 'no_reset' %} (resets
+ every {{ user.data_limit_reset_strategy.value }}){% endif %}</p>
+ <p>Expiration Date:
+ {% if not user.expire %}
+ ∞
+ {% else %}
+ {% set current_timestamp = now().timestamp() %}
+ {% set remaining_days = ((user.expire - current_timestamp) // (24 * 3600)) %}
+ {{ user.expire | datetime }} ({{ remaining_days | int }} days
+ remaining)
+ {% endif %}</p>
+
+ {% if user.status == 'active' %}
+ <h2>Links:</h2>
+ <ul>
+ {% for link in user.links %}
+ <li>
+ <input type="text" value="{{ link }}" readonly>
+ <button class="copy-button" onclick="copyLink('{{ link }}', this)">Copy</button>
+ <button class="qr-button" data-link="{{ link }}">QR Code</button>
+ </li>
+ {% endfor %}
+ </ul>
+ <div class="qr-popup" id="qrPopup">
+ <div class="qr-close-button">
+ <button onclick="closeQrPopup()">X</button>
+ </div>
+ <div id="qrCodeContainer"></div>
+ </div>
+ {% endif %}
+
+ <script>
+ function copyLink(link, button) {
+ const tempInput = document.createElement('input');
+ tempInput.setAttribute('value', link);
+ document.body.appendChild(tempInput);
+ tempInput.select();
+ document.execCommand('copy');
+ document.body.removeChild(tempInput);
+
+ button.textContent = 'Copied!';
+ setTimeout(function () {
+ button.textContent = 'Copy';
+ }, 1500);
+ }
+
+ const qrButtons = document.querySelectorAll('.qr-button');
+ const qrPopup = document.getElementById('qrPopup');
+
+ qrButtons.forEach((qrButton) => {
+ qrButton.addEventListener('click', () => {
+ const link = qrButton.dataset.link;
+ while (qrCodeContainer.firstChild) {
+ qrCodeContainer.removeChild(qrCodeContainer.firstChild);
+ }
+ const qrCode = new QRCode(qrCodeContainer, {
+ text: link,
+ width: 256,
+ height: 256
+ });
+ qrPopup.style.display = 'block';
+ });
+ });
+ function closeQrPopup() {
+ document.getElementById('qrPopup').style.display = 'none';
+ }
+ </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/app/utils/crypto.py b/app/utils/crypto.py
@@ -0,0 +1,33 @@
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from OpenSSL import crypto
+
+
+def get_cert_SANs(cert: bytes):
+ cert = x509.load_pem_x509_certificate(cert, default_backend())
+ san_list = []
+ for extension in cert.extensions:
+ if isinstance(extension.value, x509.SubjectAlternativeName):
+ san = extension.value
+ for name in san:
+ san_list.append(name.value)
+ return san_list
+
+
+def generate_certificate():
+ k = crypto.PKey()
+ k.generate_key(crypto.TYPE_RSA, 4096)
+ cert = crypto.X509()
+ cert.get_subject().CN = "Gozargah"
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(100*365*24*60*60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(k)
+ cert.sign(k, 'sha512')
+ cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")
+ key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8")
+
+ return {
+ "cert": cert_pem,
+ "key": key_pem
+ }
diff --git a/app/utils/helpers.py b/app/utils/helpers.py
@@ -0,0 +1,9 @@
+from datetime import datetime as dt
+
+
+def calculate_usage_percent(used_traffic: int, data_limit: int) -> float:
+ return (used_traffic * 100) / data_limit
+
+
+def calculate_expiration_days(expire: int) -> int:
+ return (dt.fromtimestamp(expire) - dt.utcnow()).days
diff --git a/app/utils/jwt.py b/app/utils/jwt.py
@@ -0,0 +1,57 @@
+from datetime import datetime, timedelta
+from functools import lru_cache
+from typing import Union
+
+from jose import JWTError, jwt
+
+from config import JWT_ACCESS_TOKEN_EXPIRE_MINUTES
+
+
+@lru_cache(maxsize=None)
+def get_secret_key():
+ from app.db import GetDB, get_jwt_secret_key
+ with GetDB() as db:
+ return get_jwt_secret_key(db)
+
+
+def create_admin_token(username: str, is_sudo=False) -> str:
+ data = {"sub": username, "access": "sudo" if is_sudo else "admin", "iat": datetime.utcnow()}
+ if JWT_ACCESS_TOKEN_EXPIRE_MINUTES > 0:
+ expire = datetime.utcnow() + timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
+ data["exp"] = expire
+ encoded_jwt = jwt.encode(data, get_secret_key(), algorithm="HS256")
+ return encoded_jwt
+
+
+def get_admin_payload(token: str) -> Union[dict, None]:
+ try:
+ payload = jwt.decode(token, get_secret_key(), algorithms=["HS256"])
+ username: str = payload.get("sub")
+ access: str = payload.get("access")
+ if not username or access not in ('admin', 'sudo'):
+ return
+ try:
+ created_at = datetime.utcfromtimestamp(payload['iat'])
+ except KeyError:
+ created_at = None
+
+ return {"username": username, "is_sudo": access == "sudo", "created_at": created_at}
+ except JWTError:
+ return
+
+
+def create_subscription_token(username: str) -> str:
+ data = {"sub": username, "access": "subscription", "iat": datetime.utcnow()+timedelta(seconds=1)}
+ encoded_jwt = jwt.encode(data, get_secret_key(), algorithm="HS256")
+ return encoded_jwt
+
+
+def get_subscription_payload(token: str) -> Union[dict, None]:
+ try:
+ payload = jwt.decode(token, get_secret_key(), algorithms=["HS256"])
+ if payload.get("access") != "subscription":
+ return
+
+ return {"username": payload['sub'], "created_at": datetime.utcfromtimestamp(payload['iat'])}
+ except JWTError:
+ return
diff --git a/app/utils/keygen.py b/app/utils/keygen.py
@@ -0,0 +1,17 @@
+# import hashlib
+import xxhash
+import uuid
+from app.models.proxy import ProxyTypes
+
+def gen_uuid(key: str) -> str:
+ return str(uuid.UUID(bytes=xxhash.xxh128(key.encode()).digest()))
+
+def gen_password(key: str) -> str:
+ return xxhash.xxh128(key.encode()).hexdigest()
+
+def generate_settings(key: str, protocol: ProxyTypes) -> dict:
+ if protocol in (ProxyTypes.Trojan, ProxyTypes.Shadowsocks):
+ return ProxyTypes(protocol).settings_model(password=gen_password(key)).dict(no_obj=True)
+ elif protocol in (ProxyTypes.VMess, ProxyTypes.VLESS):
+ return ProxyTypes(protocol).settings_model(id=gen_uuid(key)).dict(no_obj=True)
+
diff --git a/app/utils/notification.py b/app/utils/notification.py
@@ -0,0 +1,105 @@
+from collections import deque
+from datetime import datetime as dt
+from enum import Enum
+from typing import Type
+
+from pydantic import BaseModel
+
+import config
+from app.models.admin import Admin
+from app.models.user import UserResponse
+
+queue = deque()
+
+
+class Notification(BaseModel):
+ class Type(str, Enum):
+ user_created = "user_created"
+ user_updated = "user_updated"
+ user_deleted = "user_deleted"
+ user_limited = "user_limited"
+ user_expired = "user_expired"
+ user_enabled = "user_enabled"
+ user_disabled = "user_disabled"
+ data_usage_reset = "data_usage_reset"
+ subscription_revoked = "subscription_revoked"
+
+ reached_usage_percent = "reached_usage_percent"
+ reached_days_left = "reached_days_left"
+
+ enqueued_at: float = dt.utcnow().timestamp()
+ send_at: float = dt.utcnow().timestamp()
+ tries: int = 0
+
+
+class UserNotification(Notification):
+ username: str
+
+
+class ReachedUsagePercent(UserNotification):
+ action: Notification.Type = Notification.Type.reached_usage_percent
+ user: UserResponse
+ used_percent: float
+
+
+class ReachedDaysLeft(UserNotification):
+ action: Notification.Type = Notification.Type.reached_days_left
+ user: UserResponse
+ days_left: int
+
+
+class UserCreated(UserNotification):
+ action: Notification.Type = Notification.Type.user_created
+ by: Admin
+ user: UserResponse
+
+
+class UserUpdated(UserNotification):
+ action: Notification.Type = Notification.Type.user_updated
+ by: Admin
+ user: UserResponse
+
+
+class UserDeleted(UserNotification):
+ action: Notification.Type = Notification.Type.user_deleted
+ by: Admin
+
+
+class UserLimited(UserNotification):
+ action: Notification.Type = Notification.Type.user_limited
+ user: UserResponse
+
+
+class UserExpired(UserNotification):
+ action: Notification.Type = Notification.Type.user_expired
+ user: UserResponse
+
+
+class UserEnabled(UserNotification):
+ action: Notification.Type = Notification.Type.user_enabled
+ by: Admin = None
+ user: UserResponse
+
+
+class UserDisabled(UserNotification):
+ action: Notification.Type = Notification.Type.user_disabled
+ by: Admin
+ user: UserResponse
+ reason: str = None
+
+
+class UserDataUsageReset(UserNotification):
+ action: Notification.Type = Notification.Type.data_usage_reset
+ by: Admin
+ user: UserResponse
+
+
+class UserSubscriptionRevoked(UserNotification):
+ action: Notification.Type = Notification.Type.subscription_revoked
+ by: Admin
+ user: UserResponse
+
+
+async def notify(message: Type[Notification]) -> None:
+ if config.WEBHOOK_ADDRESS:
+ queue.append(message)
diff --git a/app/utils/report.py b/app/utils/report.py
@@ -0,0 +1,145 @@
+from datetime import datetime as dt
+from typing import Optional
+
+from app import telegram
+from app.db import Session, create_notification_reminder
+from app.db.models import UserStatus
+from app.models.admin import Admin
+from app.models.user import ReminderType, UserResponse
+from app.utils.notification import (Notification, ReachedDaysLeft,
+ ReachedUsagePercent, UserCreated,
+ UserDataUsageReset, UserDeleted,
+ UserDisabled, UserEnabled, UserExpired,
+ UserLimited, UserSubscriptionRevoked,
+ UserUpdated, notify)
+from app import discord
+
+
+async def status_change(username: str, status: UserStatus, user: UserResponse, by: Optional[Admin] = None) -> None:
+ try:
+ await telegram.report_status_change(username, status)
+ except Exception:
+ pass
+ if status == UserStatus.limited:
+ await notify(UserLimited(username=username, action=Notification.Type.user_limited, user=user))
+ elif status == UserStatus.expired:
+ await notify(UserExpired(username=username, action=Notification.Type.user_expired, user=user))
+ elif status == UserStatus.disabled:
+ await notify(UserDisabled(username=username, action=Notification.Type.user_disabled, user=user, by=by))
+ elif status == UserStatus.active:
+ await notify(UserEnabled(username=username, action=Notification.Type.user_enabled, user=user, by=by))
+ try:
+ await discord.report_status_change(username, status)
+ except Exception:
+ pass
+
+
+async def user_created(user: UserResponse, user_id: int, by: Admin) -> None:
+ await telegram.report_new_user(
+ user_id=user_id,
+ username=user.username,
+ by=by.username,
+ expire_date=user.expire,
+ data_limit=user.data_limit,
+ # proxies=[]
+ services=user.services
+ )
+
+ await notify(UserCreated(username=user.username, action=Notification.Type.user_created, by=by, user=user))
+ try:
+ await discord.report_new_user(
+ user_id=user_id,
+ username=user.username,
+ by=by.username,
+ expire_date=user.expire,
+ data_limit=user.data_limit,
+ services=user.services
+ )
+ except Exception:
+ pass
+
+
+async def user_updated(user: UserResponse, by: Admin) -> None:
+ try:
+ await telegram.report_user_modification(
+ username=user.username,
+ expire_date=user.expire,
+ data_limit=user.data_limit,
+ services=user.services,
+ by=by.username,
+ )
+ except Exception:
+ pass
+ await notify(UserUpdated(username=user.username, action=Notification.Type.user_updated, by=by, user=user))
+ try:
+ await discord.report_user_modification(
+ username=user.username,
+ expire_date=user.expire,
+ data_limit=user.data_limit,
+ services=user.services,
+ by=by.username,
+ )
+ except Exception:
+ pass
+
+
+async def user_deleted(username: str, by: Admin) -> None:
+ try:
+ await telegram.report_user_deletion(username=username, by=by.username)
+ except Exception:
+ pass
+ await notify(UserDeleted(username=username, action=Notification.Type.user_deleted, by=by))
+ try:
+ discord.report_user_deletion(username=username, by=by.username)
+ except Exception:
+ pass
+
+
+async def user_data_usage_reset(user: UserResponse, by: Admin) -> None:
+ try:
+ await telegram.report_user_usage_reset(
+ username=user.username,
+ by=by.username,
+ )
+ except Exception:
+ pass
+ await notify(UserDataUsageReset(username=user.username, action=Notification.Type.data_usage_reset, by=by, user=user))
+ try:
+ await discord.report_user_usage_reset(
+ username=user.username,
+ by=by.username,
+ )
+ except Exception:
+ pass
+
+
+async def user_subscription_revoked(user: UserResponse, by: Admin) -> None:
+ try:
+ await telegram.report_user_subscription_revoked(
+ username=user.username,
+ by=by.username,
+ )
+ except Exception:
+ pass
+ await notify(UserSubscriptionRevoked(username=user.username, action=Notification.Type.subscription_revoked, by=by, user=user))
+ try:
+ await discord.report_user_subscription_revoked(
+ username=user.username,
+ by=by.username,
+ )
+ except Exception:
+ pass
+
+
+async def data_usage_percent_reached(
+ db: Session, percent: float, user: UserResponse, user_id: int, expire: Optional[int] = None) -> None:
+ await notify(ReachedUsagePercent(username=user.username, user=user, used_percent=percent))
+ await create_notification_reminder(db, ReminderType.data_usage,
+ expires_at=dt.utcfromtimestamp(expire) if expire else None, user_id=user_id)
+
+
+async def expire_days_reached(db: Session, days: int, user: UserResponse, user_id: int, expire: int) -> None:
+ await notify(ReachedDaysLeft(username=user.username, user=user, days_left=days))
+ await create_notification_reminder(
+ db, ReminderType.expiration_date, expires_at=dt.utcfromtimestamp(expire),
+ user_id=user_id)
diff --git a/app/utils/share.py b/app/utils/share.py
@@ -0,0 +1,990 @@
+import base64
+import json
+import random
+import secrets
+import urllib.parse as urlparse
+from datetime import datetime as dt, timedelta
+from typing import TYPE_CHECKING, Literal, Union, List
+from uuid import UUID
+import traceback
+
+import yaml
+from jdatetime import date as jd
+
+from app import xray
+from app.models.proxy import FormatVariables, InboundBase, InboundHost, InboundHostSecurity
+from app.templates import render_template
+from app.utils.system import get_public_ip, readable_size
+from app.utils.keygen import generate_settings
+from app.db import GetDB, crud
+
+if TYPE_CHECKING:
+ from app.models.user import UserResponse
+
+from config import CLASH_SUBSCRIPTION_TEMPLATE, SINGBOX_SUBSCRIPTION_TEMPLATE
+
+SERVER_IP = get_public_ip()
+
+STATUS_EMOJIS = {
+ "active": "✅",
+ "expired": "⌛️",
+ "limited": "🪫",
+ "disabled": "❌",
+ "connect_to_start": "🔌",
+}
+
+class V2rayShareLink(str):
+ @classmethod
+ def vmess(
+ cls,
+ remark: str,
+ address: str,
+ port: int,
+ id: Union[str, UUID],
+ host="",
+ net="tcp",
+ path="",
+ type="",
+ tls="none",
+ sni="",
+ fp="",
+ alpn="",
+ pbk="",
+ sid="",
+ spx="",
+ ais="",
+ ):
+ payload = {
+ "add": address,
+ "aid": "0",
+ "host": host,
+ "id": str(id),
+ "net": net,
+ "path": path,
+ "port": port,
+ "ps": remark,
+ "scy": "auto",
+ "tls": tls,
+ "type": type,
+ "v": "2",
+ }
+
+ if tls == "tls":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["alpn"] = alpn
+ if ais:
+ payload["allowInsecure"] = 1
+ elif tls == "reality":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["pbk"] = pbk
+ payload["sid"] = sid
+ payload["spx"] = spx
+
+ return (
+ "vmess://"
+ + base64.b64encode(
+ json.dumps(payload, sort_keys=True).encode("utf-8")
+ ).decode()
+ )
+
+ @classmethod
+ def vless(
+ cls,
+ remark: str,
+ address: str,
+ port: int,
+ id: Union[str, UUID],
+ net="ws",
+ path="",
+ host="",
+ type="",
+ flow="",
+ tls="none",
+ sni="",
+ fp="",
+ alpn="",
+ pbk="",
+ sid="",
+ spx="",
+ ais="",
+ ):
+ payload = {"security": tls, "type": net,
+ "host": host, "headerType": type}
+ if flow and (net in ("tcp", "kcp") and type != "http"):
+ payload["flow"] = flow
+
+ if net == "grpc":
+ payload["serviceName"] = path
+ else:
+ payload["path"] = path
+
+ if tls == "tls":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["alpn"] = alpn
+ if ais:
+ payload["allowInsecure"] = 1
+ elif tls == "reality":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["pbk"] = pbk
+ payload["sid"] = sid
+ payload["spx"] = spx
+
+ return (
+ "vless://"
+ + f"{id}@{address}:{port}?"
+ + urlparse.urlencode(payload)
+ + f"#{( urlparse.quote(remark))}"
+ )
+
+ @classmethod
+ def trojan(
+ cls,
+ remark: str,
+ address: str,
+ port: int,
+ password: str,
+ net="tcp",
+ path="",
+ host="",
+ type="",
+ flow="",
+ tls="none",
+ sni="",
+ fp="",
+ alpn="",
+ pbk="",
+ sid="",
+ spx="",
+ ais="",
+ ):
+ payload = {"security": tls, "type": net,
+ "host": host, "headerType": type}
+ if flow and (net in ("tcp", "kcp") and type != "http"):
+ payload["flow"] = flow
+
+ if net == "grpc":
+ payload["serviceName"] = path
+ else:
+ payload["path"] = path
+
+ if tls == "tls":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["alpn"] = alpn
+ if ais:
+ payload["allowInsecure"] = 1
+ elif tls == "reality":
+ payload["sni"] = sni
+ payload["fp"] = fp
+ payload["pbk"] = pbk
+ payload["sid"] = sid
+ payload["spx"] = spx
+
+ return (
+ "trojan://"
+ + f"{urlparse.quote(password, safe=':')}@{address}:{port}?"
+ + urlparse.urlencode(payload)
+ + f"#{urlparse.quote(remark)}"
+ )
+
+ @classmethod
+ def shadowsocks(
+ cls, remark: str, address: str, port: int, password: str, method: str
+ ):
+ return (
+ "ss://"
+ + base64.b64encode(f"{method}:{password}".encode()).decode()
+ + f"@{address}:{port}#{urlparse.quote(remark)}"
+ )
+
+
+class ClashConfiguration(object):
+ def __init__(self):
+ self.data = {
+ "proxies": [],
+ "proxy-groups": [],
+ # Some clients rely on "rules" option and will fail without it.
+ "rules": [],
+ }
+ self.proxy_remarks = []
+
+ def render(self):
+ return yaml.dump(
+ yaml.load(
+ render_template(
+ CLASH_SUBSCRIPTION_TEMPLATE,
+ {"conf": self.data, "proxy_remarks": self.proxy_remarks},
+ ),
+ Loader=yaml.SafeLoader,
+ ),
+ sort_keys=False,
+ allow_unicode=True,
+ )
+
+ def __str__(self) -> str:
+ return self.render()
+
+ def __repr__(self) -> str:
+ return self.render()
+
+ def _remark_validation(self, remark):
+ if not remark in self.proxy_remarks:
+ return remark
+ c = 2
+ while True:
+ new = f"{remark} ({c})"
+ if not new in self.proxy_remarks:
+ return new
+ c += 1
+
+ def make_node(
+ self,
+ name: str,
+ type: str,
+ server: str,
+ port: int,
+ network: str,
+ tls: bool,
+ sni: str,
+ host: str,
+ path: str,
+ udp: bool = True,
+ alpn: str = "",
+ ais: bool = "",
+ ):
+ if type == "shadowsocks":
+ type = "ss"
+
+ remark = self._remark_validation(name)
+ node = {
+ "name": remark,
+ "type": type,
+ "server": server,
+ "port": port,
+ "network": network,
+ f"{network}-opts": {},
+ "udp": udp,
+ }
+
+ if type == "ss": # shadowsocks
+ return node
+
+ if tls:
+ node["tls"] = True
+ if type == "trojan":
+ node["sni"] = sni
+ else:
+ node["servername"] = sni
+ if alpn:
+ node["alpn"] = alpn.split(",")
+ if ais:
+ node["skip-cert-verify"] = ais
+
+ net_opts = node[f"{network}-opts"]
+
+ if network == "ws":
+ if path:
+ net_opts["path"] = path
+ if host:
+ net_opts["headers"] = {"Host": host}
+
+ if network == "grpc":
+ if path:
+ net_opts["grpc-service-name"] = path
+
+ if network == "h2":
+ if path:
+ net_opts["path"] = path
+ if host:
+ net_opts["host"] = [host]
+
+ if network == "http" or network == "tcp":
+ if path:
+ net_opts["method"] = "GET"
+ net_opts["path"] = [path]
+ if host:
+ net_opts["method"] = "GET"
+ net_opts["headers"] = {"Host": host}
+
+ return node
+
+ def add(self, remark: str, address: str, inbound: dict, settings: dict):
+ node = self.make_node(
+ name=remark,
+ type=inbound["protocol"],
+ server=address,
+ port=inbound["port"],
+ network=inbound["network"],
+ tls=(inbound["tls"] == "tls"),
+ sni=inbound["sni"],
+ host=inbound["host"],
+ path=inbound["path"],
+ udp=True,
+ alpn=inbound.get("alpn", ""),
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "vmess":
+ node["uuid"] = settings["id"]
+ node["alterId"] = 0
+ node["cipher"] = "auto"
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+ if inbound["protocol"] == "trojan":
+ node["password"] = settings["password"]
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+ if inbound["protocol"] == "shadowsocks":
+ node["password"] = settings["password"]
+ node["cipher"] = settings["method"]
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+
+class ClashMetaConfiguration(ClashConfiguration):
+ def make_node(
+ self,
+ name: str,
+ type: str,
+ server: str,
+ port: int,
+ network: str,
+ tls: bool,
+ sni: str,
+ host: str,
+ path: str,
+ udp: bool = True,
+ alpn: str = "",
+ fp: str = "",
+ pbk: str = "",
+ sid: str = "",
+ ais: bool = "",
+ ):
+ node = super().make_node(
+ name=name,
+ type=type,
+ server=server,
+ port=port,
+ network=network,
+ tls=tls,
+ sni=sni,
+ host=host,
+ path=path,
+ #headers="",
+ udp=udp,
+ alpn=alpn,
+ ais=ais,
+ )
+ if fp:
+ node["client-fingerprint"] = fp
+ if pbk:
+ node["reality-opts"] = {"public-key": pbk, "short-id": sid}
+
+ return node
+
+ def add(self, remark: str, address: str, inbound: dict, settings: dict):
+ node = self.make_node(
+ name=remark,
+ type=inbound["protocol"],
+ server=address,
+ port=inbound["port"],
+ network=inbound["network"],
+ tls=(inbound["tls"] in ("tls", "reality")),
+ sni=inbound["sni"],
+ host=inbound["host"],
+ path=inbound["path"],
+ udp=True,
+ alpn=inbound.get("alpn", ""),
+ fp=inbound.get("fp", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "vmess":
+ node["uuid"] = settings["id"]
+ node["alterId"] = 0
+ node["cipher"] = "auto"
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+ if inbound["protocol"] == "vless":
+ node["uuid"] = settings["id"]
+
+ if (
+ inbound["network"] in ("tcp", "kcp")
+ and inbound["header_type"] != "http"
+ ):
+ node["flow"] = settings.get("flow", "")
+
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+ if inbound["protocol"] == "trojan":
+ node["password"] = settings["password"]
+
+ if (
+ inbound["network"] in ("tcp", "kcp")
+ and inbound["header_type"] != "http"
+ ):
+ node["flow"] = settings.get("flow", "")
+
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+ if inbound["protocol"] == "shadowsocks":
+ node["password"] = settings["password"]
+ node["cipher"] = settings["method"]
+ self.data["proxies"].append(node)
+ self.proxy_remarks.append(remark)
+
+
+def get_v2ray_link(remark: str, address: str, inbound: dict, settings: dict):
+ if inbound["protocol"] == "vmess":
+ return V2rayShareLink.vmess(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ id=settings["id"],
+ net=inbound["network"],
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=inbound["path"],
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "vless":
+ return V2rayShareLink.vless(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ id=settings["id"],
+ flow=settings.get("flow", ""),
+ net=inbound["network"],
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=inbound["path"],
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "trojan":
+ return V2rayShareLink.trojan(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ password=settings["password"],
+ flow=settings.get("flow", ""),
+ net=inbound["network"],
+ tls=inbound["tls"],
+ sni=inbound.get("sni", ""),
+ fp=inbound.get("fp", ""),
+ alpn=inbound.get("alpn", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ spx=inbound.get("spx", ""),
+ host=inbound["host"],
+ path=inbound["path"],
+ type=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "shadowsocks":
+ return V2rayShareLink.shadowsocks(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ password=settings["password"],
+ method=settings["method"],
+ )
+
+
+class OutlineConfiguration:
+ def __init__(self):
+ self.config = {}
+
+ def add_directly(self, data: dict):
+ self.config.update(data)
+
+ def render(self):
+ return json.dumps(self.config, indent=0)
+
+ def make_outbound(
+ self, remark: str, address: str, port: int, password: str, method: str
+ ):
+ config = {
+ "method": method,
+ "password": password,
+ "server": address,
+ "server_port": port,
+ "tag": remark,
+ }
+ return config
+
+ def add(self, remark: str, address: str, inbound: dict, settings: dict):
+ if inbound["protocol"] != "shadowsocks":
+ return
+
+ outbound = self.make_outbound(
+ remark=remark,
+ address=address,
+ port=inbound["port"],
+ password=settings["password"],
+ method=settings["method"],
+ )
+ self.add_directly(outbound)
+
+
+class SingBoxConfiguration(str):
+ def __init__(self):
+ template = render_template(SINGBOX_SUBSCRIPTION_TEMPLATE)
+ self.config = json.loads(template)
+
+ def add_outbound(self, outbound_data):
+ self.config["outbounds"].append(outbound_data)
+
+ def render(self):
+ urltest_types = ["vmess", "vless", "trojan", "shadowsocks"]
+ urltest_tags = [
+ outbound["tag"]
+ for outbound in self.config["outbounds"]
+ if outbound["type"] in urltest_types
+ ]
+ selector_types = ["vmess", "vless", "trojan", "shadowsocks", "urltest"]
+ selector_tags = [
+ outbound["tag"]
+ for outbound in self.config["outbounds"]
+ if outbound["type"] in selector_types
+ ]
+
+ for outbound in self.config["outbounds"]:
+ if outbound.get("type") == "urltest":
+ outbound["outbounds"] = urltest_tags
+
+ for outbound in self.config["outbounds"]:
+ if outbound.get("type") == "selector":
+ outbound["outbounds"] = selector_tags
+
+ return json.dumps(self.config, indent=4)
+
+ @staticmethod
+ def tls_config(
+ sni=None, fp=None, tls=None, pbk=None, sid=None, alpn=None, ais=None
+ ):
+ config = {}
+ if tls in ["tls", "reality"]:
+ config["enabled"] = True
+
+ if sni is not None:
+ config["server_name"] = sni
+
+ if tls == "tls" and ais:
+ config["insecure"] = ais
+
+ if tls == "reality":
+ config["reality"] = {"enabled": True}
+ if pbk:
+ config["reality"]["public_key"] = pbk
+ if sid:
+ config["reality"]["short_id"] = sid
+
+ if fp:
+ config["utls"] = {"enabled": bool(fp), "fingerprint": fp}
+
+ if alpn:
+ config["alpn"] = [alpn] if not isinstance(alpn, list) else alpn
+
+ return config
+
+ @staticmethod
+ def transport_config(
+ transport_type="",
+ host="",
+ path="",
+ method="",
+ headers="",
+ idle_timeout="15s",
+ ping_timeout="15s",
+ max_early_data=None,
+ early_data_header_name=None,
+ permit_without_stream=False,
+ ):
+ transport_config = {}
+
+ if transport_type:
+ transport_config["type"] = transport_type
+
+ if transport_type == "http":
+ if host:
+ transport_config["host"] = host
+ if path:
+ transport_config["path"] = path
+ if method:
+ transport_config["method"] = method
+ if headers:
+ transport_config["headers"] = headers
+ if idle_timeout:
+ transport_config["idle_timeout"] = idle_timeout
+ if ping_timeout:
+ transport_config["ping_timeout"] = ping_timeout
+
+ elif transport_type == "ws":
+ if path:
+ transport_config["path"] = path
+ if headers:
+ transport_config["headers"] = headers
+ if max_early_data is not None:
+ transport_config["max_early_data"] = max_early_data
+ if early_data_header_name:
+ transport_config["early_data_header_name"] = early_data_header_name
+
+ elif transport_type == "ws":
+ if path:
+ transport_config["service_name"] = path
+ if idle_timeout:
+ transport_config["idle_timeout"] = idle_timeout
+ if ping_timeout:
+ transport_config["ping_timeout"] = ping_timeout
+ if permit_without_stream:
+ transport_config["permit_without_stream"] = permit_without_stream
+
+ return transport_config
+
+ def make_outbound(
+ self,
+ type: str,
+ remark: str,
+ address: str,
+ port: int,
+ net="ws",
+ path="",
+ host="",
+ flow="",
+ tls="",
+ sni="",
+ fp="",
+ alpn="",
+ pbk="",
+ sid="",
+ headers="",
+ ais="",
+ ):
+ config = {
+ "type": type,
+ "tag": remark,
+ "server": address,
+ "server_port": port,
+ }
+ if net in ("tcp", "kcp") or headers != "http":
+ if flow:
+ config["flow"] = flow
+
+ if net in ["http", "ws", "quic", "grpc", "h2"]:
+ if net == "h2":
+ net = "http"
+ alpn = "h2"
+ config["transport"] = self.transport_config(
+ transport_type=net, host=host, path=path, headers=headers
+ )
+ else:
+ config["network"]: net
+
+ if tls in ("tls", "reality"):
+ config["tls"] = self.tls_config(
+ sni=sni, fp=fp, tls=tls, pbk=pbk, sid=sid, alpn=alpn, ais=ais
+ )
+
+ return config
+
+ def add(self, remark: str, address: str, inbound: dict, settings: dict):
+ outbound = self.make_outbound(
+ remark=remark,
+ type=inbound["protocol"],
+ address=address,
+ port=inbound["port"],
+ net=inbound["network"],
+ tls=(inbound["tls"]),
+ flow=settings.get("flow", ""),
+ sni=inbound["sni"],
+ host=inbound["host"],
+ path=inbound["path"],
+ alpn=inbound.get("alpn", ""),
+ fp=inbound.get("fp", ""),
+ pbk=inbound.get("pbk", ""),
+ sid=inbound.get("sid", ""),
+ headers=inbound["header_type"],
+ ais=inbound.get("ais", ""),
+ )
+
+ if inbound["protocol"] == "vmess":
+ outbound["uuid"] = settings["id"]
+
+ elif inbound["protocol"] == "vless":
+ outbound["uuid"] = settings["id"]
+
+ elif inbound["protocol"] == "trojan":
+ outbound["password"] = settings["password"]
+
+ elif inbound["protocol"] == "shadowsocks":
+ outbound["password"] = settings["password"]
+ outbound["method"] = settings["method"]
+
+ self.add_outbound(outbound)
+
+
+def generate_v2ray_links(inbounds: list, key: str, extra_data: dict) -> list:
+ format_variables = setup_format_variables(extra_data)
+ return process_inbounds_and_tags(inbounds, key, format_variables, mode="v2ray")
+
+
+def generate_clash_subscription(
+ inbounds: dict, key: str, extra_data: dict, is_meta: bool = False
+) -> str:
+ if is_meta is True:
+ conf = ClashMetaConfiguration()
+ else:
+ conf = ClashConfiguration()
+
+ format_variables = setup_format_variables(extra_data)
+ return process_inbounds_and_tags(
+ inbounds, key, format_variables, mode="clash", conf=conf
+ )
+
+
+def generate_singbox_subscription(
+ inbounds: dict, key: str, extra_data: dict
+) -> str:
+ conf = SingBoxConfiguration()
+
+ format_variables = setup_format_variables(extra_data)
+ return process_inbounds_and_tags(
+ inbounds, key, format_variables, mode="sing-box", conf=conf
+ )
+
+
+def generate_v2ray_subscription(links: list) -> str:
+ return base64.b64encode("\n".join(links).encode()).decode()
+
+
+def generate_outline_subscription(
+ inbounds: dict, key: str, extra_data: dict
+) -> str:
+ conf = OutlineConfiguration()
+
+ format_variables = setup_format_variables(extra_data)
+ return process_inbounds_and_tags(
+ inbounds, key, format_variables, mode="outline", conf=conf
+ )
+
+
+def generate_subscription(
+ user: "UserResponse",
+ config_format: Literal["v2ray", "clash-meta", "clash", "sing-box", "outline"],
+ as_base64: bool,
+) -> str:
+ kwargs = {
+ # "proxies": user.proxies,
+ "inbounds": user.inbounds,
+ "key": user.key,
+ "extra_data": user.__dict__,
+ }
+
+ if config_format == "v2ray":
+ config = "\n".join(generate_v2ray_links(**kwargs))
+ elif config_format == "clash-meta":
+ config = generate_clash_subscription(**kwargs, is_meta=True)
+ elif config_format == "clash":
+ config = generate_clash_subscription(**kwargs)
+ elif config_format == "sing-box":
+ config = generate_singbox_subscription(**kwargs)
+ elif config_format == "outline":
+ config = generate_outline_subscription(**kwargs)
+ else:
+ raise ValueError(f'Unsupported format "{config_format}"')
+
+ if as_base64:
+ config = base64.b64encode(config.encode()).decode()
+
+ return config
+
+
+def format_time_left(seconds_left: int) -> str:
+ if not seconds_left or seconds_left <= 0:
+ return "∞"
+
+ minutes, seconds = divmod(seconds_left, 60)
+ hours, minutes = divmod(minutes, 60)
+ days, hours = divmod(hours, 24)
+ months, days = divmod(days, 30)
+
+ result = []
+ if months:
+ result.append(f"{months}m")
+ if days:
+ result.append(f"{days}d")
+ if hours and (days < 7):
+ result.append(f"{hours}h")
+ if minutes and not (months or days):
+ result.append(f"{minutes}m")
+ if seconds and not (months or days):
+ result.append(f"{seconds}s")
+ return " ".join(result)
+
+from app.models.user import UserStatus
+
+def setup_format_variables(extra_data: dict) -> dict:
+
+
+ user_status = extra_data.get("status")
+ expire_timestamp = extra_data.get("expire")
+ on_hold_expire_duration = extra_data.get("on_hold_expire_duration")
+
+ if user_status != UserStatus.on_hold:
+ if expire_timestamp is not None and expire_timestamp >= 0:
+ seconds_left = expire_timestamp - int(dt.utcnow().timestamp())
+ expire_datetime = dt.fromtimestamp(expire_timestamp)
+ expire_date = expire_datetime.date()
+ jalali_expire_date = jd.fromgregorian(
+ year=expire_date.year, month=expire_date.month, day=expire_date.day
+ ).strftime("%Y-%m-%d")
+ days_left = (expire_datetime - dt.utcnow()).days + 1
+ time_left = format_time_left(seconds_left)
+ else:
+ days_left = "∞"
+ time_left = "∞"
+ expire_date = "∞"
+ jalali_expire_date = "∞"
+ else:
+ if on_hold_expire_duration is not None and on_hold_expire_duration >= 0:
+ days_left = timedelta(seconds=on_hold_expire_duration).days
+ time_left = format_time_left(on_hold_expire_duration)
+ expire_date = "-"
+ jalali_expire_date = "-"
+ else:
+ days_left = "∞"
+ time_left = "∞"
+ expire_date = "∞"
+ jalali_expire_date = "∞"
+
+ if extra_data.get("data_limit"):
+ data_limit = readable_size(extra_data["data_limit"])
+ data_left = extra_data["data_limit"] - extra_data["used_traffic"]
+ if data_left < 0:
+ data_left = 0
+ data_left = readable_size(data_left)
+ else:
+ data_limit = "∞"
+ data_left = "∞"
+
+ status_emoji = STATUS_EMOJIS.get(extra_data.get("status")) or ""
+
+ format_variables = {
+ "SERVER_IP": SERVER_IP,
+ "USERNAME": extra_data.get("username", "{USERNAME}"),
+ "DATA_USAGE": readable_size(extra_data.get("used_traffic")),
+ "DATA_LIMIT": data_limit,
+ "DATA_LEFT": data_left,
+ "DAYS_LEFT": days_left,
+ "EXPIRE_DATE": expire_date,
+ "JALALI_EXPIRE_DATE": jalali_expire_date,
+ "TIME_LEFT": time_left,
+ "STATUS_EMOJI": status_emoji,
+ }
+
+ return format_variables
+
+
+def process_inbounds_and_tags(
+ inbounds: list,
+ ukey: str,
+ format_variables: dict,
+ mode: str = "v2ray",
+ conf=None,
+) -> Union[List, str]:
+ salt = secrets.token_hex(8)
+ results = []
+
+ for inb in inbounds:
+ inb_id, protocol, tag = inb.id, inb.protocol, inb.tag
+ settings = generate_settings(ukey, protocol)
+
+ format_variables.update({"PROTOCOL": protocol.name})
+ inbound = xray.configs[inb.node_id].inbounds_by_tag.get(tag)
+ if not inbound:
+ continue
+
+ format_variables.update({"TRANSPORT": inbound["network"]})
+ host_inbound = inbound.copy()
+ with GetDB() as db:
+ hosts = crud.get_inbound_hosts(db, inb_id)
+ # for host in xray.hosts.get(tag, []):
+ for host in hosts:
+ sni = ""
+ sni_list = host.sni or inbound["sni"]
+ if sni_list:
+ sni = random.choice(sni_list).replace("*", salt)
+
+ req_host = ""
+ req_host_list = host.host or inbound["host"]
+ if req_host_list:
+ req_host = random.choice(req_host_list).replace("*", salt)
+ host_tls = None if host.security == InboundHostSecurity.inbound_default else host.security.value
+ host_inbound.update(
+ {
+ "port": host.port or inbound["port"],
+ "sni": sni,
+ "host": req_host,
+ "tls": inbound["tls"] if host_tls is None else host_tls,
+ "alpn": host.alpn.value or inbound.get("alpn", ""),
+ "path": inbound.get("path", "")
+ if host.path is None
+ else host.path.format_map(format_variables),
+ "fp": host.fingerprint.value or inbound.get("fp", ""),
+ "ais": host.allowinsecure or ""
+ }
+ )
+
+ if mode == "v2ray":
+ results.append(
+ get_v2ray_link(
+ remark=host.remark.format_map(format_variables),
+ address=host.address.format_map(
+ format_variables),
+ inbound=host_inbound,
+ settings=settings,
+ )
+ )
+ elif mode in ["clash", "sing-box", "outline"]:
+ conf.add(
+ remark=host.remark.format_map(format_variables),
+ address=host.address.format_map(format_variables),
+ inbound=host_inbound,
+ settings=settings,
+ )
+
+ if mode in ["clash", "sing-box", "outline"]:
+ return conf.render()
+ return results
+
+
+def encode_title(text: str) -> str:
+ return f"base64:{base64.b64encode(text.encode()).decode()}"
diff --git a/app/utils/store.py b/app/utils/store.py
@@ -0,0 +1,87 @@
+class MemoryStorage:
+ def __init__(self):
+ self._data = {}
+
+ def set(self, key, value):
+ self._data[key] = value
+
+ def get(self, key, default=None):
+ return self._data.get(key, default)
+
+ def delete(self, key):
+ self._data.pop(key, None)
+
+ def clear(self):
+ self._data.clear()
+
+
+class ListStorage(list):
+ def __init__(self, update_func):
+ super().__init__()
+ self.update_func = update_func
+
+ def __getitem__(self, index):
+ if not self:
+ self.update()
+
+ return super().__getitem__(index)
+
+ def __iter__(self):
+ if not self:
+ self.update()
+
+ return super().__iter__()
+
+ def __str__(self):
+ if not self:
+ self.update()
+
+ return super().__str__()
+
+ def update(self):
+ self.update_func(self)
+
+
+class DictStorage(dict):
+ def __init__(self, update_func):
+ super().__init__()
+ self.update_func = update_func
+
+ def __getitem__(self, key):
+ if not self:
+ self.update()
+
+ return super().__getitem__(key)
+
+ def __iter__(self):
+ if not self:
+ self.update()
+
+ return super().__iter__()
+
+ def __str__(self):
+ if not self:
+ self.update()
+
+ return super().__str__()
+
+ def values(self):
+ if not self:
+ self.update()
+
+ return super().values()
+
+ def keys(self):
+ if not self:
+ self.update()
+
+ return super().keys()
+
+ def get(self, key, default=None):
+ if not self:
+ self.update()
+
+ return super().get(key, default)
+
+ def update(self):
+ self.update_func(self)
diff --git a/app/utils/system.py b/app/utils/system.py
@@ -0,0 +1,134 @@
+import requests
+import math
+import secrets
+import socket
+from dataclasses import dataclass
+
+import psutil
+
+from app import scheduler
+
+
+@dataclass
+class MemoryStat():
+ total: int
+ used: int
+ free: int
+
+
+@dataclass
+class CPUStat():
+ cores: int
+ percent: float
+
+
+def cpu_usage() -> CPUStat:
+ return CPUStat(cores=psutil.cpu_count(), percent=psutil.cpu_percent())
+
+
+def memory_usage() -> MemoryStat:
+ mem = psutil.virtual_memory()
+ return MemoryStat(total=mem.total, used=mem.used, free=mem.available)
+
+
+@dataclass
+class RealtimeBandwidth:
+ def __post_init__(self):
+ io = psutil.net_io_counters()
+ self.bytes_recv = io.bytes_recv
+ self.bytes_sent = io.bytes_sent
+ self.packets_recv = io.packets_recv
+ self.packet_sent = io.packets_sent
+
+ incoming_bytes: int
+ outgoing_bytes: int
+ incoming_packets: int
+ outgoing_packets: int
+
+ bytes_recv: int = None
+ bytes_sent: int = None
+ packets_recv: int = None
+ packet_sent: int = None
+
+
+@dataclass
+class RealtimeBandwidthStat:
+ incoming_bytes: int
+ outgoing_bytes: int
+ incoming_packets: int
+ outgoing_packets: int
+
+
+rt_bw = RealtimeBandwidth(
+ incoming_bytes=0, outgoing_bytes=0, incoming_packets=0, outgoing_packets=0)
+
+
+@scheduler.scheduled_job('interval', seconds=1, coalesce=True, max_instances=1)
+def record_realtime_bandwidth() -> None:
+ io = psutil.net_io_counters()
+ rt_bw.incoming_bytes, rt_bw.bytes_recv = io.bytes_recv - rt_bw.bytes_recv, io.bytes_recv
+ rt_bw.outgoing_bytes, rt_bw.bytes_sent = io.bytes_sent - rt_bw.bytes_sent, io.bytes_sent
+ rt_bw.incoming_packets, rt_bw.packets_recv = io.packets_recv - rt_bw.packets_recv, io.packets_recv
+ rt_bw.outgoing_packets, rt_bw.packet_sent = io.packets_sent - rt_bw.packet_sent, io.packets_sent
+
+
+def realtime_bandwidth() -> RealtimeBandwidthStat:
+ return RealtimeBandwidthStat(
+ incoming_bytes=rt_bw.incoming_bytes, outgoing_bytes=rt_bw.outgoing_bytes,
+ incoming_packets=rt_bw.incoming_packets, outgoing_packets=rt_bw.outgoing_packets)
+
+
+def random_password() -> str:
+ return secrets.token_urlsafe(16)
+
+
+def check_port(port: int) -> bool:
+ s = socket.socket()
+ try:
+ s.connect(('127.0.0.1', port))
+ return True
+ except socket.error:
+ return False
+ finally:
+ s.close()
+
+
+def get_public_ip():
+ """
+ try:
+ return requests.get('https://api.ipify.org?format=json&ipv=4', timeout=5).json()['ip']
+ except (requests.exceptions.RequestException,
+ requests.exceptions.RequestException,
+ KeyError) as e:
+ pass
+
+ try:
+ requests.packages.urllib3.util.connection.HAS_IPV6 = False
+ return requests.get('https://ifconfig.io/ip', timeout=5).text.strip()
+ except (requests.exceptions.RequestException,
+ requests.exceptions.RequestException,
+ KeyError) as e:
+ pass
+ finally:
+ requests.packages.urllib3.util.connection.HAS_IPV6 = True
+ """
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ sock.connect(('8.8.8.8', 80))
+ return sock.getsockname()[0]
+ except (socket.error, IndexError):
+ pass
+ finally:
+ sock.close()
+
+ return '127.0.0.1'
+
+
+def readable_size(size_bytes):
+ if size_bytes == 0:
+ return "0 B"
+ size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
+ i = int(math.floor(math.log(size_bytes, 1024)))
+ p = math.pow(1024, i)
+ s = round(size_bytes / p, 2)
+ return f'{s} {size_name[i]}'
diff --git a/app/views/__init__.py b/app/views/__init__.py
@@ -0,0 +1,9 @@
+from .admin import *
+from .subscription import *
+from .system import *
+from .core import *
+from .user import *
+#from .user_template import *
+from .service import *
+from .node import *
+from .home import *
diff --git a/app/views/admin.py b/app/views/admin.py
@@ -0,0 +1,124 @@
+from typing import List, Optional
+
+import sqlalchemy
+from app import app
+from app.db import Session, crud, get_db
+from app.models.admin import Admin, AdminCreate, AdminInDB, AdminModify, Token
+from app.utils.jwt import create_admin_token
+from config import SUDOERS
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordRequestForm
+
+
+def authenticate_env_sudo(username: str, password: str) -> bool:
+ try:
+ return password == SUDOERS[username]
+ except KeyError:
+ return False
+
+
+def authenticate_admin(db: Session, username: str, password: str) -> Optional[Admin]:
+ dbadmin = crud.get_admin(db, username)
+ if not dbadmin:
+ return None
+
+ return dbadmin if AdminInDB.model_validate(dbadmin).verify_password(password) else None
+
+
+@app.post("/api/admin/token", tags=['Admin'], response_model=Token)
+def admin_token(form_data: OAuth2PasswordRequestForm = Depends(),
+ db: Session = Depends(get_db)):
+ if authenticate_env_sudo(form_data.username, form_data.password):
+ return Token(access_token=create_admin_token(form_data.username, is_sudo=True))
+
+ if dbadmin := authenticate_admin(db, form_data.username, form_data.password):
+ return Token(access_token=create_admin_token(form_data.username, is_sudo=dbadmin.is_sudo))
+
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+
+@app.post("/api/admin", tags=['Admin'], response_model=Admin)
+def create_admin(new_admin: AdminCreate,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ try:
+ dbadmin = crud.create_admin(db, new_admin)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail="Admin already exists")
+
+ return dbadmin
+
+
+@app.put("/api/admin/{username}", tags=['Admin'], response_model=Admin)
+def modify_admin(username: str,
+ modified_admin: AdminModify,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not (admin.is_sudo or admin.username == username):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ # If a non-sudoer admin is making itself a sudoer
+ if (admin.username == username) and (modified_admin.is_sudo and not admin.is_sudo):
+ raise HTTPException(status_code=403, detail="You can't make yourself sudoer!")
+
+ dbadmin = crud.get_admin(db, username)
+ if not dbadmin:
+ raise HTTPException(status_code=404, detail="Admin not found")
+
+ # If a sudoer admin wants to edit another sudoer
+ if (username != admin.username) and dbadmin.is_sudo:
+ raise HTTPException(
+ status_code=403,
+ detail=("You're not allowed to edit another sudoers account. Use marzban-cli instead.",),
+ )
+
+ dbadmin = crud.update_admin(db, dbadmin, modified_admin)
+ return dbadmin
+
+
+@app.delete("/api/admin/{username}", tags=['Admin'])
+def remove_admin(username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbadmin = crud.get_admin(db, username)
+ if not dbadmin:
+ raise HTTPException(status_code=404, detail="Admin not found")
+
+ if dbadmin.is_sudo:
+ raise HTTPException(
+ status_code=403,
+ detail=("You're not allowed to delete sudoers accounts. Use marzban-cli instead."),
+ )
+
+ dbadmin = crud.remove_admin(db, dbadmin)
+ return {}
+
+
+@app.get("/api/admin", tags=["Admin"], response_model=Admin)
+def get_current_admin(admin: Admin = Depends(Admin.get_current)):
+ return admin
+
+
+@app.get("/api/admins", tags=['Admin'], response_model=List[Admin])
+def get_admins(offset: int = None,
+ limit: int = None,
+ username: str = None,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ return crud.get_admins(db, offset, limit, username)
diff --git a/app/views/core.py b/app/views/core.py
@@ -0,0 +1,115 @@
+import asyncio
+import json
+# import time
+
+import commentjson
+from fastapi import Depends, HTTPException, WebSocket
+from starlette.websockets import WebSocketDisconnect
+from websockets.exceptions import ConnectionClosedError
+
+from app import app, xray
+from app.db import Session, get_db
+from app.models.admin import Admin
+from app.models.core import CoreStats
+from app.xray import XRayConfig
+from config import XRAY_JSON
+from anyio import create_memory_object_stream, run
+
+
+@app.websocket("/api/core/logs")
+async def core_logs(websocket: WebSocket, db: Session = Depends(get_db)):
+ token = (
+ websocket.query_params.get('token')
+ or
+ websocket.headers.get('Authorization', '').removeprefix("Bearer ")
+ )
+ admin = Admin.get_admin(token, db)
+ if not admin:
+ return await websocket.close(reason="Unauthorized", code=4401)
+
+ if not admin.is_sudo:
+ return await websocket.close(reason="You're not allowed", code=4403)
+
+ interval = websocket.query_params.get('interval')
+ if interval:
+ try:
+ interval = float(interval)
+ except ValueError:
+ return await websocket.close(reason="Invalid interval value", code=4400)
+ if interval > 10:
+ return await websocket.close(reason="Interval must be more than 0 and at most 10 seconds", code=4400)
+
+ await websocket.accept()
+
+ try:
+ buffer = xray.core.get_buffer()
+ while buffer:
+ await websocket.send_text(buffer.popleft().decode("utf-8"))
+ rcv_stm = await xray.core.get_logs_stm()
+ while l := await rcv_stm.receive():
+ l = l.decode("utf-8")
+ await websocket.send_text(l)
+ except (WebSocketDisconnect, RuntimeError, ConnectionClosedError):
+ pass
+ except asyncio.CancelledError:
+ await websocket.close()
+ finally:
+ await rcv_stm.aclose()
+
+
+@app.get("/api/core", tags=["Core"], response_model=CoreStats)
+def get_core_stats(admin: Admin = Depends(Admin.get_current)):
+ return CoreStats(
+ version=xray.core.version,
+ started=xray.core.started,
+ logs_websocket=app.url_path_for('core_logs')
+ )
+
+
+@app.post("/api/core/restart", tags=["Core"])
+async def restart_core(admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ startup_config = xray.config.include_db_users()
+ await xray.core.restart(startup_config)
+ for node_id, node in list(xray.nodes.items()):
+ if await node.is_healthy():
+ await xray.operations.restart_node(node_id, startup_config)
+ return {}
+
+
+@app.get("/api/core/config", tags=["Core"])
+def get_core_config(admin: Admin = Depends(Admin.get_current)) -> dict:
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ with open(XRAY_JSON, "r") as f:
+ config = commentjson.loads(f.read())
+
+ return config
+
+
+@app.put("/api/core/config", tags=["Core"])
+async def modify_core_config(payload: dict, admin: Admin = Depends(Admin.get_current)) -> dict:
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ try:
+ config = XRayConfig(payload, api_port=xray.config.api_port)
+ except ValueError as err:
+ raise HTTPException(status_code=400, detail=str(err))
+
+ xray.config = config
+ with open(XRAY_JSON, 'w') as f:
+ f.write(json.dumps(payload, indent=4))
+
+ startup_config = xray.config.include_db_users()
+ await xray.core.restart(startup_config)
+ for node_id, node in list(xray.nodes.items()):
+ if await node.is_healthy():
+ await xray.operations.restart_node(node_id, startup_config)
+
+ xray.hosts.update()
+
+ return payload
diff --git a/app/views/home.py b/app/views/home.py
@@ -0,0 +1,10 @@
+from fastapi.responses import HTMLResponse
+
+from app import app
+from app.templates import render_template
+from config import HOME_PAGE_TEMPLATE
+
+
+@app.get("/", response_class=HTMLResponse)
+def base():
+ return render_template(HOME_PAGE_TEMPLATE)
diff --git a/app/views/node.py b/app/views/node.py
@@ -0,0 +1,246 @@
+import asyncio
+# import time
+from datetime import datetime
+from typing import List
+
+import sqlalchemy
+from fastapi import BackgroundTasks, Depends, HTTPException, WebSocket
+from starlette.websockets import WebSocketDisconnect
+
+from app import app, logger, xray
+from app.db import Session, crud, get_db
+from app.models.admin import Admin
+from app.models.node import (NodeCreate, NodeModify, NodeResponse,
+ NodeSettings, NodeStatus, NodesUsageResponse)
+from app.models.proxy import InboundHost
+
+
+@app.get("/api/node/settings", tags=['Node'], response_model=NodeSettings)
+def get_node_settings(db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ tls = crud.get_tls_certificate(db)
+
+ return NodeSettings(
+ certificate=tls.certificate
+ )
+
+
+@app.post("/api/node", tags=['Node'], response_model=NodeResponse)
+async def add_node(new_node: NodeCreate,
+ bg: BackgroundTasks,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ try:
+ dbnode = crud.create_node(db, new_node)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail=f"Node \"{new_node.name}\" already exists")
+
+ asyncio.get_running_loop().create_task(
+ xray.operations.start_node(
+ node_id=dbnode.id,
+ config=xray.config.include_db_users())
+ )
+
+ if new_node.add_as_new_host is True:
+ host = InboundHost(
+ remark=(new_node.name + " ({USERNAME}) [{PROTOCOL} - {TRANSPORT}]"),
+ address=new_node.address
+ )
+ for inbound_tag in xray.config.inbounds_by_tag:
+ crud.add_host(db, inbound_tag, host)
+
+ xray.hosts.update()
+
+ logger.info(f"New node \"{dbnode.name}\" added")
+ return dbnode
+
+
+@app.get("/api/node/{node_id}", tags=['Node'], response_model=NodeResponse)
+def get_node(node_id: int,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbnode = crud.get_node_by_id(db, node_id)
+ if not dbnode:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ return dbnode
+
+
+@app.websocket("/api/node/{node_id}/logs")
+async def node_logs(node_id: int, websocket: WebSocket, db: Session = Depends(get_db)):
+ token = (
+ websocket.query_params.get('token')
+ or
+ websocket.headers.get('Authorization', '').removeprefix("Bearer ")
+ )
+ admin = Admin.get_admin(token, db)
+ if not admin:
+ return await websocket.close(reason="Unauthorized", code=4401)
+
+ if not admin.is_sudo:
+ return await websocket.close(reason="You're not allowed", code=4403)
+
+ if not xray.nodes.get(node_id):
+ return await websocket.close(reason="Node not found", code=4404)
+
+ if not await xray.nodes[node_id].is_healthy():
+ return await websocket.close(reason="Node is not connected", code=4400)
+
+ interval = websocket.query_params.get('interval')
+ if interval:
+ try:
+ interval = float(interval)
+ except ValueError:
+ return await websocket.close(reason="Invalid interval value", code=4400)
+ if interval > 10:
+ return await websocket.close(reason="Interval must be more than 0 and at most 10 seconds", code=4400)
+
+ await websocket.accept()
+ async for l in xray.nodes[node_id].get_logs():
+ await websocket.send_text(l)
+ """
+ cache = ''
+ last_sent_ts = 0
+ with xray.nodes[node_id].get_logs() as logs:
+ while True:
+ if interval and time.time() - last_sent_ts >= interval and cache:
+ try:
+ await websocket.send_text(cache)
+ except (WebSocketDisconnect, RuntimeError):
+ break
+ cache = ''
+ last_sent_ts = time.time()
+
+ if not logs:
+ try:
+ await asyncio.wait_for(websocket.receive(), timeout=0.2)
+ continue
+ except asyncio.TimeoutError:
+ continue
+ except (WebSocketDisconnect, RuntimeError):
+ break
+
+ log = logs.popleft()
+
+ if interval:
+ cache += f'{log}\n'
+ continue
+
+ try:
+ await websocket.send_text(log)
+ except (WebSocketDisconnect, RuntimeError):
+ break"""
+
+
+@app.get("/api/nodes", tags=['Node'], response_model=List[NodeResponse])
+def get_nodes(db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+ return crud.get_nodes(db)
+
+
+@app.put("/api/node/{node_id}", tags=['Node'], response_model=NodeResponse)
+async def modify_node(node_id: int,
+ modified_node: NodeModify,
+ bg: BackgroundTasks,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbnode = crud.get_node_by_id(db, node_id)
+ if not dbnode:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ dbnode = crud.update_node(db, dbnode, modified_node)
+
+ await xray.operations.remove_node(dbnode.id)
+ if dbnode.status != NodeStatus.disabled:
+ asyncio.get_running_loop().create_task(
+ xray.operations.start_node(
+ node_id=dbnode.id,
+ config=xray.config.include_db_users()
+ ))
+
+ logger.info(f"Node \"{dbnode.name}\" modified")
+ return dbnode
+
+
+@app.post("/api/node/{node_id}/reconnect", tags=['Node'])
+async def reconnect_node(node_id: int,
+ bg: BackgroundTasks,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbnode = crud.get_node_by_id(db, node_id)
+ if not dbnode:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ asyncio.get_running_loop().create_task(
+ xray.operations.start_node(
+ node_id=dbnode.id,
+ config=xray.config.include_db_users())
+ )
+ return {}
+
+
+@app.delete("/api/node/{node_id}", tags=['Node'])
+async def remove_node(node_id: int,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbnode = crud.get_node_by_id(db, node_id)
+ if not dbnode:
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ crud.remove_node(db, dbnode)
+ await xray.operations.remove_node(dbnode.id)
+
+ logger.info(f"Node \"{dbnode.name}\" deleted")
+ return {}
+
+
+@app.get("/api/nodes/usage", tags=['Node'], response_model=NodesUsageResponse)
+def get_usage(db: Session = Depends(get_db),
+ start: str = None,
+ end: str = None,
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Get nodes usage
+ """
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ if start is None:
+ start_date = datetime.fromtimestamp(datetime.utcnow().timestamp() - 30 * 24 * 3600)
+ else:
+ start_date = datetime.fromisoformat(start)
+
+ if end is None:
+ end_date = datetime.utcnow()
+ else:
+ end_date = datetime.fromisoformat(end)
+
+ usages = crud.get_nodes_usage(db, start_date, end_date)
+
+ return {"usages": usages}
diff --git a/app/views/service.py b/app/views/service.py
@@ -0,0 +1,90 @@
+from typing import List
+
+import sqlalchemy
+from fastapi import Depends, HTTPException
+
+from app import app
+from app.db import Session, crud, get_db
+from app.models.admin import Admin
+from app.models.service import (ServiceCreate, ServiceModify,
+ ServiceResponse)
+
+
+@app.post("/api/service", tags=['Service'], response_model=ServiceResponse)
+def add_service(new_service: ServiceCreate,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Add a new user template
+
+ - **name** can be up to 64 characters
+ - **data_limit** must be in bytes and larger or equal to 0
+ - **expire_duration** must be in seconds and larger or equat to 0
+ - **inbounds** dictionary of protocol:inbound_tags, empty means all inbounds
+ """
+ if not (admin.is_sudo):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+ try:
+ return crud.create_service(db, new_service)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail="Service by this name already exists")
+
+
+@app.get("/api/service/{id}", tags=['Service'], response_model=ServiceResponse)
+def get_service(id: int, db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current)):
+ """
+ Get Service information with id
+ """
+ dbservice = crud.get_service(db, id)
+ if not dbservice:
+ raise HTTPException(status_code=404, detail="Service not found")
+
+ return dbservice
+
+
+@app.put("/api/service/{id}", tags=['Service'], response_model=ServiceResponse)
+def modify_service(id: int, modify_service: ServiceModify,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Modify Service
+
+ - **name** can be up to 64 characters
+ - **data_limit** must be in bytes and larger or equal to 0
+ - **expire_duration** must be in seconds and larger or equat to 0
+ - **inbounds** dictionary of protocol:inbound_tags, empty means all inbounds
+ """
+ if not (admin.is_sudo):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbservice = crud.get_service(db, id)
+ if not dbuser_template:
+ raise HTTPException(status_code=404, detail="Service not found")
+
+ try:
+ return crud.update_service(db, dbservice, modify_service)
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail="Service by this name already exists")
+
+
+@app.delete("/api/service/{id}", tags=['Service'])
+def remove_service(id: int,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not (admin.is_sudo):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbservice = crud.get_service(db, id)
+ if not dbuser_template:
+ raise HTTPException(status_code=404, detail="Service not found")
+
+ return crud.remove_service(db, dbservice)
+
+
+@app.get("/api/services", tags=['Service'], response_model=List[ServiceResponse])
+def get_services(
+ offset: int = None, limit: int = None, db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ return crud.get_services(db) #, offset, limit)
diff --git a/app/views/subscription.py b/app/views/subscription.py
@@ -0,0 +1,213 @@
+import re
+from datetime import datetime
+
+from fastapi import Depends, Header, HTTPException, Path, Request, Response
+from fastapi.responses import HTMLResponse
+
+from app import app
+from app.db import Session, crud, get_db
+from app.models.user import UserResponse
+from app.templates import render_template
+from app.utils.jwt import get_subscription_payload
+from app.utils.share import encode_title, generate_subscription
+from config import (
+ SUB_PROFILE_TITLE,
+ SUB_SUPPORT_URL,
+ SUB_UPDATE_INTERVAL,
+ SUBSCRIPTION_PAGE_TEMPLATE,
+ XRAY_SUBSCRIPTION_PATH
+)
+
+
+@app.get("/%s/{token}/" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'])
+@app.get("/%s/{token}" % XRAY_SUBSCRIPTION_PATH, include_in_schema=False)
+def user_subscription(token: str,
+ request: Request,
+ db: Session = Depends(get_db),
+ user_agent: str = Header(default="")):
+ """
+ Subscription link, V2ray and Clash supported
+ """
+ accept_header = request.headers.get("Accept", "")
+
+ def get_subscription_user_info(user: UserResponse) -> dict:
+ return {
+ "upload": 0,
+ "download": user.used_traffic,
+ "total": user.data_limit,
+ "expire": user.expire,
+ }
+
+ sub = get_subscription_payload(token)
+ if not sub:
+ return Response(status_code=204)
+
+ dbuser = crud.get_user(db, sub['username'])
+ if not dbuser or dbuser.created_at > sub['created_at']:
+ return Response(status_code=204)
+
+ if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']:
+ return Response(status_code=204)
+
+ user: UserResponse = UserResponse.model_validate(dbuser)
+
+ if "text/html" in accept_header:
+ return HTMLResponse(
+ render_template(
+ SUBSCRIPTION_PAGE_TEMPLATE,
+ {"user": user}
+ )
+ )
+
+ response_headers = {
+ "content-disposition": f'attachment; filename="{user.username}"',
+ "profile-web-page-url": str(request.url),
+ "support-url": SUB_SUPPORT_URL,
+ "profile-title": encode_title(SUB_PROFILE_TITLE),
+ "profile-update-interval": SUB_UPDATE_INTERVAL,
+ "subscription-userinfo": "; ".join(
+ f"{key}={val}"
+ for key, val in get_subscription_user_info(user).items()
+ if val is not None
+ )
+ }
+
+ crud.update_user_sub(db, dbuser, user_agent)
+
+ if re.match('^([Cc]lash-verge|[Cc]lash-?[Mm]eta)', user_agent):
+ conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False)
+ return Response(content=conf, media_type="text/yaml", headers=response_headers)
+
+ elif re.match('^([Cc]lash|[Ss]tash)', user_agent):
+ conf = generate_subscription(user=user, config_format="clash", as_base64=False)
+ return Response(content=conf, media_type="text/yaml", headers=response_headers)
+
+ elif re.match('^(SFA|SFI|SFM|SFT)', user_agent):
+ conf = generate_subscription(user=user, config_format="sing-box", as_base64=False)
+ return Response(content=conf, media_type="application/json", headers=response_headers)
+
+ elif re.match('^(SS|SSR|SSD|SSS|Outline|Shadowsocks|SSconf)', user_agent):
+ conf = generate_subscription(user=user, config_format="outline", as_base64=False)
+ return Response(content=conf, media_type="application/json", headers=response_headers)
+
+ else:
+ conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
+ return Response(content=conf, media_type="text/plain", headers=response_headers)
+
+
+@app.get("/%s/{token}/info" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'], response_model=UserResponse)
+def user_subscription_info(token: str,
+ db: Session = Depends(get_db)):
+ sub = get_subscription_payload(token)
+ if not sub:
+ return Response(status_code=404)
+
+ dbuser = crud.get_user(db, sub['username'])
+ if not dbuser or dbuser.created_at > sub['created_at']:
+ return Response(status_code=404)
+
+ elif dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']:
+ return Response(status_code=404)
+
+ return dbuser
+
+
+@app.get("/%s/{token}/usage" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'])
+def user_get_usage(token: str,
+ start: str = None,
+ end: str = None,
+ db: Session = Depends(get_db)):
+
+ sub = get_subscription_payload(token)
+ if not sub:
+ return Response(status_code=204)
+
+ dbuser = crud.get_user(db, sub['username'])
+ if not dbuser or dbuser.created_at > sub['created_at']:
+ return Response(status_code=204)
+
+ if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']:
+ return Response(status_code=204)
+
+ if start is None:
+ start_date = datetime.fromtimestamp(datetime.utcnow().timestamp() - 30 * 24 * 3600)
+ else:
+ start_date = datetime.fromisoformat(start)
+
+ if end is None:
+ end_date = datetime.utcnow()
+ else:
+ end_date = datetime.fromisoformat(end)
+
+ usages = crud.get_user_usages(db, dbuser, start_date, end_date)
+
+ return {"usages": usages, "username": dbuser.username}
+
+
+@app.get("/%s/{token}/{client_type}" % XRAY_SUBSCRIPTION_PATH, tags=['Subscription'])
+def user_subscription_with_client_type(
+ token: str,
+ request: Request,
+ client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray"),
+ db: Session = Depends(get_db),
+):
+ """
+ Subscription link, v2ray, clash, sing-box, outline and clash-meta supported
+ """
+
+ def get_subscription_user_info(user: UserResponse) -> dict:
+ return {
+ "upload": 0,
+ "download": user.used_traffic,
+ "total": user.data_limit,
+ "expire": user.expire,
+ }
+
+ sub = get_subscription_payload(token)
+ if not sub:
+ return Response(status_code=204)
+
+ dbuser = crud.get_user(db, sub['username'])
+ if not dbuser or dbuser.created_at > sub['created_at']:
+ return Response(status_code=204)
+
+ if dbuser.sub_revoked_at and dbuser.sub_revoked_at > sub['created_at']:
+ return Response(status_code=204)
+
+ user: UserResponse = UserResponse.model_validate(dbuser)
+
+ response_headers = {
+ "content-disposition": f'attachment; filename="{user.username}"',
+ "profile-web-page-url": str(request.url),
+ "support-url": SUB_SUPPORT_URL,
+ "profile-title": encode_title(SUB_PROFILE_TITLE),
+ "profile-update-interval": SUB_UPDATE_INTERVAL,
+ "subscription-userinfo": "; ".join(
+ f"{key}={val}"
+ for key, val in get_subscription_user_info(user).items()
+ if val is not None
+ )
+ }
+
+ if client_type == "clash-meta":
+ conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False)
+ return Response(content=conf, media_type="text/yaml", headers=response_headers)
+
+ elif client_type == "sing-box":
+ conf = generate_subscription(user=user, config_format="sing-box", as_base64=False)
+ return Response(content=conf, media_type="application/json", headers=response_headers)
+
+ elif client_type == "clash":
+ conf = generate_subscription(user=user, config_format="clash", as_base64=False)
+ return Response(content=conf, media_type="text/yaml", headers=response_headers)
+
+ elif client_type == "v2ray":
+ conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
+ return Response(content=conf, media_type="text/plain", headers=response_headers)
+
+ elif client_type == "outline":
+ conf = generate_subscription(user=user, config_format="outline", as_base64=False)
+ return Response(content=conf, media_type="application/json", headers=response_headers)
+
+ else:
+ raise HTTPException(status_code=400, detail="Invalid subscription type")
diff --git a/app/views/system.py b/app/views/system.py
@@ -0,0 +1,87 @@
+from typing import Dict, List, Union
+
+from fastapi import Depends, HTTPException
+
+from app import app, xray
+from app.db import Session, crud, get_db
+from app.models.admin import Admin
+from app.models.proxy import InboundHost, Inbound, ProxyTypes, InboundBase
+from app.models.system import SystemStats
+from app.models.user import UserStatus
+from app.utils.system import memory_usage, cpu_usage, realtime_bandwidth
+from app import __version__
+
+
+@app.get("/api/system", tags=["System"], response_model=SystemStats)
+def get_system_stats(db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current)):
+ mem = memory_usage()
+ cpu = cpu_usage()
+ system = crud.get_system_usage(db)
+ dbadmin: Union[Admin, None] = crud.get_admin(db, admin.username)
+
+ total_user = crud.get_users_count(db, admin=dbadmin if not admin.is_sudo else None)
+ users_active = crud.get_users_count(db, status=UserStatus.active, admin=dbadmin if not admin.is_sudo else None)
+ realtime_bandwidth_stats = realtime_bandwidth()
+
+ return SystemStats(
+ version=__version__,
+ mem_total=mem.total,
+ mem_used=mem.used,
+ cpu_cores=cpu.cores,
+ cpu_usage=cpu.percent,
+ total_user=total_user,
+ users_active=users_active,
+ incoming_bandwidth=system.uplink,
+ outgoing_bandwidth=system.downlink,
+ incoming_bandwidth_speed=realtime_bandwidth_stats.incoming_bytes,
+ outgoing_bandwidth_speed=realtime_bandwidth_stats.outgoing_bytes,
+ )
+
+
+@app.get('/api/inbounds', tags=["System"], response_model=Dict[str, List[Inbound]])
+def get_inbounds(db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current)):
+ inbounds = crud.get_all_inbounds(db)# xray.config.inbounds_by_protocol
+ resp = {}
+ for i in inbounds:
+ if resp.get(i.protocol):
+ resp[i.protocol].append(i)
+ else:
+ resp[i.protocol] = [i]
+ return resp
+
+
+@app.get('/api/hosts', tags=["System"], response_model=Dict[str, List[InboundHost]])
+def get_hosts(db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ # hosts = {}
+ hosts = crud.get_all_hosts(db)
+ #for inbound_tag in xray.config.inbounds_by_tag:
+ # hosts[inbound_tag] = crud.get_hosts(db, inbound_tag)
+
+ return hosts
+
+
+@app.put('/api/hosts', tags=["System"], response_model=Dict[str, List[InboundHost]])
+def modify_hosts(modified_hosts: Dict[str, List[InboundHost]],
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ # validate
+ #for inbound_id, hosts in modified_hosts.items():
+ # if not xray.config.inbounds_by_id.get(inbound_id):
+ # raise HTTPException(status_code=400, detail=f"Inbound {inbound_id} doesn't exist")
+
+ for inbound_id, hosts in modified_hosts.items():
+ crud.update_hosts(db, inbound_id, hosts)
+
+ #xray.hosts.update()
+
+ hosts = {}
+ #for inbound_tag in xray.config.inbounds_by_tag:
+ # hosts[inbound_tag] = crud.get_hosts(db, inbound_tag)
+
+ return hosts
diff --git a/app/views/user.py b/app/views/user.py
@@ -0,0 +1,348 @@
+from datetime import datetime, timezone
+from typing import List
+
+import asyncio
+import sqlalchemy
+from fastapi import BackgroundTasks, Depends, HTTPException, Query
+
+from app import app, logger, xray
+from app.db import Session, crud, get_db
+from app.models.admin import Admin
+from app.models.user import (UserCreate, UserModify, UserResponse,
+ UsersResponse, UserStatus, UserUsagesResponse)
+from app.utils import report
+
+
+@app.post("/api/user", tags=['User'], response_model=UserResponse)
+async def add_user(new_user: UserCreate,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Add a new user
+
+ - **username** must have 3 to 32 characters and is allowed to contain a-z, 0-9, and underscores in between
+ - **expire** must be an UTC timestamp
+ - **data_limit** must be in Bytes, e.g. 1073741824B = 1GB
+ - **proxies** dictionary of protocol:settings
+ - **inbounds** dictionary of protocol:inbound_tags, empty means all inbounds
+ """
+ # TODO expire should be datetime instead of timestamp
+
+ try:
+ dbuser = crud.create_user(db, new_user,
+ admin=crud.get_admin(db, admin.username))
+ except sqlalchemy.exc.IntegrityError:
+ db.rollback()
+ raise HTTPException(status_code=409, detail="User already exists")
+
+ user = UserResponse.model_validate(dbuser)
+ asyncio.get_running_loop().create_task(xray.operations.add_user(user=user))
+ asyncio.get_running_loop().create_task(
+ report.user_created(
+ user=user,
+ user_id=dbuser.id,
+ by=admin
+ ))
+ logger.info(f"New user \"{dbuser.username}\" added")
+ return user
+
+
+@app.get("/api/user/{username}", tags=['User'], response_model=UserResponse)
+def get_user(username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Get users information
+ """
+ if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ return dbuser
+
+
+@app.put("/api/user/{username}", tags=['User'], response_model=UserResponse)
+async def modify_user(username: str,
+ modified_user: UserModify,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Modify a user
+
+ - set **expire** to 0 to make the user unlimited in time, null to no change
+ - set **data_limit** to 0 to make the user unlimited in data, null to no change
+ - **proxies** dictionary of protocol:settings, empty means no change
+ - **inbounds** dictionary of protocol:inbound_tags, empty means no change
+ """
+ if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+ old_inbounds = {(i.node_id, i.protocol, i.tag) for i in dbuser.inbounds}
+ old_status = dbuser.status
+ new_user = crud.update_user(db, dbuser, modified_user)
+ new_inbounds = {(i.node_id, i.protocol, i.tag) for i in new_user.inbounds}
+ user = UserResponse.model_validate(dbuser)
+
+ if user.status in [UserStatus.active, UserStatus.on_hold]:
+ asyncio.get_running_loop().create_task(xray.operations.update_user(
+ user=new_user, new_inbounds=new_inbounds, old_inbounds=old_inbounds)
+ )
+ else:
+ asyncio.get_running_loop().create_task(xray.operations.remove_user(dbuser))
+
+ asyncio.get_running_loop().create_task(report.user_updated(
+ user=user,
+ by=admin))
+
+ logger.info(f"User \"{user.username}\" modified")
+
+ if user.status != old_status:
+ asyncio.get_running_loop().create_task(report.status_change(
+ username=user.username,
+ status=user.status,
+ user=user,
+ by=admin))
+ logger.info(
+ f"User \"{dbuser.username}\" status changed from {old_status} to {user.status}")
+
+ return user
+
+
+@app.delete("/api/user/{username}", tags=['User'])
+async def remove_user(username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Remove a user
+ """
+ if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ await xray.operations.remove_user(dbuser)
+
+ # db.expunge(dbuser)
+ crud.remove_user(db, dbuser)
+ db.flush()
+
+ # asyncio.get_running_loop().create_task(xray.operations.remove_user(dbuser))
+
+ asyncio.get_running_loop().create_task(
+ report.user_deleted(
+ username=dbuser.username,
+ by=admin
+ ))
+ logger.info(f"User \"{username}\" deleted")
+ return {}
+
+
+@app.post("/api/user/{username}/reset", tags=['User'], response_model=UserResponse)
+async def reset_user_data_usage(username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Reset user data usage
+ """
+ if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ dbuser = crud.reset_user_data_usage(db=db, dbuser=dbuser)
+ if dbuser.status in [UserStatus.active, UserStatus.on_hold]:
+ asyncio.get_running_loop().create_task(xray.operations.add_user(dbuser=dbuser))
+
+ user = UserResponse.model_validate(dbuser)
+ asyncio.get_running_loop().create_task(report.user_data_usage_reset(
+ user=user,
+ by=admin))
+
+ logger.info(f"User \"{username}\"'s usage was reset")
+
+ return dbuser
+
+
+@app.post("/api/user/{username}/revoke_sub", tags=['User'], response_model=UserResponse)
+async def revoke_user_subscription(username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Revoke users subscription (Subscription link and proxies)
+ """
+ if not (admin.is_sudo or (dbuser.admin and dbuser.admin.username == admin.username)):
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ dbuser = crud.revoke_user_sub(db=db, dbuser=dbuser)
+
+ if dbuser.status in [UserStatus.active, UserStatus.on_hold]:
+ asyncio.get_running_loop().create_task(xray.operations.update_user(dbuser=dbuser))
+ user = UserResponse.model_validate(dbuser)
+ asyncio.get_running_loop().create_task(
+ report.user_subscription_revoked(
+ user=user,
+ by=admin
+ ))
+
+ logger.info(f"User \"{username}\" subscription revoked")
+
+ return user
+
+
+@app.get("/api/users", tags=['User'], response_model=UsersResponse)
+def get_users(offset: int = None,
+ limit: int = None,
+ username: List[str] = Query(None),
+ status: UserStatus = None,
+ sort: str = None,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Get all users
+ """
+ dbadmin = crud.get_admin(db, admin.username)
+
+ if sort is not None:
+ opts = sort.strip(',').split(',')
+ sort = []
+ for opt in opts:
+ try:
+ sort.append(crud.UsersSortingOptions[opt])
+ except KeyError:
+ raise HTTPException(status_code=400,
+ detail=f'"{opt}" is not a valid sort option')
+
+ users, count = crud.get_users(db=db,
+ offset=offset,
+ limit=limit,
+ usernames=username,
+ status=status,
+ sort=sort,
+ admin=dbadmin if not admin.is_sudo else None,
+ return_with_count=True)
+
+ return {"users": users, "total": count}
+
+
+@app.post("/api/users/reset", tags=['User'])
+async def reset_users_data_usage(db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Reset all users data usage
+ """
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbadmin = crud.get_admin(db, admin.username)
+ crud.reset_all_users_data_usage(db=db, admin=dbadmin)
+ # startup_config = xray.config.include_db_users()
+ await xray.core.restart(xray.config.include_db_users(node_id=0))
+ for node_id, node in list(xray.nodes.items()):
+ if node.connected:
+ xray.operations.restart_node(node_id, xray.config.include_db_users(node_id=node_id))
+ return {}
+
+
+@app.get("/api/user/{username}/usage", tags=['User'], response_model=UserUsagesResponse)
+def get_user_usage(username: str,
+ start: str = None,
+ end: str = None,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Get users usage
+ """
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ if start is None:
+ start_date = datetime.fromtimestamp(
+ datetime.utcnow().timestamp() - 30 * 24 * 3600)
+ else:
+ start_date = datetime.fromisoformat(start)
+
+ if end is None:
+ end_date = datetime.utcnow()
+ else:
+ end_date = datetime.fromisoformat(end)
+
+ usages = crud.get_user_usages(db, dbuser, start_date, end_date)
+
+ return {"usages": usages, "username": username}
+
+
+@app.put("/api/user/{username}/set-owner", tags=['User'], response_model=UserResponse)
+def set_owner(username: str,
+ admin_username: str,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+
+ if not admin.is_sudo:
+ raise HTTPException(status_code=403, detail="You're not allowed")
+
+ dbuser = crud.get_user(db, username)
+ if not dbuser:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ new_admin = crud.get_admin(db, username=admin_username)
+ if not new_admin:
+ raise HTTPException(status_code=404, detail="Admin not found")
+
+ dbuser = crud.set_owner(db, dbuser, new_admin)
+ user = UserResponse.model_validate(dbuser)
+
+ logger.info(f"{user.username}\"owner successfully set to{admin.username}")
+
+ return user
+
+
+@app.delete("/api/users/expired", tags=['User'])
+def delete_expired(passed_time: int,
+ db: Session = Depends(get_db),
+ admin: Admin = Depends(Admin.get_current)):
+ """
+ Delete expired users
+ - **passed_time** must be a timestamp
+ - This function will delete all expired users that meet the specified number of days passed and can't be undone.
+ """
+
+ dbadmin = crud.get_admin(db, admin.username)
+
+ dbusers = crud.get_users(db=db,
+ status=[UserStatus.expired, UserStatus.limited],
+ admin=dbadmin if not admin.is_sudo else None)
+
+ current_time = int(datetime.now(timezone.utc).timestamp())
+ expiration_threshold = current_time - passed_time
+ expired_users = [
+ user for user in dbusers if user.expire is not None and user.expire <= expiration_threshold]
+ if not expired_users:
+ raise HTTPException(status_code=404, detail=f'No expired user found.')
+
+ for dbuser in expired_users:
+ crud.remove_user(db, dbuser)
+
+ asyncio.get_running_loop().create_task(
+ report.user_deleted(
+ username=dbuser.username,
+ by=admin
+ ))
+
+ logger.info(f"User \"{dbuser.username}\" deleted")
+
+ return {}
diff --git a/app/xray/__init__.py b/app/xray/__init__.py
@@ -0,0 +1,82 @@
+from random import randint
+from typing import TYPE_CHECKING, Dict, Sequence
+
+from app.models.proxy import InboundHostSecurity
+from app.utils.store import DictStorage
+from app.utils.system import check_port
+from app.xray import operations
+from app.xray.config import XRayConfig
+from app.xray.core import XRayCore
+from app.xray.node import XRayNode
+from config import XRAY_ASSETS_PATH, XRAY_EXECUTABLE_PATH, XRAY_JSON
+from xray_api import XRay as XRayAPI
+from xray_api import exceptions
+from xray_api import exceptions as exc
+from xray_api import types
+
+
+core = XRayCore(XRAY_EXECUTABLE_PATH, XRAY_ASSETS_PATH)
+configs: Dict[int, XRayConfig] = {}
+
+# Search for a free API port
+try:
+ for api_port in range(randint(10000, 60000), 65536):
+ if not check_port(api_port):
+ break
+finally:
+ configs[0] = XRayConfig(XRAY_JSON, api_port=api_port)
+ del api_port
+
+api = XRayAPI(configs[0].api_host, configs[0].api_port)
+
+nodes: Dict[int, XRayNode] = {}
+
+
+if TYPE_CHECKING:
+ from app.db.models import InboundHost
+
+"""
+@DictStorage
+def hosts(storage: dict):
+ from app.db import GetDB, crud
+
+ storage.clear()
+ with GetDB() as db:
+ for inbound_id, node_id in config.inbounds_by_id:
+ inbound_hosts: Sequence[InboundHost] = crud.get_hosts(db, inbound_)
+
+ storage[inbound_tag] = [
+ {
+ "remark": host.remark,
+ "address": host.address,
+ "port": host.port,
+ "path": host.path if host.path else None,
+ "sni": [i.strip() for i in host.sni.split(',')] if host.sni else [],
+ "host": [i.strip() for i in host.host.split(',')] if host.host else [],
+ "alpn": host.alpn.value,
+ "fingerprint": host.fingerprint.value,
+ # None means the tls is not specified by host itself and
+ # complies with its inbound's settings.
+ "tls": None
+ if host.security == InboundHostSecurity.inbound_default
+ else host.security.value,
+ "allowinsecure":host.allowinsecure,
+ } for host in inbound_hosts if not host.is_disabled
+ ]
+
+"""
+
+__all__ = [
+ "configs",
+ "hosts",
+ "core",
+ "api",
+ "nodes",
+ "operations",
+ "exceptions",
+ "exc",
+ "types",
+ "XRayConfig",
+ "XRayCore",
+ "XRayNode",
+]
diff --git a/app/xray/config.py b/app/xray/config.py
@@ -0,0 +1,345 @@
+from __future__ import annotations
+
+import json
+from copy import deepcopy
+from pathlib import PosixPath
+from typing import Union, Optional
+
+import commentjson
+
+from app.db import GetDB, crud
+#from app.db.models import Inbound
+from app.models.proxy import ProxyTypes, InboundBase
+from app.models.user import UserStatus
+from app.utils.crypto import get_cert_SANs
+from app.utils.keygen import generate_settings
+from config import DEBUG, XRAY_EXCLUDE_INBOUND_TAGS, XRAY_FALLBACKS_INBOUND_TAG
+
+class XRayConfig(dict):
+ def __init__(self,
+ config: Union[dict, str, PosixPath] = {},
+ api_host: str = "127.0.0.1",
+ api_port: int = 8080):
+ if isinstance(config, str):
+ try:
+ # considering string as json
+ config = commentjson.loads(config)
+ except (json.JSONDecodeError, ValueError):
+ # considering string as file path
+ with open(config, 'r') as file:
+ config = commentjson.loads(file.read())
+
+ if isinstance(config, PosixPath):
+ with open(config, 'r') as file:
+ config = commentjson.loads(file.read())
+
+ if isinstance(config, dict):
+ config = deepcopy(config)
+
+ self.api_host = api_host
+ self.api_port = api_port
+
+ super().__init__(config)
+ self._validate()
+
+ self.inbounds = []
+ self.inbounds_by_protocol = {}
+ self.inbounds_by_tag = {}
+ self._fallbacks_inbound = self.get_inbound(XRAY_FALLBACKS_INBOUND_TAG)
+ self._addr_clients_by_tag = {}
+ self._resolve_inbounds()
+
+ self._apply_api()
+
+ def _apply_api(self):
+ api_inbound = self.get_inbound("API_INBOUND")
+ if api_inbound:
+ api_inbound["listen"] = self.api_host
+ api_inbound["listen"]["address"] = self.api_host
+ api_inbound["port"] = self.api_port
+ return
+
+ self["api"] = {
+ "services": [
+ "HandlerService",
+ "StatsService",
+ "LoggerService"
+ ],
+ "tag": "API"
+ }
+ self["stats"] = {}
+ self["policy"] = {
+ "levels": {
+ "0": {
+ "statsUserUplink": True,
+ "statsUserDownlink": True
+ }
+ },
+ "system": {
+ "statsInboundDownlink": False,
+ "statsInboundUplink": False,
+ "statsOutboundDownlink": True,
+ "statsOutboundUplink": True
+ }
+ }
+ inbound = {
+ "listen": self.api_host,
+ "port": self.api_port,
+ "protocol": "dokodemo-door",
+ "settings": {
+ "address": self.api_host
+ },
+ "tag": "API_INBOUND"
+ }
+ try:
+ self["inbounds"].insert(0, inbound)
+ except KeyError:
+ self["inbounds"] = []
+ self["inbounds"].insert(0, inbound)
+
+ rule = {
+ "inboundTag": [
+ "API_INBOUND"
+ ],
+ "outboundTag": "API",
+ "type": "field"
+ }
+ try:
+ self["routing"]["rules"].insert(0, rule)
+ except KeyError:
+ self["routing"] = {"rules": []}
+ self["routing"]["rules"].insert(0, rule)
+
+ def _validate(self):
+ if not self.get("inbounds"):
+ raise ValueError("config doesn't have inbounds")
+
+ if not self.get("outbounds"):
+ raise ValueError("config doesn't have outbounds")
+
+ for inbound in self['inbounds']:
+ if not inbound.get("tag"):
+ raise ValueError("all inbounds must have a unique tag")
+ for outbound in self['outbounds']:
+ if not outbound.get("tag"):
+ raise ValueError("all outbounds must have a unique tag")
+
+ def _resolve_inbounds(self):
+ for inbound in self['inbounds']:
+ if not inbound['protocol'] in ProxyTypes._value2member_map_:
+ continue
+
+ if inbound['tag'] in XRAY_EXCLUDE_INBOUND_TAGS:
+ continue
+
+ if not inbound.get('settings'):
+ inbound['settings'] = {}
+ if not inbound['settings'].get('clients'):
+ inbound['settings']['clients'] = []
+ self._addr_clients_by_tag[inbound['tag']
+ ] = inbound['settings']['clients']
+
+ settings = {
+ "tag": inbound["tag"],
+ "protocol": inbound["protocol"],
+ "port": None,
+ "network": "tcp",
+ "tls": 'none',
+ "sni": [],
+ "host": [],
+ "path": "",
+ "header_type": "",
+ "is_fallback": False
+ }
+
+ # port settings
+ try:
+ settings['port'] = inbound['port']
+ except KeyError:
+ if not self._fallbacks_inbound:
+ raise ValueError(
+ f"port missing on {inbound['tag']}"
+ "\nset XRAY_FALLBACKS_INBOUND_TAG if you're using an inbound containing fallbacks"
+ )
+ try:
+ settings['port'] = self._fallbacks_inbound['port']
+ settings['is_fallback'] = True
+ except KeyError:
+ raise ValueError("fallbacks inbound doesn't have port")
+
+ # stream settings
+ if stream := inbound.get('streamSettings'):
+ net = stream.get('network', 'tcp')
+ net_settings = stream.get(f"{net}Settings", {})
+ security = stream.get("security")
+ tls_settings = stream.get(f"{security}Settings")
+
+ if settings['is_fallback'] is True:
+ # probably this is a fallback
+ security = self._fallbacks_inbound.get(
+ 'streamSettings', {}).get('security')
+ tls_settings = self._fallbacks_inbound.get(
+ 'streamSettings', {}).get(f"{security}Settings", {})
+
+ settings['network'] = net
+
+ if security == 'tls':
+ # settings['fp']
+ # settings['alpn']
+ settings['tls'] = 'tls'
+ for certificate in tls_settings.get('certificates', []):
+
+ if certificate.get("certificateFile", None):
+ with open(certificate['certificateFile'], 'rb') as file:
+ cert = file.read()
+ settings['sni'].extend(get_cert_SANs(cert))
+
+ if certificate.get("certificate", None):
+ cert = certificate['certificate']
+ if isinstance(cert, list):
+ cert = '\n'.join(cert)
+ if isinstance(cert, str):
+ cert = cert.encode()
+ settings['sni'].extend(get_cert_SANs(cert))
+
+ elif security == 'reality':
+ settings['fp'] = 'chrome'
+ settings['tls'] = 'reality'
+ settings['sni'] = tls_settings.get('serverNames', [])
+
+ try:
+ settings['pbk'] = tls_settings['publicKey']
+ except KeyError:
+ pvk = tls_settings.get('privateKey')
+ if not pvk:
+ raise ValueError(
+ f"You need to provide privateKey in realitySettings of {inbound['tag']}")
+
+ try:
+ from app.xray import core
+ x25519 = core.get_x25519(pvk)
+ settings['pbk'] = x25519['public_key']
+ except ImportError:
+ pass
+
+ if not settings.get('pbk'):
+ raise ValueError(
+ f"You need to provide publicKey in realitySettings of {inbound['tag']}")
+
+ try:
+ settings['sid'] = tls_settings.get('shortIds')[0]
+ except (IndexError, TypeError):
+ raise ValueError(
+ f"You need to define at least one shortID in realitySettings of {inbound['tag']}")
+
+ if net == 'tcp':
+ header = net_settings.get('header', {})
+ request = header.get('request', {})
+ path = request.get('path')
+ host = request.get('headers', {}).get('Host')
+
+ settings['header_type'] = header.get('type', '')
+
+ if isinstance(path, str) or isinstance(host, str):
+ raise ValueError(f"Settings of {inbound['tag']} for path and host must be list, not str\n"
+ "https://xtls.github.io/config/transports/tcp.html#httpheaderobject")
+
+ if path and isinstance(path, list):
+ settings['path'] = path[0]
+
+ if host and isinstance(host, list):
+ settings['host'] = host
+
+ elif net == 'ws':
+ path = net_settings.get('path', '')
+ host = net_settings.get('headers', {}).get('Host')
+
+ settings['header_type'] = ''
+
+ if isinstance(path, list) or isinstance(host, list):
+ raise ValueError(f"Settings of {inbound['tag']} for path and host must be str, not list\n"
+ "https://xtls.github.io/config/transports/websocket.html#websocketobject")
+
+ if isinstance(path, str):
+ settings['path'] = path
+
+ if isinstance(host, str):
+ settings['host'] = [host]
+
+ elif net == 'grpc':
+ settings['header_type'] = ''
+ settings['path'] = net_settings.get('serviceName', '')
+ settings['host'] = []
+
+ else:
+ settings['path'] = net_settings.get('path', '')
+ host = net_settings.get(
+ 'host', {}) or net_settings.get('Host', {})
+ if host and isinstance(host, list):
+ settings['host'] = host[0]
+ elif host and isinstance(host, str):
+ settings['host'] = host
+
+ self.inbounds.append(settings)
+ self.inbounds_by_tag[inbound['tag']] = settings
+
+ try:
+ self.inbounds_by_protocol[inbound['protocol']].append(settings)
+ except KeyError:
+ self.inbounds_by_protocol[inbound['protocol']] = [settings]
+
+ def add_inbound_client(self, inbound_tag: str, email: str, settings: dict):
+ inbound = self.inbounds_by_tag.get(inbound_tag, {})
+ client = {"email": email, **settings}
+
+ # XTLS currently only supports transmission methods of TCP and mKCP
+ if client.get('flow') and (
+ inbound.get('network', 'tcp') not in ('tcp', 'kcp')
+ or
+ (
+ inbound.get('network', 'tcp') in ('tcp', 'kcp')
+ and
+ inbound.get('tls') not in ('tls', 'reality')
+ )
+ or
+ inbound.get('header_type') == 'http'
+ ):
+ del client['flow']
+
+ try:
+ self._addr_clients_by_tag[inbound_tag].append(client)
+ except KeyError:
+ return
+ return client
+
+ def get_inbound(self, tag) -> dict:
+ for inbound in self['inbounds']:
+ if inbound['tag'] == tag:
+ return inbound
+
+ def get_outbound(self, tag) -> dict:
+ for outbound in self['outbounds']:
+ if outbound['tag'] == tag:
+ return outbound
+
+ def to_json(self, **json_kwargs):
+ return json.dumps(self, **json_kwargs)
+
+ def copy(self):
+ return deepcopy(self)
+
+ def include_db_users(self, node_id: int) -> XRayConfig:
+ config = self.copy()
+
+ with GetDB() as db:
+ inbounds = [InboundBase(protocol=i["protocol"], tag=i["tag"]) for i in self.inbounds]
+ crud.assure_node_inbounds(db,
+ inbounds=inbounds, node_id=node_id)
+ for user_id, username, user_key, inbound_protocol, inbound_tag in crud.get_node_users(db, statuses=[UserStatus.active,
+ UserStatus.on_hold], node_id=node_id):
+ config.add_inbound_client(inbound_tag, f"{user_id}.{username}", generate_settings(user_key, inbound_protocol))
+ if DEBUG:
+ with open('generated_config-debug.json', 'w') as f:
+ f.write(config.to_json(indent=4))
+
+ return config
diff --git a/app/xray/core.py b/app/xray/core.py
@@ -0,0 +1,151 @@
+import asyncio
+import atexit
+import re
+import subprocess
+import threading
+from collections import deque
+from contextlib import contextmanager
+from anyio import create_memory_object_stream, run, ClosedResourceError, BrokenResourceError
+
+from app import logger
+from app.xray.config import XRayConfig
+from config import DEBUG
+
+
+class XRayCore:
+ def __init__(self,
+ executable_path: str = "/usr/bin/xray",
+ assets_path: str = "/usr/share/xray"):
+ self.executable_path = executable_path
+ self.assets_path = assets_path
+
+ self.version = self.get_version()
+ self.process = None
+ self.restarting = False
+
+ self._snd_stms, self._rcv_stms = [], []
+ self._logs_buffer = deque(maxlen=100)
+ self._on_start_funcs = []
+ self._on_stop_funcs = []
+ self._env = {
+ "XRAY_LOCATION_ASSET": assets_path
+ }
+
+ atexit.register(lambda: self.stop() if self.started else None)
+
+ async def aget_version(self):
+ cmd = [self.executable_path, "version"]
+ p = await asyncio.create_subprocess_shell(" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
+ stdout = (await p.communicate())[0]
+ return m.groups()[0] if (m := re.match(r'^Xray (\d+\.\d+\.\d+)', stdout.decode("utf-8"))) else None
+
+ def get_version(self):
+ return asyncio.get_event_loop().run_until_complete(self.aget_version())
+
+ async def aget_x25519(self, private_key: str = None):
+ cmd = [self.executable_path, "x25519"]
+ if private_key:
+ cmd.extend(['-i', private_key])
+ p = await asyncio.create_subprocess_shell(" ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
+ stdout = (await p.communicate())[0]
+ if m := re.match(r'Private key: (.+)\nPublic key: (.+)', stdout.decode()):
+ private, public = m.groups()
+ return {
+ "private_key": private,
+ "public_key": public
+ }
+
+ def get_x25519(self, private_key: str = None):
+ # just calls the same function in a blocking way
+ return asyncio.get_event_loop().run_until_complete(self.aget_x25519(private_key))
+
+ async def __capture_process_logs(self):
+ # capture the logs, push it into the stream, and store it in the deck
+ # note that the stream blocks sending if it's full, so a deck is necessary
+ try:
+ while output := (await self.process.stdout.readline()):
+ # self._logs_buffer.append(output)
+ for stm in self._snd_stms:
+ try:
+ await stm.send(output)
+ except (ClosedResourceError, BrokenResourceError):
+ self._snd_stms.remove(stm)
+ continue
+ except Exception as e:
+ print(e, type(e), flush=True)
+
+ async def get_logs_stm(self):
+ new_snd_stm, new_rcv_stm = create_memory_object_stream()
+ self._snd_stms.append(new_snd_stm)
+ return new_rcv_stm
+
+ def get_buffer(self):
+ # makes a copy of the buffer, so it could be read multiple times
+ # the buffer is never cleared in case logs from xray's exit are useful
+ return self._logs_buffer.copy()
+
+ @property
+ def started(self):
+ if not self.process:
+ return False
+
+ if self.process.returncode is None:
+ return True
+
+ return False
+
+ async def start(self, config: XRayConfig):
+ if self.started is True:
+ raise RuntimeError("Xray is started already")
+
+ if config.get('log', {}).get('logLevel') in ('none', 'error'):
+ config['log']['logLevel'] = 'warning'
+
+ cmd = [
+ self.executable_path,
+ "run",
+ '-config',
+ 'stdin:'
+ ]
+ self.process = await asyncio.create_subprocess_shell(
+ " ".join(cmd),
+ env=self._env,
+ stdin=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE
+ )
+ self.process.stdin.write(str.encode(config.to_json()))
+ await self.process.stdin.drain()
+ self.process.stdin.close()
+ await self.process.stdin.wait_closed()
+ logger.warning(f"Xray core {self.version} started")
+
+ asyncio.get_event_loop().create_task(self.__capture_process_logs())
+
+ async def stop(self):
+ if not self.started:
+ return
+
+ self.process.terminate()
+ self.process = None
+ logger.warning("Xray core stopped")
+
+ async def restart(self, config: XRayConfig):
+ if self.restarting is True:
+ return
+
+ try:
+ self.restarting = True
+ logger.warning("Restarting Xray core...")
+ await self.stop()
+ await self.start(config)
+ finally:
+ self.restarting = False
+
+ def on_start(self, func: callable):
+ self._on_start_funcs.append(func)
+ return func
+
+ def on_stop(self, func: callable):
+ self._on_stop_funcs.append(func)
+ return func
diff --git a/app/xray/node.py b/app/xray/node.py
@@ -0,0 +1,164 @@
+import re
+import ssl
+import tempfile
+# import time
+from contextlib import contextmanager
+from typing import List
+
+from grpclib.health.v1.health_pb2 import HealthCheckRequest, HealthCheckResponse
+from grpclib.health.v1.health_grpc import HealthStub
+from grpclib.exceptions import GRPCError
+from grpclib.client import Channel
+from mnode.service_pb2 import XrayFetchVersionRequest, XrayStartRequest, XrayStopRequest, XrayRestartRequest, XrayFetchVersionRequest
+from mnode.service_grpc import XrayServiceStub
+
+from app.xray.config import XRayConfig
+from xray_api import XRay as XRayAPI
+
+
+def string_to_temp_file(content: str):
+ file = tempfile.NamedTemporaryFile(mode='w+t')
+ file.write(content)
+ file.flush()
+ return file
+
+
+class XRayNode:
+
+ def __init__(self,
+ address: str,
+ port: int,
+ api_port: int,
+ ssl_key: str,
+ ssl_cert: str,
+ usage_coefficient: float = 1):
+
+ self.address = address
+ self.port = port
+ self.api_port = api_port
+ self.ssl_key = ssl_key
+ self.ssl_cert = ssl_cert
+ self.usage_coefficient = usage_coefficient
+
+ self.started = False
+
+ self._keyfile = string_to_temp_file(ssl_key)
+ self._certfile = string_to_temp_file(ssl_cert)
+
+ ctx = ssl.create_default_context()# cadata=ssl_cert)
+ ctx.load_cert_chain(self._certfile.name, self._keyfile.name)
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ self._channel = Channel(self.address, self.port, ssl=ctx)
+ self._stub = XrayServiceStub(self._channel)
+ self._health = HealthStub(self._channel)
+ # self._service = Service()
+ self._api = None
+
+ async def is_healthy(self):
+ try:
+ response = await self._health.Check(HealthCheckRequest())
+ except (ConnectionError, GRPCError):
+ pass
+ else:
+ if response.status is HealthCheckResponse.SERVING:
+ return True
+ return False
+
+ @property
+ def api(self):
+ return self._api
+
+ def _prepare_config(self, config: XRayConfig):
+ for inbound in config.get("inbounds", []):
+ streamSettings = inbound.get("streamSettings") or {}
+ tlsSettings = streamSettings.get("tlsSettings") or {}
+ certificates = tlsSettings.get("certificates") or []
+ for certificate in certificates:
+ if certificate.get("certificateFile"):
+ with open(certificate['certificateFile']) as file:
+ certificate['certificate'] = [
+ line.strip() for line in file.readlines()
+ ]
+ del certificate['certificateFile']
+
+ if certificate.get("keyFile"):
+ with open(certificate['keyFile']) as file:
+ certificate['key'] = [
+ line.strip() for line in file.readlines()
+ ]
+ del certificate['keyFile']
+
+ return config
+
+ async def fetch_xray_version(self):
+ async with self._stub.XrayFetchVersion.open() as stm:
+ await stm.send_message(XrayFetchVersionRequest())
+ r = await stm.recv_message()
+ return r.version
+
+ async def start(self, config: XRayConfig):
+ config = self._prepare_config(config)
+
+ json_config = config.to_json()
+ async with self._stub.XrayStart.open() as stm:
+ await stm.send_message(XrayStartRequest(config=json_config))
+ r = await stm.recv_message()
+ if r.success is True or r.already_started is True:
+ self.started = True
+ self._node_cert = ssl.get_server_certificate((self.address, self.port))
+ self._node_certfile = string_to_temp_file(self._node_cert)
+ # connect to API
+ self._api = XRayAPI(
+ address=self.address,
+ port=self.api_port,
+ ssl_cert=self._node_cert,
+ ssl_target_name="Gozargah"
+ )
+ """try:
+ grpc.channel_ready_future(self._api._channel).result(timeout=5)
+ except grpc.FutureTimeoutError:
+
+ start_time = time.time()
+ end_time = start_time + 3 # check logs for 3 seconds
+ last_log = ''
+ with self.get_logs() as logs:
+ while time.time() < end_time:
+ if logs:
+ last_log = logs[-1].strip().split('n')[-1]
+ time.sleep(0.1)
+
+ self.disconnect()
+
+ if re.search(r'[Ff]ailed', last_log):
+ raise RuntimeError(last_log)
+
+ raise ConnectionError('Failed to connect to node's API')"""
+
+ async def stop(self):
+ async with self._stub.XrayStop.open() as stm:
+ await stm.send_message(XrayStopRequest())
+ await stm.recv_message()
+ # self.remote.stop()
+ self.started = False
+ self._api = None
+
+ async def restart(self, config: XRayConfig):
+ self.started = False
+ config = self._prepare_config(config)
+ json_config = config.to_json()
+ async with self._stub.XrayRestart.open() as stm:
+ await stm.send_message(XrayRestartRequest(config=json_config))
+ r = await stm.recv_message()
+ if r.success is True:
+ self.started = True
+
+ @contextmanager
+ async def get_logs(self):
+ if not self.connected:
+ raise ConnectionError("Node is not connected")
+ async with self._stub.XrayFetchLogs.open() as stm:
+ await stm.send_message(XrayFetchLogsRequest())
+ async for l in stm:
+ yield l
+
diff --git a/app/xray/operations.py b/app/xray/operations.py
@@ -0,0 +1,292 @@
+from functools import lru_cache
+from typing import TYPE_CHECKING
+import traceback
+
+from sqlalchemy.exc import SQLAlchemyError
+
+from app import logger, xray
+from app.db import GetDB, crud
+from app.models.node import NodeStatus
+from app.models.user import UserResponse
+from app.utils.keygen import generate_settings
+from app.xray.node import XRayNode
+from xray_api import XRay as XRayAPI
+from xray_api.types.account import Account, XTLSFlows
+
+if TYPE_CHECKING:
+ from app.db import User as DBUser
+ from app.db.models import Node as DBNode
+
+
+@lru_cache(maxsize=None)
+def get_tls():
+ from app.db import GetDB, get_tls_certificate
+ with GetDB() as db:
+ tls = get_tls_certificate(db)
+ return {
+ "key": tls.key,
+ "certificate": tls.certificate
+ }
+
+
+async def _add_user_to_inbound(api: XRayAPI, inbound_tag: str, account: Account):
+ logger.debug(f"adding user {account.email} to inbound {inbound_tag}")
+ try:
+ await api.add_inbound_user(tag=inbound_tag, user=account)
+ except (xray.exc.EmailExistsError, xray.exc.ConnectionError):
+ pass
+
+
+async def _remove_user_from_inbound(api: XRayAPI, inbound_tag: str, email: str):
+ logger.debug(f"removing user {email} from inbound {inbound_tag}")
+ try:
+ await api.remove_inbound_user(tag=inbound_tag, email=email)
+ except (xray.exc.EmailNotFoundError, xray.exc.ConnectionError):
+ pass
+
+
+async def _alter_inbound_user(api: XRayAPI, inbound_tag: str, account: Account):
+ try:
+ await api.remove_inbound_user(tag=inbound_tag, email=account.email)
+ except (xray.exc.EmailNotFoundError, xray.exc.ConnectionError):
+ pass
+ try:
+ await api.add_inbound_user(tag=inbound_tag, user=account)
+ except (xray.exc.EmailExistsError, xray.exc.ConnectionError):
+ pass
+
+
+async def add_user(user: "DBUser"):
+ email, key = f"{user.id}.{user.username}", user.key
+
+ for inb in user.inbounds:
+ proxy_type, tag = inb.protocol, inb.tag
+ # for inbound_tag in inbound_tags:
+ # inbound = xray.config.inbounds_by_tag.get(inbound_tag, {})
+
+ proxy_settings = generate_settings(key, proxy_type)# user.proxies[proxy_type].dict(no_obj=True)
+
+ account = proxy_type.account_model(email=email, **proxy_settings)
+
+ # XTLS currently only supports transmission methods of TCP and mKCP TODO
+ """
+ if getattr(account, 'flow', None) and (
+ inbound.get('network', 'tcp') not in ('tcp', 'kcp')
+ or
+ (
+ inbound.get('network', 'tcp') in ('tcp', 'kcp')
+ and
+ inbound.get('tls') not in ('tls', 'reality')
+ )
+ or
+ inbound.get('header_type') == 'http'
+ ):
+ account.flow = XTLSFlows.NONE
+ """
+ if inb.node_id == 0:
+ await _add_user_to_inbound(xray.api, tag, account) # main core
+ else:
+ node = xray.nodes[inb.node_id]
+ if await node.is_healthy():
+ await _add_user_to_inbound(node.api, tag, account)
+ #for node in list(xray.nodes.values()):
+ # if await node.is_healthy():
+ # await _add_user_to_inbound(node.api, tag, account)
+
+
+async def remove_user(dbuser: "DBUser"):
+ email = f"{dbuser.id}.{dbuser.username}"
+
+ for inb in dbuser.inbounds:
+ if inb.node_id == 0:
+ await _remove_user_from_inbound(xray.api, inb.tag, email)
+ else:
+ await _remove_user_from_inbound(xray.nodes[node_id].api, inb.tag, email)
+ #for node in list(xray.nodes.values()):
+ # if await node.is_healthy():
+ # await _remove_user_from_inbound(node.api, inb.tag, email)
+ """
+ for inbound_tag in xray.config.inbounds_by_tag:
+ await _remove_user_from_inbound(xray.api, inbound_tag, email)
+ for node in list(xray.nodes.values()):
+ if await node.is_healthy():
+ await _remove_user_from_inbound(node.api, inbound_tag, email)
+ """
+
+async def update_user(user: "DBUser", new_inbounds: set, old_inbounds: set):
+ # user = UserResponse.model_validate(dbuser)
+ email = f"{user.id}.{user.username}"
+ key = user.key
+ activated_inbounds = new_inbounds - old_inbounds
+ disabled_inbounds = old_inbounds - new_inbounds
+
+ for node_id, protocol, tag in activated_inbounds:
+ account = protocol.account_model(email=email, **generate_settings(key, protocol))
+ if node_id == 0:
+ await _add_user_to_inbound(xray.api, tag, account)
+ else:
+ node = xray.nodes[node_id]
+ if await node.is_healthy():
+ await _add_user_to_inbound(node.api, tag, account)
+
+ for node_id, protocol, tag in disabled_inbounds:
+ if node_id == 0:
+ await _remove_user_from_inbound(xray.api, tag, email)
+ else:
+ node = xray.nodes[node_id]
+ if await node.is_healthy():
+ await _remove_user_from_inbound(node.api, tag, email)
+ """
+ active_inbounds = []
+ for proxy_type, inbound_tags in user.inbounds.items():
+ for inbound_tag in inbound_tags:
+ active_inbounds.append(inbound_tag)
+ inbound = xray.config.inbounds_by_tag.get(inbound_tag, {})
+
+ try:
+ proxy_settings = user.proxies[proxy_type].dict(no_obj=True)
+ except KeyError:
+ pass
+ account = proxy_type.account_model(email=email, **proxy_settings)
+
+ # XTLS currently only supports transmission methods of TCP and mKCP
+ if getattr(account, 'flow', None) and (
+ inbound.get('network', 'tcp') not in ('tcp', 'kcp')
+ or
+ (
+ inbound.get('network', 'tcp') in ('tcp', 'kcp')
+ and
+ inbound.get('tls') not in ('tls', 'reality')
+ )
+ or
+ inbound.get('header_type') == 'http'
+ ):
+ account.flow = XTLSFlows.NONE
+
+ await _alter_inbound_user(xray.api, inbound_tag, account) # main core
+ for node in list(xray.nodes.values()):
+ if await node.is_healthy():
+ await _alter_inbound_user(node.api, inbound_tag, account)
+
+ for inbound_tag in xray.config.inbounds_by_tag:
+ if inbound_tag in active_inbounds:
+ continue
+ # remove disabled inbounds
+ await _remove_user_from_inbound(xray.api, inbound_tag, email)
+ for node in list(xray.nodes.values()):
+ if await node.is_healthy():
+ await _remove_user_from_inbound(node.api, inbound_tag, email)"""
+
+
+async def remove_node(node_id: int):
+ if node_id in xray.nodes:
+ try:
+ await xray.nodes[node_id].stop()
+ except Exception:
+ pass
+ finally:
+ del xray.nodes[node_id]
+
+
+async def add_node(dbnode: "DBNode"):
+ await remove_node(dbnode.id)
+
+ tls = get_tls()
+ xray.nodes[dbnode.id] = XRayNode(address=dbnode.address,
+ port=dbnode.port,
+ api_port=dbnode.api_port,
+ ssl_key=tls['key'],
+ ssl_cert=tls['certificate'],
+ usage_coefficient=dbnode.usage_coefficient)
+
+ return xray.nodes[dbnode.id]
+
+
+def _change_node_status(node_id: int, status: NodeStatus, message: str = None, version: str = None):
+ with GetDB() as db:
+ try:
+ dbnode = crud.get_node_by_id(db, node_id)
+ if not dbnode:
+ return
+ crud.update_node_status(db, dbnode, status, message, version)
+ except SQLAlchemyError:
+ db.rollback()
+
+
+# @threaded_function
+async def start_node(node_id, config):
+ with GetDB() as db:
+ dbnode = crud.get_node_by_id(db, node_id)
+
+ if not dbnode:
+ return
+
+ try:
+ node = xray.nodes[dbnode.id]
+ assert await node.is_healthy()
+ except (KeyError, AssertionError):
+ node = await xray.operations.add_node(dbnode)
+
+ try:
+ _change_node_status(node_id, NodeStatus.connecting)
+ logger.info(f"Starting \"{dbnode.name}\" node")
+ await node.start(config)
+ version = await node.fetch_xray_version()
+ _change_node_status(node_id, NodeStatus.connected, version=version)
+ logger.info(f"Started \"{dbnode.name}\" node, xray run on v{version}")
+ except Exception as e:
+ _change_node_status(node_id, NodeStatus.error, message=str(e))
+ logger.info(f"Unable to Start \"{dbnode.name}\" node")
+
+
+# @threaded_function
+async def restart_node(node_id, config):
+ with GetDB() as db:
+ dbnode = crud.get_node_by_id(db, node_id)
+
+ if not dbnode:
+ return
+
+ try:
+ node = xray.nodes[dbnode.id]
+ except KeyError:
+ node = xray.operations.add_node(dbnode)
+
+ if not await node.is_healthy():
+ return await start_node(node_id, config)
+
+ if not node.started:
+ try:
+ logger.info(f"Starting Xray core of \"{dbnode.name}\" node")
+ await node.start(config)
+ logger.info(f"Xray core of \"{dbnode.name}\" node started")
+ except Exception as e:
+ _change_node_status(node_id, NodeStatus.error, message=str(e))
+ logger.info(f"Unable to start node {node_id}")
+ try:
+ node.disconnect()
+ except Exception:
+ pass
+ return
+
+ try:
+ logger.info(f"Restarting Xray core of \"{dbnode.name}\" node")
+ await node.restart(config)
+ logger.info(f"Xray core of \"{dbnode.name}\" node restarted")
+ except Exception as e:
+ _change_node_status(node_id, NodeStatus.error, message=str(e))
+ logger.info(f"Unable to restart node {node_id}")
+ try:
+ node.disconnect()
+ except Exception:
+ pass
+
+
+__all__ = [
+ "add_user",
+ "remove_user",
+ "add_node",
+ "remove_node",
+ "connect_node",
+ "restart_node",
+]
diff --git a/build_dashboard.sh b/build_dashboard.sh
@@ -0,0 +1,3 @@
+cd `dirname $0`/app/dashboard
+VITE_BASE_API=/api/ npm run build --if-present -- --outDir build --base '/dashboard/'
+cp ./build/index.html ./build/404.html
\ No newline at end of file
diff --git a/cli/README.md b/cli/README.md
@@ -0,0 +1,300 @@
+# CLI
+
+**Usage**:
+
+```console
+$ [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `admin`
+* `completion`: Generate and install completion scripts.
+* `subscription`
+* `user`
+
+## `admin`
+
+**Usage**:
+
+```console
+$ admin [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `create`: Creates an admin
+* `delete`: Deletes the specified admin
+* `import-from-env`: Imports the sudo admin from env
+* `list`: Displays a table of admins
+* `update`: Updates the specified admin
+
+### `admin create`
+
+Creates an admin
+
+Password can also be set using the `MARZBAN_ADMIN_PASSWORD` environment variable for non-interactive usages.
+
+**Usage**:
+
+```console
+$ admin create [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`: [required]
+* `--sudo / --no-sudo`
+* `--help`: Show this message and exit.
+
+### `admin delete`
+
+Deletes the specified admin
+
+Confirmations can be skipped using `--yes/-y` option.
+
+**Usage**:
+
+```console
+$ admin delete [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`: [required]
+* `-y, --yes`: Skips confirmations
+* `--help`: Show this message and exit.
+
+### `admin import-from-env`
+
+Imports the sudo admin from env
+
+Confirmations can be skipped using `--yes/-y` option.
+
+What does it do?
+ - Creates a sudo admin according to `SUDO_USERNAME` and `SUDO_PASSWORD`.
+ - Links any user which doesn't have an `admin_id` to the imported sudo admin.
+
+**Usage**:
+
+```console
+$ admin import-from-env [OPTIONS]
+```
+
+**Options**:
+
+* `-y, --yes`: Skips confirmations
+* `--help`: Show this message and exit.
+
+### `admin list`
+
+Displays a table of admins
+
+**Usage**:
+
+```console
+$ admin list [OPTIONS]
+```
+
+**Options**:
+
+* `-o, --offset INTEGER`
+* `-l, --limit INTEGER`
+* `-u, --username TEXT`: Search by username
+* `--help`: Show this message and exit.
+
+### `admin update`
+
+Updates the specified admin
+
+NOTE: This command CAN NOT be used non-interactively.
+
+**Usage**:
+
+```console
+$ admin update [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`: [required]
+* `--help`: Show this message and exit.
+
+## `completion`
+
+Generate and install completion scripts.
+
+**Usage**:
+
+```console
+$ completion [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `install`: Install completion for the specified shell.
+* `show`: Show completion for the specified shell,...
+
+### `completion install`
+
+Install completion for the specified shell.
+
+**Usage**:
+
+```console
+$ completion install [OPTIONS] [SHELL]:[bash|zsh|fish|powershell|pwsh]
+```
+
+**Arguments**:
+
+* `[SHELL]:[bash|zsh|fish|powershell|pwsh]`: The shell to install completion for.
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+### `completion show`
+
+Show completion for the specified shell, to copy or customize it.
+
+**Usage**:
+
+```console
+$ completion show [OPTIONS] [SHELL]:[bash|zsh|fish|powershell|pwsh]
+```
+
+**Arguments**:
+
+* `[SHELL]:[bash|zsh|fish|powershell|pwsh]`: The shell to install completion for.
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+## `subscription`
+
+**Usage**:
+
+```console
+$ subscription [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `get-config`: Generates a subscription config.
+* `get-link`: Prints the given user's subscription link.
+
+### `subscription get-config`
+
+Generates a subscription config.
+
+Generates a subscription config for the given user in the given format.
+
+The output will be written in the output file when the `output-file` is present,
+ otherwise will be shown in the terminal.
+
+**Usage**:
+
+```console
+$ subscription get-config [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`: [required]
+* `-f, --format [v2ray|clash]`: [required]
+* `-o, --output TEXT`: Writes the generated config in the file if provided
+* `--base64`: Encodes output in base64 format if present
+* `--help`: Show this message and exit.
+
+### `subscription get-link`
+
+Prints the given user's subscription link.
+
+NOTE: This command needs `XRAY_SUBSCRIPTION_URL_PREFIX` environment variable to be set
+ in order to work correctly.
+
+**Usage**:
+
+```console
+$ subscription get-link [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`: [required]
+* `--help`: Show this message and exit.
+
+## `user`
+
+**Usage**:
+
+```console
+$ user [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `list`: Displays a table of users
+* `set-owner`: Transfers user's ownership
+
+### `user list`
+
+Displays a table of users
+
+NOTE: Sorting is not currently available.
+
+**Usage**:
+
+```console
+$ user list [OPTIONS]
+```
+
+**Options**:
+
+* `-o, --offset INTEGER`
+* `-l, --limit INTEGER`
+* `-u, --username TEXT`: Search by username
+* `--status [active|disabled|limited|expired]`
+* `--admin, --owner TEXT`: Search by owner admin's username
+* `--help`: Show this message and exit.
+
+### `user set-owner`
+
+Transfers user's ownership
+
+NOTE: This command needs additional confirmation for users who already have an owner.
+
+**Usage**:
+
+```console
+$ user set-owner [OPTIONS]
+```
+
+**Options**:
+
+* `-u, --username TEXT`
+* `--admin, --owner TEXT`: Admin's username
+* `-y, --yes`: Skips confirmations
+* `--help`: Show this message and exit.
+
diff --git a/cli/__init__.py b/cli/__init__.py
diff --git a/cli/admin.py b/cli/admin.py
@@ -0,0 +1,170 @@
+from typing import Optional, Union
+
+import typer
+from rich.table import Table
+from rich.console import Console
+from rich.panel import Panel
+from sqlalchemy.exc import IntegrityError
+from decouple import config, UndefinedValueError
+
+from app.db import GetDB
+from app.db import crud
+from app.db.models import Admin, User
+from app.models.admin import AdminCreate, AdminPartialModify
+from . import utils
+
+app = typer.Typer(no_args_is_help=True)
+
+
+@app.command(name="list")
+def list_admins(
+ offset: Optional[int] = typer.Option(None, *utils.FLAGS["offset"]),
+ limit: Optional[int] = typer.Option(None, *utils.FLAGS["limit"]),
+ username: Optional[str] = typer.Option(None, *utils.FLAGS["username"], help="Search by username"),
+):
+ """Displays a table of admins"""
+ with GetDB() as db:
+ admins: list[Admin] = crud.get_admins(db, offset=offset, limit=limit, username=username)
+ utils.print_table(
+ table=Table("Username", "Is sudo", "Created at"),
+ rows=[
+ (str(admin.username),
+ "✔️" if admin.is_sudo else "✖️",
+ utils.readable_datetime(admin.created_at))
+ for admin in admins
+ ]
+ )
+
+
+@app.command(name="delete")
+def delete_admin(
+ username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True),
+ yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations")
+):
+ """
+ Deletes the specified admin
+
+ Confirmations can be skipped using `--yes/-y` option.
+ """
+ with GetDB() as db:
+ admin: Union[Admin, None] = crud.get_admin(db, username=username)
+ if not admin:
+ utils.error(f"There's no admin with username \"{username}\"!")
+
+ if yes_to_all or typer.confirm(f'Are you sure about deleting "{username}"?', default=False):
+ crud.remove_admin(db, admin)
+ utils.success(f'"{username}" deleted successfully.')
+ else:
+ utils.error("Operation aborted!")
+
+
+@app.command(name="create")
+def create_admin(
+ username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True),
+ is_sudo: bool = typer.Option(None, *utils.FLAGS["is_sudo"], prompt=True),
+ password: str = typer.Option(..., prompt=True, confirmation_prompt=True,
+ hide_input=True, hidden=True, envvar=utils.PASSWORD_ENVIRON_NAME)
+):
+ """
+ Creates an admin
+
+ Password can also be set using the `MARZBAN_ADMIN_PASSWORD` environment variable for non-interactive usages.
+ """
+ with GetDB() as db:
+ try:
+ crud.create_admin(db, AdminCreate(username=username, password=password, is_sudo=is_sudo))
+ utils.success(f'Admin "{username}" created successfully.')
+ except IntegrityError:
+ utils.error(f'Admin "{username}" already exists!')
+
+
+@app.command(name="update")
+def update_admin(username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True)):
+ """
+ Updates the specified admin
+
+ NOTE: This command CAN NOT be used non-interactively.
+ """
+
+ def _get_modify_model(admin: Admin):
+ Console().print(
+ Panel(f'Editing "{username}". Just press "Enter" to leave each field unchanged.')
+ )
+
+ is_sudo: bool = typer.confirm("Is sudo", default=admin.is_sudo)
+ new_password: Union[str, None] = typer.prompt(
+ "New password",
+ default="",
+ show_default=False,
+ confirmation_prompt=True,
+ hide_input=True
+ ) or None
+
+ return AdminPartialModify(
+ is_sudo=is_sudo,
+ password=new_password,
+ )
+
+ with GetDB() as db:
+ admin: Union[Admin, None] = crud.get_admin(db, username=username)
+ if not admin:
+ utils.error(f"There's no admin with username \"{username}\"!")
+
+ crud.partial_update_admin(db, admin, _get_modify_model(admin))
+ utils.success(f'Admin "{username}" updated successfully.')
+
+
+@app.command(name="import-from-env")
+def import_from_env(yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations")):
+ """
+ Imports the sudo admin from env
+
+ Confirmations can be skipped using `--yes/-y` option.
+
+ What does it do?
+ - Creates a sudo admin according to `SUDO_USERNAME` and `SUDO_PASSWORD`.
+ - Links any user which doesn't have an `admin_id` to the imported sudo admin.
+ """
+ try:
+ username, password = config("SUDO_USERNAME"), config("SUDO_PASSWORD")
+ except UndefinedValueError:
+ utils.error(
+ "Unable to get SUDO_USERNAME and/or SUDO_PASSWORD.\n"
+ "Make sure you have set them in the env file or as environment variables."
+ )
+
+ if not (username and password):
+ utils.error("Unable to retrieve username and password.\n"
+ "Make sure both SUDO_USERNAME and SUDO_PASSWORD are set.")
+
+ with GetDB() as db:
+ admin: Union[None, Admin] = None
+
+ # If env admin already exists
+ if current_admin := crud.get_admin(db, username=username):
+ if not yes_to_all and not typer.confirm(
+ f'Admin "{username}" already exists. Do you want to sync it with env?', default=None
+ ):
+ utils.error("Aborted.")
+
+ admin = crud.partial_update_admin(
+ db,
+ current_admin,
+ AdminPartialModify(password=password, is_sudo=True)
+ )
+ # If env admin does not exist yet
+ else:
+ admin = crud.create_admin(db, AdminCreate(
+ username=username,
+ password=password,
+ is_sudo=True
+ ))
+
+ updated_user_count = db.query(User).filter_by(admin_id=None).update({"admin_id": admin.id})
+ db.commit()
+
+ utils.success(
+ f'Admin "{username}" imported successfully.\n'
+ f"{updated_user_count} users' admin_id set to the {username}'s id.\n"
+ 'You must delete SUDO_USERNAME and SUDO_PASSWORD from your env file now.'
+ )
diff --git a/cli/subscription.py b/cli/subscription.py
@@ -0,0 +1,75 @@
+import typer
+from enum import Enum
+from typing import Optional
+from rich.console import Console
+
+from app.db import GetDB
+from app.models.user import UserResponse
+from app.utils.share import generate_subscription
+
+from . import utils
+
+app = typer.Typer(no_args_is_help=True)
+console = Console()
+
+
+class ConfigFormat(str, Enum):
+ v2ray = "v2ray"
+ clash = "clash"
+
+
+@app.command(name="get-link")
+def get_link(
+ username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True)
+):
+ """
+ Prints the given user's subscription link.
+
+ NOTE: This command needs `XRAY_SUBSCRIPTION_URL_PREFIX` environment variable to be set
+ in order to work correctly.
+ """
+ with GetDB() as db:
+ user: UserResponse = UserResponse.from_orm(utils.get_user(db, username))
+ print(user.subscription_url)
+
+
+@app.command(name="get-config")
+def get_config(
+ username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True),
+ config_format: ConfigFormat = typer.Option(..., *utils.FLAGS["format"], prompt=True),
+ output_file: Optional[str] = typer.Option(
+ None, *utils.FLAGS["output_file"], help="Writes the generated config in the file if provided"
+ ),
+ as_base64: bool = typer.Option(
+ False, "--base64", is_flag=True, help="Encodes output in base64 format if present"
+ )
+):
+ """
+ Generates a subscription config.
+
+ Generates a subscription config for the given user in the given format.
+
+ The output will be written in the output file when the `output-file` is present,
+ otherwise will be shown in the terminal.
+ """
+ with GetDB() as db:
+ user: UserResponse = UserResponse.from_orm(utils.get_user(db, username))
+ conf: str = generate_subscription(
+ user=user, config_format=config_format.name, as_base64=as_base64
+ )
+
+ if output_file:
+ with open(output_file, "w") as out_file:
+ out_file.write(conf)
+
+ utils.success(
+ f'{username}\'s configuration in "{config_format.value}" format'
+ f' successfully save to "{output_file}".'
+ )
+ else:
+ utils.success(
+ 'No output file specified.'
+ f' using pager for {username}\'s config in "{config_format}" format.',
+ auto_exit=False
+ )
+ utils.paginate(conf)
diff --git a/cli/user.py b/cli/user.py
@@ -0,0 +1,83 @@
+from typing import List, Optional
+
+import typer
+from rich.table import Table
+
+from app.db import GetDB, crud
+from app.db.models import User
+from app.utils.system import readable_size
+
+from . import utils
+
+app = typer.Typer(no_args_is_help=True)
+
+
+@app.command(name="list")
+def list_users(
+ offset: Optional[int] = typer.Option(None, *utils.FLAGS["offset"]),
+ limit: Optional[int] = typer.Option(None, *utils.FLAGS["limit"]),
+ username: Optional[List[str]] = typer.Option(None, *utils.FLAGS["username"], help="Search by username(s)"),
+ status: Optional[crud.UserStatus] = typer.Option(None, *utils.FLAGS["status"]),
+ admin: Optional[str] = typer.Option(None, "--admin", "--owner", help="Search by owner admin's username")
+):
+ """
+ Displays a table of users
+
+ NOTE: Sorting is not currently available.
+ """
+ with GetDB() as db:
+ users: list[User] = crud.get_users(
+ db=db, offset=offset, limit=limit,
+ usernames=username, status=status,
+ admin=crud.get_admin(db, admin) if admin else None
+ )
+
+ utils.print_table(
+ table=Table(
+ "ID", "Username", "Status", "Used traffic",
+ "Data limit", "Reset strategy", "Expires at", "Owner",
+ ),
+ rows=[
+ (
+ str(user.id),
+ user.username,
+ user.status.value,
+ readable_size(user.used_traffic),
+ readable_size(user.data_limit) if user.data_limit else "Unlimited",
+ user.data_limit_reset_strategy.value,
+ utils.readable_datetime(user.expire, include_time=False),
+ user.admin.username if user.admin else ''
+ )
+ for user in users
+ ]
+ )
+
+
+@app.command(name="set-owner")
+def set_owner(
+ username: str = typer.Option(None, *utils.FLAGS["username"], prompt=True),
+ admin: str = typer.Option(None, "--admin", "--owner", prompt=True, help="Admin's username"),
+ yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations")
+):
+ """
+ Transfers user's ownership
+
+ NOTE: This command needs additional confirmation for users who already have an owner.
+ """
+ with GetDB() as db:
+ user: User = utils.raise_if_falsy(
+ crud.get_user(db, username=username), f'User "{username}" not found.')
+
+ dbadmin = utils.raise_if_falsy(
+ crud.get_admin(db, username=admin), f'Admin "{admin}" not found.')
+
+ # Ask for confirmation if user already has an owner
+ if user.admin and not yes_to_all and not typer.confirm(
+ f'{username}\'s current owner is "{user.admin.username}".'
+ f' Are you sure about transferring its ownership to "{admin}"?'
+ ):
+ utils.error("Aborted.")
+
+ crud.set_owner(db, user, dbadmin)
+
+ utils.success(f'{username}\'s owner successfully set to "{admin}".')
diff --git a/cli/utils.py b/cli/utils.py
@@ -0,0 +1,90 @@
+import pydoc
+from datetime import datetime
+from typing import Any, Dict, Iterable, Optional, TypeVar, Union
+
+import typer
+from rich.console import Console
+from rich.table import Table
+
+from app.db import crud
+from app.db.models import User
+
+T = TypeVar("T")
+
+rich_console = Console()
+PASSWORD_ENVIRON_NAME = "MARZBAN_ADMIN_PASSWORD"
+
+FLAGS: Dict[str, tuple] = {
+ "username": ("--username", "-u"),
+ "limit": ("--limit", "-l"),
+ "offset": ("--offset", "-o"),
+ "yes_to_all": ("--yes", "-y"),
+ "is_sudo": ("--sudo/--no-sudo",),
+ "format": ("--format", "-f"),
+ "output_file": ("--output", "-o"),
+ "status": ("--status",),
+}
+
+
+def success(text: str, auto_exit: bool = True):
+ typer.echo(typer.style(text, fg=typer.colors.GREEN))
+ if auto_exit:
+ raise typer.Exit(0)
+
+
+def error(text: str, auto_exit: bool = True):
+ typer.echo(typer.style(text, fg=typer.colors.RED), err=True)
+ if auto_exit:
+ raise typer.Exit(-1)
+
+
+def paginate(text: str):
+ pydoc.pager(text)
+
+
+def get_user(db, username: str) -> User:
+ user: Union[User, None] = crud.get_user(db=db, username=username)
+ if not user:
+ error(f'User "{username}" not found!')
+
+ return user
+
+
+def print_table(
+ table: Table,
+ rows: Iterable[Iterable[Any]],
+ console: Optional[Console] = None
+):
+ for row in rows:
+ table.add_row(*row)
+
+ (console or rich_console).print(table)
+
+
+def readable_datetime(
+ date_time: Union[datetime, int, None],
+ include_date: bool = True,
+ include_time: bool = True
+):
+ def get_datetime_format():
+ dt_format = ""
+ if include_date:
+ dt_format += "%d %B %Y"
+ if include_time:
+ if dt_format:
+ dt_format += ", "
+ dt_format += "%H:%M:%S"
+
+ return dt_format
+
+ if isinstance(date_time, int):
+ date_time = datetime.fromtimestamp(date_time)
+
+ return date_time.strftime(get_datetime_format()) if date_time else "-"
+
+
+def raise_if_falsy(obj: T, message: str) -> T:
+ if not obj:
+ error(text=message, auto_exit=True)
+
+ return obj
diff --git a/config.py b/config.py
@@ -0,0 +1,86 @@
+import os
+from decouple import config
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+SQLALCHEMY_DATABASE_URL = config("SQLALCHEMY_DATABASE_URL", default="sqlite:///db.sqlite3")
+
+
+UVICORN_HOST = config("UVICORN_HOST", default="0.0.0.0")
+UVICORN_PORT = config("UVICORN_PORT", cast=int, default=8000)
+UVICORN_UDS = config("UVICORN_UDS", default=None)
+UVICORN_SSL_CERTFILE = config("UVICORN_SSL_CERTFILE", default=None)
+UVICORN_SSL_KEYFILE = config("UVICORN_SSL_KEYFILE", default=None)
+
+
+DEBUG = config("DEBUG", default=False, cast=bool)
+DOCS = config("DOCS", default=False, cast=bool)
+
+VITE_BASE_API = f"http://127.0.0.1:{UVICORN_PORT}/api/" \
+ if DEBUG and config("VITE_BASE_API", default="/api/") == "/api/" \
+ else config("VITE_BASE_API", default="/api/")
+
+XRAY_JSON = config("XRAY_JSON", default="./xray_config.json")
+XRAY_FALLBACKS_INBOUND_TAG = config("XRAY_FALLBACKS_INBOUND_TAG", cast=str, default="") or config(
+ "XRAY_FALLBACK_INBOUND_TAG", cast=str, default=""
+)
+XRAY_EXECUTABLE_PATH = config("XRAY_EXECUTABLE_PATH", default="/usr/local/bin/xray")
+if not os.path.isfile(XRAY_EXECUTABLE_PATH):
+ raise FileNotFoundError("Xray path doesn't exist.")
+XRAY_ASSETS_PATH = config("XRAY_ASSETS_PATH", default="/usr/local/share/xray")
+XRAY_EXCLUDE_INBOUND_TAGS = config("XRAY_EXCLUDE_INBOUND_TAGS", default='').split()
+XRAY_SUBSCRIPTION_URL_PREFIX = config("XRAY_SUBSCRIPTION_URL_PREFIX", default="").strip("/")
+XRAY_SUBSCRIPTION_PATH = config("XRAY_SUBSCRIPTION_PATH", default="sub").strip("/")
+
+TELEGRAM_API_TOKEN = config("TELEGRAM_API_TOKEN", default="")
+TELEGRAM_ADMIN_ID = config(
+ 'TELEGRAM_ADMIN_ID',
+ default="",
+ cast=lambda v: [int(i) for i in filter(str.isdigit, (s.strip() for s in v.split(',')))]
+)
+TELEGRAM_PROXY_URL = config("TELEGRAM_PROXY_URL", default="")
+TELEGRAM_LOGGER_CHANNEL_ID = config("TELEGRAM_LOGGER_CHANNEL_ID", cast=int, default=0)
+TELEGRAM_DEFAULT_VLESS_FLOW = config("TELEGRAM_DEFAULT_VLESS_FLOW", default="")
+
+JWT_ACCESS_TOKEN_EXPIRE_MINUTES = config("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", cast=int, default=1440)
+
+CUSTOM_TEMPLATES_DIRECTORY = config("CUSTOM_TEMPLATES_DIRECTORY", default=None)
+CLASH_SUBSCRIPTION_TEMPLATE = config("CLASH_SUBSCRIPTION_TEMPLATE", default="clash/default.yml")
+SUBSCRIPTION_PAGE_TEMPLATE = config("SUBSCRIPTION_PAGE_TEMPLATE", default="subscription/index.html")
+HOME_PAGE_TEMPLATE = config("HOME_PAGE_TEMPLATE", default="home/index.html")
+SINGBOX_SUBSCRIPTION_TEMPLATE = config("SINGBOX_SUBSCRIPTION_TEMPLATE", default="singbox/default.json")
+
+
+# USERNAME: PASSWORD
+SUDOERS = {config("SUDO_USERNAME"): config("SUDO_PASSWORD")} \
+ if config("SUDO_USERNAME", default='') and config("SUDO_PASSWORD", default='') \
+ else {}
+
+
+WEBHOOK_ADDRESS = config("WEBHOOK_ADDRESS", default=None)
+WEBHOOK_SECRET = config("WEBHOOK_SECRET", default=None)
+
+# recurrent notifications
+
+# timeout between each retry of sending a notification in seconds
+RECURRENT_NOTIFICATIONS_TIMEOUT = config("RECURRENT_NOTIFICATIONS_TIMEOUT", default=180, cast=int)
+# how many times to try after ok response not recevied after sending a notifications
+NUMBER_OF_RECURRENT_NOTIFICATIONS = config("NUMBER_OF_RECURRENT_NOTIFICATIONS", default=3, cast=int)
+
+# sends a notification when the user uses this much of thier data
+NOTIFY_REACHED_USAGE_PERCENT = config("NOTIFY_REACHED_USAGE_PERCENT", default=80, cast=int)
+
+# sends a notification when there is n days left of their service
+NOTIFY_DAYS_LEFT = config("NOTIFY_DAYS_LEFT", default=3, cast=int)
+
+DISABLE_RECORDING_NODE_USAGE = config("DISABLE_RECORDING_NODE_USAGE", cast=bool, default=False)
+
+# headers: profile-update-interval, support-url, profile-title
+SUB_UPDATE_INTERVAL = config("SUB_UPDATE_INTERVAL", default="12")
+SUB_SUPPORT_URL = config("SUB_SUPPORT_URL", default="https://t.me/")
+SUB_PROFILE_TITLE = config("SUB_PROFILE_TITLE", default="Subscription")
+
+# discord webhook log
+DISCORD_WEBHOOK_URL = config("DISCORD_WEBHOOK_URL", default="")
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -0,0 +1,8 @@
+services:
+ marzban:
+ image: gozargah/marzban:latest
+ restart: always
+ env_file: .env
+ network_mode: host
+ volumes:
+ - /var/lib/marzban:/var/lib/marzban
diff --git a/install_service.sh b/install_service.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+SERVICE_NAME="marzban"
+SERVICE_DESCRIPTION="Marzban Service"
+SERVICE_DOCUMENTATION="https://github.com/gozargah/marzban"
+MAIN_PY_PATH="$PWD/main.py"
+SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
+
+# Create the service file
+cat > $SERVICE_FILE <<EOF
+[Unit]
+Description=$SERVICE_DESCRIPTION
+Documentation=$SERVICE_DOCUMENTATION
+After=network.target nss-lookup.target
+
+[Service]
+ExecStart=/usr/bin/env python3 $MAIN_PY_PATH
+Restart=on-failure
+WorkingDirectory=$PWD
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+
+echo "Service file created at: $SERVICE_FILE"
diff --git a/main.py b/main.py
@@ -0,0 +1,36 @@
+import asyncio
+
+from app import app
+from config import (
+ DEBUG,
+ UVICORN_HOST,
+ UVICORN_PORT,
+ UVICORN_UDS,
+ UVICORN_SSL_CERTFILE,
+ UVICORN_SSL_KEYFILE
+)
+from uvicorn import Config, Server
+
+import logging
+
+if __name__ == "__main__":
+ # Do NOT change workers count for now
+ # multi-workers support isn't implemented yet for APScheduler and XRay module
+ loop = asyncio.new_event_loop()
+ try:
+ cfg = Config(
+ app = app,
+ host=('0.0.0.0' if DEBUG else UVICORN_HOST),
+ port=UVICORN_PORT,
+ uds=(None if DEBUG else UVICORN_UDS),
+ ssl_certfile=UVICORN_SSL_CERTFILE,
+ ssl_keyfile=UVICORN_SSL_KEYFILE,
+ workers=1,
+ reload=DEBUG,
+ log_level=logging.DEBUG if DEBUG else logging.INFO,
+ loop=loop
+ )
+ server = Server(cfg)
+ loop.run_until_complete(server.serve())
+ except FileNotFoundError: # to prevent error on removing unix sock
+ pass
diff --git a/marzban-cli.py b/marzban-cli.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+import os # noqa
+import readline # noqa
+import sys # noqa
+
+sys.path.insert(0, os.getcwd()) # noqa
+
+import typer
+from typer._completion_shared import Shells
+
+import cli.admin
+import cli.subscription
+import cli.user
+
+app = typer.Typer(no_args_is_help=True, add_completion=False)
+app.add_typer(cli.admin.app, name="admin")
+app.add_typer(cli.subscription.app, name="subscription")
+app.add_typer(cli.user.app, name="user")
+
+
+# Hidden completion app
+app_completion = typer.Typer(no_args_is_help=True, help="Generate and install completion scripts.", hidden=True)
+app.add_typer(app_completion, name="completion")
+
+
+def get_default_shell() -> Shells:
+ shell = os.environ.get('SHELL')
+ if shell:
+ shell = shell.split('/')[-1]
+ if shell in Shells.__members__:
+ return getattr(Shells, shell)
+ return Shells.bash
+
+
+@app_completion.command(help="Show completion for the specified shell, to copy or customize it.")
+def show(ctx: typer.Context, shell: Shells = typer.Option(None,
+ help="The shell to install completion for.",
+ case_sensitive=False)) -> None:
+ if shell is None:
+ shell = get_default_shell()
+ typer.completion.show_callback(ctx, None, shell)
+
+
+@app_completion.command(help="Install completion for the specified shell.")
+def install(ctx: typer.Context, shell: Shells = typer.Option(None,
+ help="The shell to install completion for.",
+ case_sensitive=False)) -> None:
+ if shell is None:
+ shell = get_default_shell()
+ typer.completion.install_callback(ctx, None, shell)
+
+
+if __name__ == "__main__":
+ typer.completion.completion_init()
+ app(prog_name=os.environ.get('CLI_PROG_NAME'))
diff --git a/mnode/service_grpc.py b/mnode/service_grpc.py
@@ -0,0 +1,104 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: service.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import mnode.service_pb2 as service_pb2
+
+
+class XrayServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def XrayStart(self, stream: 'grpclib.server.Stream[service_pb2.XrayStartRequest, service_pb2.XrayStartResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def XrayRestart(self, stream: 'grpclib.server.Stream[service_pb2.XrayRestartRequest, service_pb2.XrayRestartResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def XrayStop(self, stream: 'grpclib.server.Stream[service_pb2.XrayStopRequest, service_pb2.XrayStopResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def XrayFetchVersion(self, stream: 'grpclib.server.Stream[service_pb2.XrayFetchVersionRequest, service_pb2.XrayFetchVersionResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def XrayLogsStream(self, stream: 'grpclib.server.Stream[service_pb2.XrayLogsRequest, service_pb2.XrayLogsResponse]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/mnode.XrayService/XrayStart': grpclib.const.Handler(
+ self.XrayStart,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ service_pb2.XrayStartRequest,
+ service_pb2.XrayStartResponse,
+ ),
+ '/mnode.XrayService/XrayRestart': grpclib.const.Handler(
+ self.XrayRestart,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ service_pb2.XrayRestartRequest,
+ service_pb2.XrayRestartResponse,
+ ),
+ '/mnode.XrayService/XrayStop': grpclib.const.Handler(
+ self.XrayStop,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ service_pb2.XrayStopRequest,
+ service_pb2.XrayStopResponse,
+ ),
+ '/mnode.XrayService/XrayFetchVersion': grpclib.const.Handler(
+ self.XrayFetchVersion,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ service_pb2.XrayFetchVersionRequest,
+ service_pb2.XrayFetchVersionResponse,
+ ),
+ '/mnode.XrayService/XrayLogsStream': grpclib.const.Handler(
+ self.XrayLogsStream,
+ grpclib.const.Cardinality.UNARY_STREAM,
+ service_pb2.XrayLogsRequest,
+ service_pb2.XrayLogsResponse,
+ ),
+ }
+
+
+class XrayServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.XrayStart = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/mnode.XrayService/XrayStart',
+ service_pb2.XrayStartRequest,
+ service_pb2.XrayStartResponse,
+ )
+ self.XrayRestart = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/mnode.XrayService/XrayRestart',
+ service_pb2.XrayRestartRequest,
+ service_pb2.XrayRestartResponse,
+ )
+ self.XrayStop = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/mnode.XrayService/XrayStop',
+ service_pb2.XrayStopRequest,
+ service_pb2.XrayStopResponse,
+ )
+ self.XrayFetchVersion = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/mnode.XrayService/XrayFetchVersion',
+ service_pb2.XrayFetchVersionRequest,
+ service_pb2.XrayFetchVersionResponse,
+ )
+ self.XrayLogsStream = grpclib.client.UnaryStreamMethod(
+ channel,
+ '/mnode.XrayService/XrayLogsStream',
+ service_pb2.XrayLogsRequest,
+ service_pb2.XrayLogsResponse,
+ )
diff --git a/mnode/service_pb2.py b/mnode/service_pb2.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: service.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rservice.proto\x12\x05mnode\"\"\n\x10XrayStartRequest\x12\x0e\n\x06\x63onfig\x18\x01 \x01(\t\"\x86\x01\n\x11XrayStartResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x1c\n\x0f\x61lready_started\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1b\n\x0e\x66\x61ilure_reason\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x12\n\x10_already_startedB\x11\n\x0f_failure_reason\"$\n\x12XrayRestartRequest\x12\x0e\n\x06\x63onfig\x18\x01 \x01(\t\"\x80\x01\n\x13XrayRestartResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x18\n\x0bnot_started\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1b\n\x0e\x66\x61ilure_reason\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x0e\n\x0c_not_startedB\x11\n\x0f_failure_reason\"\x11\n\x0fXrayStopRequest\"\x12\n\x10XrayStopResponse\"\x19\n\x17XrayFetchVersionRequest\"+\n\x18XrayFetchVersionResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x11\n\x0fXrayLogsRequest\" \n\x10XrayLogsResponse\x12\x0c\n\x04line\x18\x01 \x01(\t2\xea\x02\n\x0bXrayService\x12>\n\tXrayStart\x12\x17.mnode.XrayStartRequest\x1a\x18.mnode.XrayStartResponse\x12\x44\n\x0bXrayRestart\x12\x19.mnode.XrayRestartRequest\x1a\x1a.mnode.XrayRestartResponse\x12;\n\x08XrayStop\x12\x16.mnode.XrayStopRequest\x1a\x17.mnode.XrayStopResponse\x12S\n\x10XrayFetchVersion\x12\x1e.mnode.XrayFetchVersionRequest\x1a\x1f.mnode.XrayFetchVersionResponse\x12\x43\n\x0eXrayLogsStream\x12\x16.mnode.XrayLogsRequest\x1a\x17.mnode.XrayLogsResponse0\x01\x62\x06proto3')
+
+
+
+_XRAYSTARTREQUEST = DESCRIPTOR.message_types_by_name['XrayStartRequest']
+_XRAYSTARTRESPONSE = DESCRIPTOR.message_types_by_name['XrayStartResponse']
+_XRAYRESTARTREQUEST = DESCRIPTOR.message_types_by_name['XrayRestartRequest']
+_XRAYRESTARTRESPONSE = DESCRIPTOR.message_types_by_name['XrayRestartResponse']
+_XRAYSTOPREQUEST = DESCRIPTOR.message_types_by_name['XrayStopRequest']
+_XRAYSTOPRESPONSE = DESCRIPTOR.message_types_by_name['XrayStopResponse']
+_XRAYFETCHVERSIONREQUEST = DESCRIPTOR.message_types_by_name['XrayFetchVersionRequest']
+_XRAYFETCHVERSIONRESPONSE = DESCRIPTOR.message_types_by_name['XrayFetchVersionResponse']
+_XRAYLOGSREQUEST = DESCRIPTOR.message_types_by_name['XrayLogsRequest']
+_XRAYLOGSRESPONSE = DESCRIPTOR.message_types_by_name['XrayLogsResponse']
+XrayStartRequest = _reflection.GeneratedProtocolMessageType('XrayStartRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYSTARTREQUEST,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayStartRequest)
+ })
+_sym_db.RegisterMessage(XrayStartRequest)
+
+XrayStartResponse = _reflection.GeneratedProtocolMessageType('XrayStartResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYSTARTRESPONSE,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayStartResponse)
+ })
+_sym_db.RegisterMessage(XrayStartResponse)
+
+XrayRestartRequest = _reflection.GeneratedProtocolMessageType('XrayRestartRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYRESTARTREQUEST,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayRestartRequest)
+ })
+_sym_db.RegisterMessage(XrayRestartRequest)
+
+XrayRestartResponse = _reflection.GeneratedProtocolMessageType('XrayRestartResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYRESTARTRESPONSE,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayRestartResponse)
+ })
+_sym_db.RegisterMessage(XrayRestartResponse)
+
+XrayStopRequest = _reflection.GeneratedProtocolMessageType('XrayStopRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYSTOPREQUEST,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayStopRequest)
+ })
+_sym_db.RegisterMessage(XrayStopRequest)
+
+XrayStopResponse = _reflection.GeneratedProtocolMessageType('XrayStopResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYSTOPRESPONSE,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayStopResponse)
+ })
+_sym_db.RegisterMessage(XrayStopResponse)
+
+XrayFetchVersionRequest = _reflection.GeneratedProtocolMessageType('XrayFetchVersionRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYFETCHVERSIONREQUEST,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayFetchVersionRequest)
+ })
+_sym_db.RegisterMessage(XrayFetchVersionRequest)
+
+XrayFetchVersionResponse = _reflection.GeneratedProtocolMessageType('XrayFetchVersionResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYFETCHVERSIONRESPONSE,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayFetchVersionResponse)
+ })
+_sym_db.RegisterMessage(XrayFetchVersionResponse)
+
+XrayLogsRequest = _reflection.GeneratedProtocolMessageType('XrayLogsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYLOGSREQUEST,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayLogsRequest)
+ })
+_sym_db.RegisterMessage(XrayLogsRequest)
+
+XrayLogsResponse = _reflection.GeneratedProtocolMessageType('XrayLogsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _XRAYLOGSRESPONSE,
+ '__module__' : 'service_pb2'
+ # @@protoc_insertion_point(class_scope:mnode.XrayLogsResponse)
+ })
+_sym_db.RegisterMessage(XrayLogsResponse)
+
+_XRAYSERVICE = DESCRIPTOR.services_by_name['XrayService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ _XRAYSTARTREQUEST._serialized_start=24
+ _XRAYSTARTREQUEST._serialized_end=58
+ _XRAYSTARTRESPONSE._serialized_start=61
+ _XRAYSTARTRESPONSE._serialized_end=195
+ _XRAYRESTARTREQUEST._serialized_start=197
+ _XRAYRESTARTREQUEST._serialized_end=233
+ _XRAYRESTARTRESPONSE._serialized_start=236
+ _XRAYRESTARTRESPONSE._serialized_end=364
+ _XRAYSTOPREQUEST._serialized_start=366
+ _XRAYSTOPREQUEST._serialized_end=383
+ _XRAYSTOPRESPONSE._serialized_start=385
+ _XRAYSTOPRESPONSE._serialized_end=403
+ _XRAYFETCHVERSIONREQUEST._serialized_start=405
+ _XRAYFETCHVERSIONREQUEST._serialized_end=430
+ _XRAYFETCHVERSIONRESPONSE._serialized_start=432
+ _XRAYFETCHVERSIONRESPONSE._serialized_end=475
+ _XRAYLOGSREQUEST._serialized_start=477
+ _XRAYLOGSREQUEST._serialized_end=494
+ _XRAYLOGSRESPONSE._serialized_start=496
+ _XRAYLOGSRESPONSE._serialized_end=528
+ _XRAYSERVICE._serialized_start=531
+ _XRAYSERVICE._serialized_end=893
+# @@protoc_insertion_point(module_scope)
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,36 @@
+alembic==1.13.1
+anyio==4.2.0
+APScheduler==3.10.4
+commentjson==0.9.0
+cryptography==41.0.7
+fastapi==0.109.0
+fastapi_responses==0.2.1
+grpclib==0.4.7
+jdatetime==4.1.1
+Jinja2==3.1.3
+passlib==1.7.4
+protobuf==4.25.2
+psutil==5.9.7
+pydantic==2.5.3
+PyMySQL==1.1.0
+pyOpenSSL==23.3.0
+pyOpenSSL==23.3.0
+pyTelegramBotAPI==4.15.2
+pyTelegramBotAPI==4.15.2
+python-decouple==3.8
+python-dotenv==1.0.0
+python_dateutil==2.8.2
+python_jose==3.3.0
+pytz==2023.3.post1
+qrcode==7.4.2
+Requests==2.31.0
+rich==13.7.0
+SQLAlchemy==2.0.25
+starlette==0.35.1
+typer==0.9.0
+uvicorn==0.25.0
+websockets==12.0
+PyYAML==6.0.1
+aiohttp==3.9.1
+python-multipart==0.0.6
+xxhash==3.4.1
diff --git a/xray_api/__init__.py b/xray_api/__init__.py
@@ -0,0 +1,17 @@
+from . import exceptions
+from . import exceptions as exc
+from . import types
+from .proxyman import Proxyman
+from .stats import Stats
+
+
+class XRay(Proxyman, Stats):
+ pass
+
+
+__all__ = [
+ "XRay",
+ "exceptions",
+ "exc",
+ "types"
+]
diff --git a/xray_api/base.py b/xray_api/base.py
@@ -0,0 +1,18 @@
+import grpclib
+# import asyncio
+# import functools
+import ssl
+
+class XRayBase(object):
+ def __init__(self, address: str, port: int, ssl_cert: str = None, ssl_target_name: str = None):
+ self.ssl_context = None
+ if ssl_cert:
+ self.ssl_context = ssl.create_default_context(cadata=ssl_cert)
+ self.ssl_context.check_hostname = False
+ self._channel = None
+ self.address = address
+ self.port = port
+
+ def create_channel(self):
+ if not self._channel:
+ self._channel = grpclib.client.Channel(self.address, self.port, ssl=self.ssl_context)
diff --git a/xray_api/exceptions.py b/xray_api/exceptions.py
@@ -0,0 +1,60 @@
+import re
+
+import grpclib
+
+
+class XrayError(Exception):
+ REGEXP = ...
+
+ def __init__(self, details):
+ self.details = details
+
+
+class EmailExistsError(XrayError):
+ REGEXP = re.compile(r"User (.*) already exists.")
+
+ def __init__(self, details, email):
+ self.email = email
+ super().__init__(details)
+
+
+class EmailNotFoundError(XrayError):
+ REGEXP = re.compile(r"User (.*) not found.")
+
+ def __init__(self, details, email):
+ self.email = email
+ super().__init__(details)
+
+
+class TagNotFoundError(XrayError):
+ REGEXP = re.compile(r"handler not found: (.*)")
+
+ def __init__(self, details, tag):
+ self.tag = tag
+ super().__init__(details)
+
+
+class ConnectionError(XrayError):
+ REGEXP = re.compile(r"Failed to connect to remote host|Socket closed|Broken pipe")
+
+ def __init__(self, details, tag):
+ self.tag = tag
+ super().__init__(details)
+
+
+class UnknownError(XrayError):
+ def __init__(self, details=''):
+ super().__init__(details)
+
+
+class RelatedError(XrayError):
+ def __new__(cls, error: grpclib.exceptions.GRPCError):
+ details = error.message
+ for e in (EmailExistsError, EmailNotFoundError, TagNotFoundError, ConnectionError):
+ args = e.REGEXP.findall(details)
+ if not args:
+ continue
+
+ return e(details, *args)
+
+ return UnknownError(details)
diff --git a/xray_api/proto/app/commander/config_grpc.py b/xray_api/proto/app/commander/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/commander/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/commander/config_pb2.py b/xray_api/proto/app/commander/config_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/commander/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x61pp/commander/config.proto\x12\x12xray.app.commander\x1a!common/serial/typed_message.proto\"H\n\x06\x43onfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x31\n\x07service\x18\x02 \x03(\x0b\x32 .xray.common.serial.TypedMessage\"\x12\n\x10ReflectionConfigBX\n\x16\x63om.xray.app.commanderP\x01Z\'github.com/xtls/xray-core/app/commander\xaa\x02\x12Xray.App.Commanderb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_REFLECTIONCONFIG = DESCRIPTOR.message_types_by_name['ReflectionConfig']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.commander.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.commander.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+ReflectionConfig = _reflection.GeneratedProtocolMessageType('ReflectionConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _REFLECTIONCONFIG,
+ '__module__' : 'app.commander.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.commander.ReflectionConfig)
+ })
+_sym_db.RegisterMessage(ReflectionConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\026com.xray.app.commanderP\001Z\'github.com/xtls/xray-core/app/commander\252\002\022Xray.App.Commander'
+ _CONFIG._serialized_start=85
+ _CONFIG._serialized_end=157
+ _REFLECTIONCONFIG._serialized_start=159
+ _REFLECTIONCONFIG._serialized_end=177
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/dispatcher/config_grpc.py b/xray_api/proto/app/dispatcher/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/dispatcher/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/dispatcher/config_pb2.py b/xray_api/proto/app/dispatcher/config_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/dispatcher/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/dispatcher/config.proto\x12\x13xray.app.dispatcher\"\x15\n\rSessionConfigJ\x04\x08\x01\x10\x02\">\n\x06\x43onfig\x12\x34\n\x08settings\x18\x01 \x01(\x0b\x32\".xray.app.dispatcher.SessionConfigB[\n\x17\x63om.xray.app.dispatcherP\x01Z(github.com/xtls/xray-core/app/dispatcher\xaa\x02\x13Xray.App.Dispatcherb\x06proto3')
+
+
+
+_SESSIONCONFIG = DESCRIPTOR.message_types_by_name['SessionConfig']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+SessionConfig = _reflection.GeneratedProtocolMessageType('SessionConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SESSIONCONFIG,
+ '__module__' : 'app.dispatcher.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dispatcher.SessionConfig)
+ })
+_sym_db.RegisterMessage(SessionConfig)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.dispatcher.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dispatcher.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\027com.xray.app.dispatcherP\001Z(github.com/xtls/xray-core/app/dispatcher\252\002\023Xray.App.Dispatcher'
+ _SESSIONCONFIG._serialized_start=52
+ _SESSIONCONFIG._serialized_end=73
+ _CONFIG._serialized_start=75
+ _CONFIG._serialized_end=137
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/dns/config_grpc.py b/xray_api/proto/app/dns/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/dns/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/dns/config_pb2.py b/xray_api/proto/app/dns/config_pb2.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/dns/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+from xray_api.proto.common.net import destination_pb2 as common_dot_net_dot_destination__pb2
+from xray_api.proto.app.router import config_pb2 as app_dot_router_dot_config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x61pp/dns/config.proto\x12\x0cxray.app.dns\x1a\x18\x63ommon/net/address.proto\x1a\x1c\x63ommon/net/destination.proto\x1a\x17\x61pp/router/config.proto\"\xbf\x03\n\nNameServer\x12*\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x19.xray.common.net.Endpoint\x12\x11\n\tclient_ip\x18\x05 \x01(\x0c\x12\x14\n\x0cskipFallback\x18\x06 \x01(\x08\x12\x43\n\x12prioritized_domain\x18\x02 \x03(\x0b\x32\'.xray.app.dns.NameServer.PriorityDomain\x12%\n\x05geoip\x18\x03 \x03(\x0b\x32\x16.xray.app.router.GeoIP\x12=\n\x0eoriginal_rules\x18\x04 \x03(\x0b\x32%.xray.app.dns.NameServer.OriginalRule\x12\x33\n\x0equery_strategy\x18\x07 \x01(\x0e\x32\x1b.xray.app.dns.QueryStrategy\x1aP\n\x0ePriorityDomain\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .xray.app.dns.DomainMatchingType\x12\x0e\n\x06\x64omain\x18\x02 \x01(\t\x1a*\n\x0cOriginalRule\x12\x0c\n\x04rule\x18\x01 \x01(\t\x12\x0c\n\x04size\x18\x02 \x01(\r\"\xbf\x04\n\x06\x43onfig\x12\x32\n\x0bNameServers\x18\x01 \x03(\x0b\x32\x19.xray.common.net.EndpointB\x02\x18\x01\x12-\n\x0bname_server\x18\x05 \x03(\x0b\x32\x18.xray.app.dns.NameServer\x12\x32\n\x05Hosts\x18\x02 \x03(\x0b\x32\x1f.xray.app.dns.Config.HostsEntryB\x02\x18\x01\x12\x11\n\tclient_ip\x18\x03 \x01(\x0c\x12\x36\n\x0cstatic_hosts\x18\x04 \x03(\x0b\x32 .xray.app.dns.Config.HostMapping\x12\x0b\n\x03tag\x18\x06 \x01(\t\x12\x14\n\x0c\x64isableCache\x18\x08 \x01(\x08\x12\x33\n\x0equery_strategy\x18\t \x01(\x0e\x32\x1b.xray.app.dns.QueryStrategy\x12\x17\n\x0f\x64isableFallback\x18\n \x01(\x08\x12\x1e\n\x16\x64isableFallbackIfMatch\x18\x0b \x01(\x08\x1aI\n\nHostsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain:\x02\x38\x01\x1aq\n\x0bHostMapping\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .xray.app.dns.DomainMatchingType\x12\x0e\n\x06\x64omain\x18\x02 \x01(\t\x12\n\n\x02ip\x18\x03 \x03(\x0c\x12\x16\n\x0eproxied_domain\x18\x04 \x01(\tJ\x04\x08\x07\x10\x08*E\n\x12\x44omainMatchingType\x12\x08\n\x04\x46ull\x10\x00\x12\r\n\tSubdomain\x10\x01\x12\x0b\n\x07Keyword\x10\x02\x12\t\n\x05Regex\x10\x03*5\n\rQueryStrategy\x12\n\n\x06USE_IP\x10\x00\x12\x0b\n\x07USE_IP4\x10\x01\x12\x0b\n\x07USE_IP6\x10\x02\x42\x46\n\x10\x63om.xray.app.dnsP\x01Z!github.com/xtls/xray-core/app/dns\xaa\x02\x0cXray.App.Dnsb\x06proto3')
+
+_DOMAINMATCHINGTYPE = DESCRIPTOR.enum_types_by_name['DomainMatchingType']
+DomainMatchingType = enum_type_wrapper.EnumTypeWrapper(_DOMAINMATCHINGTYPE)
+_QUERYSTRATEGY = DESCRIPTOR.enum_types_by_name['QueryStrategy']
+QueryStrategy = enum_type_wrapper.EnumTypeWrapper(_QUERYSTRATEGY)
+Full = 0
+Subdomain = 1
+Keyword = 2
+Regex = 3
+USE_IP = 0
+USE_IP4 = 1
+USE_IP6 = 2
+
+
+_NAMESERVER = DESCRIPTOR.message_types_by_name['NameServer']
+_NAMESERVER_PRIORITYDOMAIN = _NAMESERVER.nested_types_by_name['PriorityDomain']
+_NAMESERVER_ORIGINALRULE = _NAMESERVER.nested_types_by_name['OriginalRule']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONFIG_HOSTSENTRY = _CONFIG.nested_types_by_name['HostsEntry']
+_CONFIG_HOSTMAPPING = _CONFIG.nested_types_by_name['HostMapping']
+NameServer = _reflection.GeneratedProtocolMessageType('NameServer', (_message.Message,), {
+
+ 'PriorityDomain' : _reflection.GeneratedProtocolMessageType('PriorityDomain', (_message.Message,), {
+ 'DESCRIPTOR' : _NAMESERVER_PRIORITYDOMAIN,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.NameServer.PriorityDomain)
+ })
+ ,
+
+ 'OriginalRule' : _reflection.GeneratedProtocolMessageType('OriginalRule', (_message.Message,), {
+ 'DESCRIPTOR' : _NAMESERVER_ORIGINALRULE,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.NameServer.OriginalRule)
+ })
+ ,
+ 'DESCRIPTOR' : _NAMESERVER,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.NameServer)
+ })
+_sym_db.RegisterMessage(NameServer)
+_sym_db.RegisterMessage(NameServer.PriorityDomain)
+_sym_db.RegisterMessage(NameServer.OriginalRule)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+
+ 'HostsEntry' : _reflection.GeneratedProtocolMessageType('HostsEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG_HOSTSENTRY,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.Config.HostsEntry)
+ })
+ ,
+
+ 'HostMapping' : _reflection.GeneratedProtocolMessageType('HostMapping', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG_HOSTMAPPING,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.Config.HostMapping)
+ })
+ ,
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.Config)
+ })
+_sym_db.RegisterMessage(Config)
+_sym_db.RegisterMessage(Config.HostsEntry)
+_sym_db.RegisterMessage(Config.HostMapping)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\020com.xray.app.dnsP\001Z!github.com/xtls/xray-core/app/dns\252\002\014Xray.App.Dns'
+ _CONFIG_HOSTSENTRY._options = None
+ _CONFIG_HOSTSENTRY._serialized_options = b'8\001'
+ _CONFIG.fields_by_name['NameServers']._options = None
+ _CONFIG.fields_by_name['NameServers']._serialized_options = b'\030\001'
+ _CONFIG.fields_by_name['Hosts']._options = None
+ _CONFIG.fields_by_name['Hosts']._serialized_options = b'\030\001'
+ _DOMAINMATCHINGTYPE._serialized_start=1147
+ _DOMAINMATCHINGTYPE._serialized_end=1216
+ _QUERYSTRATEGY._serialized_start=1218
+ _QUERYSTRATEGY._serialized_end=1271
+ _NAMESERVER._serialized_start=120
+ _NAMESERVER._serialized_end=567
+ _NAMESERVER_PRIORITYDOMAIN._serialized_start=443
+ _NAMESERVER_PRIORITYDOMAIN._serialized_end=523
+ _NAMESERVER_ORIGINALRULE._serialized_start=525
+ _NAMESERVER_ORIGINALRULE._serialized_end=567
+ _CONFIG._serialized_start=570
+ _CONFIG._serialized_end=1145
+ _CONFIG_HOSTSENTRY._serialized_start=951
+ _CONFIG_HOSTSENTRY._serialized_end=1024
+ _CONFIG_HOSTMAPPING._serialized_start=1026
+ _CONFIG_HOSTMAPPING._serialized_end=1139
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/dns/fakedns/fakedns_grpc.py b/xray_api/proto/app/dns/fakedns/fakedns_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/dns/fakedns/fakedns.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/dns/fakedns/fakedns_pb2.py b/xray_api/proto/app/dns/fakedns/fakedns_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/dns/fakedns/fakedns.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x61pp/dns/fakedns/fakedns.proto\x12\x14xray.app.dns.fakedns\"/\n\x0b\x46\x61keDnsPool\x12\x0f\n\x07ip_pool\x18\x01 \x01(\t\x12\x0f\n\x07lruSize\x18\x02 \x01(\x03\"D\n\x10\x46\x61keDnsPoolMulti\x12\x30\n\x05pools\x18\x01 \x03(\x0b\x32!.xray.app.dns.fakedns.FakeDnsPoolB^\n\x18\x63om.xray.app.dns.fakednsP\x01Z)github.com/xtls/xray-core/app/dns/fakedns\xaa\x02\x14Xray.App.Dns.Fakednsb\x06proto3')
+
+
+
+_FAKEDNSPOOL = DESCRIPTOR.message_types_by_name['FakeDnsPool']
+_FAKEDNSPOOLMULTI = DESCRIPTOR.message_types_by_name['FakeDnsPoolMulti']
+FakeDnsPool = _reflection.GeneratedProtocolMessageType('FakeDnsPool', (_message.Message,), {
+ 'DESCRIPTOR' : _FAKEDNSPOOL,
+ '__module__' : 'app.dns.fakedns.fakedns_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.fakedns.FakeDnsPool)
+ })
+_sym_db.RegisterMessage(FakeDnsPool)
+
+FakeDnsPoolMulti = _reflection.GeneratedProtocolMessageType('FakeDnsPoolMulti', (_message.Message,), {
+ 'DESCRIPTOR' : _FAKEDNSPOOLMULTI,
+ '__module__' : 'app.dns.fakedns.fakedns_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.dns.fakedns.FakeDnsPoolMulti)
+ })
+_sym_db.RegisterMessage(FakeDnsPoolMulti)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.app.dns.fakednsP\001Z)github.com/xtls/xray-core/app/dns/fakedns\252\002\024Xray.App.Dns.Fakedns'
+ _FAKEDNSPOOL._serialized_start=55
+ _FAKEDNSPOOL._serialized_end=102
+ _FAKEDNSPOOLMULTI._serialized_start=104
+ _FAKEDNSPOOLMULTI._serialized_end=172
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/log/command/config_grpc.py b/xray_api/proto/app/log/command/config_grpc.py
@@ -0,0 +1,40 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/log/command/config.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.app.log.command.config_pb2
+
+
+class LoggerServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def RestartLogger(self, stream: 'grpclib.server.Stream[app.log.command.config_pb2.RestartLoggerRequest, app.log.command.config_pb2.RestartLoggerResponse]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.app.log.command.LoggerService/RestartLogger': grpclib.const.Handler(
+ self.RestartLogger,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.log.command.config_pb2.RestartLoggerRequest,
+ xray_api.proto.app.log.command.config_pb2.RestartLoggerResponse,
+ ),
+ }
+
+
+class LoggerServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.RestartLogger = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.log.command.LoggerService/RestartLogger',
+ xray_api.proto.app.log.command.config_pb2.RestartLoggerRequest,
+ xray_api.proto.app.log.command.config_pb2.RestartLoggerResponse,
+ )
diff --git a/xray_api/proto/app/log/command/config_pb2.py b/xray_api/proto/app/log/command/config_pb2.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/log/command/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x61pp/log/command/config.proto\x12\x14xray.app.log.command\"\x08\n\x06\x43onfig\"\x16\n\x14RestartLoggerRequest\"\x17\n\x15RestartLoggerResponse2{\n\rLoggerService\x12j\n\rRestartLogger\x12*.xray.app.log.command.RestartLoggerRequest\x1a+.xray.app.log.command.RestartLoggerResponse\"\x00\x42^\n\x18\x63om.xray.app.log.commandP\x01Z)github.com/xtls/xray-core/app/log/command\xaa\x02\x14Xray.App.Log.Commandb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_RESTARTLOGGERREQUEST = DESCRIPTOR.message_types_by_name['RestartLoggerRequest']
+_RESTARTLOGGERRESPONSE = DESCRIPTOR.message_types_by_name['RestartLoggerResponse']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.log.command.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.log.command.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+RestartLoggerRequest = _reflection.GeneratedProtocolMessageType('RestartLoggerRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _RESTARTLOGGERREQUEST,
+ '__module__' : 'app.log.command.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.log.command.RestartLoggerRequest)
+ })
+_sym_db.RegisterMessage(RestartLoggerRequest)
+
+RestartLoggerResponse = _reflection.GeneratedProtocolMessageType('RestartLoggerResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _RESTARTLOGGERRESPONSE,
+ '__module__' : 'app.log.command.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.log.command.RestartLoggerResponse)
+ })
+_sym_db.RegisterMessage(RestartLoggerResponse)
+
+_LOGGERSERVICE = DESCRIPTOR.services_by_name['LoggerService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.app.log.commandP\001Z)github.com/xtls/xray-core/app/log/command\252\002\024Xray.App.Log.Command'
+ _CONFIG._serialized_start=54
+ _CONFIG._serialized_end=62
+ _RESTARTLOGGERREQUEST._serialized_start=64
+ _RESTARTLOGGERREQUEST._serialized_end=86
+ _RESTARTLOGGERRESPONSE._serialized_start=88
+ _RESTARTLOGGERRESPONSE._serialized_end=111
+ _LOGGERSERVICE._serialized_start=113
+ _LOGGERSERVICE._serialized_end=236
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/log/config_grpc.py b/xray_api/proto/app/log/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/log/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/log/config_pb2.py b/xray_api/proto/app/log/config_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/log/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.log import log_pb2 as common_dot_log_dot_log__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x61pp/log/config.proto\x12\x0cxray.app.log\x1a\x14\x63ommon/log/log.proto\"\xe4\x01\n\x06\x43onfig\x12-\n\x0e\x65rror_log_type\x18\x01 \x01(\x0e\x32\x15.xray.app.log.LogType\x12\x32\n\x0f\x65rror_log_level\x18\x02 \x01(\x0e\x32\x19.xray.common.log.Severity\x12\x16\n\x0e\x65rror_log_path\x18\x03 \x01(\t\x12.\n\x0f\x61\x63\x63\x65ss_log_type\x18\x04 \x01(\x0e\x32\x15.xray.app.log.LogType\x12\x17\n\x0f\x61\x63\x63\x65ss_log_path\x18\x05 \x01(\t\x12\x16\n\x0e\x65nable_dns_log\x18\x06 \x01(\x08*5\n\x07LogType\x12\x08\n\x04None\x10\x00\x12\x0b\n\x07\x43onsole\x10\x01\x12\x08\n\x04\x46ile\x10\x02\x12\t\n\x05\x45vent\x10\x03\x42\x46\n\x10\x63om.xray.app.logP\x01Z!github.com/xtls/xray-core/app/log\xaa\x02\x0cXray.App.Logb\x06proto3')
+
+_LOGTYPE = DESCRIPTOR.enum_types_by_name['LogType']
+LogType = enum_type_wrapper.EnumTypeWrapper(_LOGTYPE)
+globals()['None'] = 0
+Console = 1
+File = 2
+Event = 3
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.log.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.log.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\020com.xray.app.logP\001Z!github.com/xtls/xray-core/app/log\252\002\014Xray.App.Log'
+ _LOGTYPE._serialized_start=291
+ _LOGTYPE._serialized_end=344
+ _CONFIG._serialized_start=61
+ _CONFIG._serialized_end=289
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/metrics/config_grpc.py b/xray_api/proto/app/metrics/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/metrics/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/metrics/config_pb2.py b/xray_api/proto/app/metrics/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/metrics/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x61pp/metrics/config.proto\x12\x10xray.app.metrics\"\x15\n\x06\x43onfig\x12\x0b\n\x03tag\x18\x01 \x01(\tBR\n\x14\x63om.xray.app.metricsP\x01Z%github.com/xtls/xray-core/app/metrics\xaa\x02\x10Xray.App.Metricsb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.metrics.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.metrics.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\024com.xray.app.metricsP\001Z%github.com/xtls/xray-core/app/metrics\252\002\020Xray.App.Metrics'
+ _CONFIG._serialized_start=46
+ _CONFIG._serialized_end=67
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/observatory/command/command_grpc.py b/xray_api/proto/app/observatory/command/command_grpc.py
@@ -0,0 +1,41 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/observatory/command/command.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.app.observatory.config_pb2
+import xray_api.proto.app.observatory.command.command_pb2
+
+
+class ObservatoryServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def GetOutboundStatus(self, stream: 'grpclib.server.Stream[app.observatory.command.command_pb2.GetOutboundStatusRequest, app.observatory.command.command_pb2.GetOutboundStatusResponse]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.core.app.observatory.command.ObservatoryService/GetOutboundStatus': grpclib.const.Handler(
+ self.GetOutboundStatus,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.observatory.command.command_pb2.GetOutboundStatusRequest,
+ xray_api.proto.app.observatory.command.command_pb2.GetOutboundStatusResponse,
+ ),
+ }
+
+
+class ObservatoryServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.GetOutboundStatus = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.core.app.observatory.command.ObservatoryService/GetOutboundStatus',
+ xray_api.proto.app.observatory.command.command_pb2.GetOutboundStatusRequest,
+ xray_api.proto.app.observatory.command.command_pb2.GetOutboundStatusResponse,
+ )
diff --git a/xray_api/proto/app/observatory/command/command_pb2.py b/xray_api/proto/app/observatory/command/command_pb2.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/observatory/command/command.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.app.observatory import config_pb2 as app_dot_observatory_dot_config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%app/observatory/command/command.proto\x12!xray.core.app.observatory.command\x1a\x1c\x61pp/observatory/config.proto\"\x1a\n\x18GetOutboundStatusRequest\"Y\n\x19GetOutboundStatusResponse\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.xray.core.app.observatory.ObservationResult\"\x08\n\x06\x43onfig2\xa7\x01\n\x12ObservatoryService\x12\x90\x01\n\x11GetOutboundStatus\x12;.xray.core.app.observatory.command.GetOutboundStatusRequest\x1a<.xray.core.app.observatory.command.GetOutboundStatusResponse\"\x00\x42\x80\x01\n%com.xray.core.app.observatory.commandP\x01Z1github.com/xtls/xray-core/app/observatory/command\xaa\x02!Xray.Core.App.Observatory.Commandb\x06proto3')
+
+
+
+_GETOUTBOUNDSTATUSREQUEST = DESCRIPTOR.message_types_by_name['GetOutboundStatusRequest']
+_GETOUTBOUNDSTATUSRESPONSE = DESCRIPTOR.message_types_by_name['GetOutboundStatusResponse']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+GetOutboundStatusRequest = _reflection.GeneratedProtocolMessageType('GetOutboundStatusRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETOUTBOUNDSTATUSREQUEST,
+ '__module__' : 'app.observatory.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.command.GetOutboundStatusRequest)
+ })
+_sym_db.RegisterMessage(GetOutboundStatusRequest)
+
+GetOutboundStatusResponse = _reflection.GeneratedProtocolMessageType('GetOutboundStatusResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _GETOUTBOUNDSTATUSRESPONSE,
+ '__module__' : 'app.observatory.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.command.GetOutboundStatusResponse)
+ })
+_sym_db.RegisterMessage(GetOutboundStatusResponse)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.observatory.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.command.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+_OBSERVATORYSERVICE = DESCRIPTOR.services_by_name['ObservatoryService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n%com.xray.core.app.observatory.commandP\001Z1github.com/xtls/xray-core/app/observatory/command\252\002!Xray.Core.App.Observatory.Command'
+ _GETOUTBOUNDSTATUSREQUEST._serialized_start=106
+ _GETOUTBOUNDSTATUSREQUEST._serialized_end=132
+ _GETOUTBOUNDSTATUSRESPONSE._serialized_start=134
+ _GETOUTBOUNDSTATUSRESPONSE._serialized_end=223
+ _CONFIG._serialized_start=225
+ _CONFIG._serialized_end=233
+ _OBSERVATORYSERVICE._serialized_start=236
+ _OBSERVATORYSERVICE._serialized_end=403
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/observatory/config_grpc.py b/xray_api/proto/app/observatory/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/observatory/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/observatory/config_pb2.py b/xray_api/proto/app/observatory/config_pb2.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/observatory/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x61pp/observatory/config.proto\x12\x19xray.core.app.observatory\"N\n\x11ObservationResult\x12\x39\n\x06status\x18\x01 \x03(\x0b\x32).xray.core.app.observatory.OutboundStatus\"\x8e\x01\n\x0eOutboundStatus\x12\r\n\x05\x61live\x18\x01 \x01(\x08\x12\r\n\x05\x64\x65lay\x18\x02 \x01(\x03\x12\x19\n\x11last_error_reason\x18\x03 \x01(\t\x12\x14\n\x0coutbound_tag\x18\x04 \x01(\t\x12\x16\n\x0elast_seen_time\x18\x05 \x01(\x03\x12\x15\n\rlast_try_time\x18\x06 \x01(\x03\"F\n\x0bProbeResult\x12\r\n\x05\x61live\x18\x01 \x01(\x08\x12\r\n\x05\x64\x65lay\x18\x02 \x01(\x03\x12\x19\n\x11last_error_reason\x18\x03 \x01(\t\"#\n\tIntensity\x12\x16\n\x0eprobe_interval\x18\x01 \x01(\r\"i\n\x06\x43onfig\x12\x18\n\x10subject_selector\x18\x02 \x03(\t\x12\x11\n\tprobe_url\x18\x03 \x01(\t\x12\x16\n\x0eprobe_interval\x18\x04 \x01(\x03\x12\x1a\n\x12\x65nable_concurrency\x18\x05 \x01(\x08\x42^\n\x18\x63om.xray.app.observatoryP\x01Z)github.com/xtls/xray-core/app/observatory\xaa\x02\x14Xray.App.Observatoryb\x06proto3')
+
+
+
+_OBSERVATIONRESULT = DESCRIPTOR.message_types_by_name['ObservationResult']
+_OUTBOUNDSTATUS = DESCRIPTOR.message_types_by_name['OutboundStatus']
+_PROBERESULT = DESCRIPTOR.message_types_by_name['ProbeResult']
+_INTENSITY = DESCRIPTOR.message_types_by_name['Intensity']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+ObservationResult = _reflection.GeneratedProtocolMessageType('ObservationResult', (_message.Message,), {
+ 'DESCRIPTOR' : _OBSERVATIONRESULT,
+ '__module__' : 'app.observatory.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.ObservationResult)
+ })
+_sym_db.RegisterMessage(ObservationResult)
+
+OutboundStatus = _reflection.GeneratedProtocolMessageType('OutboundStatus', (_message.Message,), {
+ 'DESCRIPTOR' : _OUTBOUNDSTATUS,
+ '__module__' : 'app.observatory.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.OutboundStatus)
+ })
+_sym_db.RegisterMessage(OutboundStatus)
+
+ProbeResult = _reflection.GeneratedProtocolMessageType('ProbeResult', (_message.Message,), {
+ 'DESCRIPTOR' : _PROBERESULT,
+ '__module__' : 'app.observatory.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.ProbeResult)
+ })
+_sym_db.RegisterMessage(ProbeResult)
+
+Intensity = _reflection.GeneratedProtocolMessageType('Intensity', (_message.Message,), {
+ 'DESCRIPTOR' : _INTENSITY,
+ '__module__' : 'app.observatory.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.Intensity)
+ })
+_sym_db.RegisterMessage(Intensity)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.observatory.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.xray_api.proto.app.observatory.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.app.observatoryP\001Z)github.com/xtls/xray-core/app/observatory\252\002\024Xray.App.Observatory'
+ _OBSERVATIONRESULT._serialized_start=59
+ _OBSERVATIONRESULT._serialized_end=137
+ _OUTBOUNDSTATUS._serialized_start=140
+ _OUTBOUNDSTATUS._serialized_end=282
+ _PROBERESULT._serialized_start=284
+ _PROBERESULT._serialized_end=354
+ _INTENSITY._serialized_start=356
+ _INTENSITY._serialized_end=391
+ _CONFIG._serialized_start=393
+ _CONFIG._serialized_end=498
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/policy/config_grpc.py b/xray_api/proto/app/policy/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/policy/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/policy/config_pb2.py b/xray_api/proto/app/policy/config_pb2.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/policy/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x61pp/policy/config.proto\x12\x0fxray.app.policy\"\x17\n\x06Second\x12\r\n\x05value\x18\x01 \x01(\r\"\xb3\x03\n\x06Policy\x12\x30\n\x07timeout\x18\x01 \x01(\x0b\x32\x1f.xray.app.policy.Policy.Timeout\x12,\n\x05stats\x18\x02 \x01(\x0b\x32\x1d.xray.app.policy.Policy.Stats\x12.\n\x06\x62uffer\x18\x03 \x01(\x0b\x32\x1e.xray.app.policy.Policy.Buffer\x1a\xc5\x01\n\x07Timeout\x12*\n\thandshake\x18\x01 \x01(\x0b\x32\x17.xray.app.policy.Second\x12\x30\n\x0f\x63onnection_idle\x18\x02 \x01(\x0b\x32\x17.xray.app.policy.Second\x12,\n\x0buplink_only\x18\x03 \x01(\x0b\x32\x17.xray.app.policy.Second\x12.\n\rdownlink_only\x18\x04 \x01(\x0b\x32\x17.xray.app.policy.Second\x1a\x33\n\x05Stats\x12\x13\n\x0buser_uplink\x18\x01 \x01(\x08\x12\x15\n\ruser_downlink\x18\x02 \x01(\x08\x1a\x1c\n\x06\x42uffer\x12\x12\n\nconnection\x18\x01 \x01(\x05\"\xb1\x01\n\x0cSystemPolicy\x12\x32\n\x05stats\x18\x01 \x01(\x0b\x32#.xray.app.policy.SystemPolicy.Stats\x1am\n\x05Stats\x12\x16\n\x0einbound_uplink\x18\x01 \x01(\x08\x12\x18\n\x10inbound_downlink\x18\x02 \x01(\x08\x12\x17\n\x0foutbound_uplink\x18\x03 \x01(\x08\x12\x19\n\x11outbound_downlink\x18\x04 \x01(\x08\"\xb1\x01\n\x06\x43onfig\x12\x31\n\x05level\x18\x01 \x03(\x0b\x32\".xray.app.policy.Config.LevelEntry\x12-\n\x06system\x18\x02 \x01(\x0b\x32\x1d.xray.app.policy.SystemPolicy\x1a\x45\n\nLevelEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.xray.app.policy.Policy:\x02\x38\x01\x42O\n\x13\x63om.xray.app.policyP\x01Z$github.com/xtls/xray-core/app/policy\xaa\x02\x0fXray.App.Policyb\x06proto3')
+
+
+
+_SECOND = DESCRIPTOR.message_types_by_name['Second']
+_POLICY = DESCRIPTOR.message_types_by_name['Policy']
+_POLICY_TIMEOUT = _POLICY.nested_types_by_name['Timeout']
+_POLICY_STATS = _POLICY.nested_types_by_name['Stats']
+_POLICY_BUFFER = _POLICY.nested_types_by_name['Buffer']
+_SYSTEMPOLICY = DESCRIPTOR.message_types_by_name['SystemPolicy']
+_SYSTEMPOLICY_STATS = _SYSTEMPOLICY.nested_types_by_name['Stats']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONFIG_LEVELENTRY = _CONFIG.nested_types_by_name['LevelEntry']
+Second = _reflection.GeneratedProtocolMessageType('Second', (_message.Message,), {
+ 'DESCRIPTOR' : _SECOND,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Second)
+ })
+_sym_db.RegisterMessage(Second)
+
+Policy = _reflection.GeneratedProtocolMessageType('Policy', (_message.Message,), {
+
+ 'Timeout' : _reflection.GeneratedProtocolMessageType('Timeout', (_message.Message,), {
+ 'DESCRIPTOR' : _POLICY_TIMEOUT,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Policy.Timeout)
+ })
+ ,
+
+ 'Stats' : _reflection.GeneratedProtocolMessageType('Stats', (_message.Message,), {
+ 'DESCRIPTOR' : _POLICY_STATS,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Policy.Stats)
+ })
+ ,
+
+ 'Buffer' : _reflection.GeneratedProtocolMessageType('Buffer', (_message.Message,), {
+ 'DESCRIPTOR' : _POLICY_BUFFER,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Policy.Buffer)
+ })
+ ,
+ 'DESCRIPTOR' : _POLICY,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Policy)
+ })
+_sym_db.RegisterMessage(Policy)
+_sym_db.RegisterMessage(Policy.Timeout)
+_sym_db.RegisterMessage(Policy.Stats)
+_sym_db.RegisterMessage(Policy.Buffer)
+
+SystemPolicy = _reflection.GeneratedProtocolMessageType('SystemPolicy', (_message.Message,), {
+
+ 'Stats' : _reflection.GeneratedProtocolMessageType('Stats', (_message.Message,), {
+ 'DESCRIPTOR' : _SYSTEMPOLICY_STATS,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.SystemPolicy.Stats)
+ })
+ ,
+ 'DESCRIPTOR' : _SYSTEMPOLICY,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.SystemPolicy)
+ })
+_sym_db.RegisterMessage(SystemPolicy)
+_sym_db.RegisterMessage(SystemPolicy.Stats)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+
+ 'LevelEntry' : _reflection.GeneratedProtocolMessageType('LevelEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG_LEVELENTRY,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Config.LevelEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.policy.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.policy.Config)
+ })
+_sym_db.RegisterMessage(Config)
+_sym_db.RegisterMessage(Config.LevelEntry)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.app.policyP\001Z$github.com/xtls/xray-core/app/policy\252\002\017Xray.App.Policy'
+ _CONFIG_LEVELENTRY._options = None
+ _CONFIG_LEVELENTRY._serialized_options = b'8\001'
+ _SECOND._serialized_start=44
+ _SECOND._serialized_end=67
+ _POLICY._serialized_start=70
+ _POLICY._serialized_end=505
+ _POLICY_TIMEOUT._serialized_start=225
+ _POLICY_TIMEOUT._serialized_end=422
+ _POLICY_STATS._serialized_start=424
+ _POLICY_STATS._serialized_end=475
+ _POLICY_BUFFER._serialized_start=477
+ _POLICY_BUFFER._serialized_end=505
+ _SYSTEMPOLICY._serialized_start=508
+ _SYSTEMPOLICY._serialized_end=685
+ _SYSTEMPOLICY_STATS._serialized_start=576
+ _SYSTEMPOLICY_STATS._serialized_end=685
+ _CONFIG._serialized_start=688
+ _CONFIG._serialized_end=865
+ _CONFIG_LEVELENTRY._serialized_start=796
+ _CONFIG_LEVELENTRY._serialized_end=865
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/proxyman/command/command_grpc.py b/xray_api/proto/app/proxyman/command/command_grpc.py
@@ -0,0 +1,123 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/proxyman/command/command.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.common.protocol.user_pb2
+import xray_api.proto.common.serial.typed_message_pb2
+import xray_api.proto.core.config_pb2
+import xray_api.proto.app.proxyman.command.command_pb2
+
+
+class HandlerServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def AddInbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.AddInboundRequest, app.proxyman.command.command_pb2.AddInboundResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def RemoveInbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.RemoveInboundRequest, app.proxyman.command.command_pb2.RemoveInboundResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def AlterInbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.AlterInboundRequest, app.proxyman.command.command_pb2.AlterInboundResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def AddOutbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.AddOutboundRequest, app.proxyman.command.command_pb2.AddOutboundResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def RemoveOutbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.RemoveOutboundRequest, app.proxyman.command.command_pb2.RemoveOutboundResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def AlterOutbound(self, stream: 'grpclib.server.Stream[app.proxyman.command.command_pb2.AlterOutboundRequest, app.proxyman.command.command_pb2.AlterOutboundResponse]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.app.proxyman.command.HandlerService/AddInbound': grpclib.const.Handler(
+ self.AddInbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.AddInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AddInboundResponse,
+ ),
+ '/xray.app.proxyman.command.HandlerService/RemoveInbound': grpclib.const.Handler(
+ self.RemoveInbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveInboundResponse,
+ ),
+ '/xray.app.proxyman.command.HandlerService/AlterInbound': grpclib.const.Handler(
+ self.AlterInbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterInboundResponse,
+ ),
+ '/xray.app.proxyman.command.HandlerService/AddOutbound': grpclib.const.Handler(
+ self.AddOutbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.AddOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AddOutboundResponse,
+ ),
+ '/xray.app.proxyman.command.HandlerService/RemoveOutbound': grpclib.const.Handler(
+ self.RemoveOutbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveOutboundResponse,
+ ),
+ '/xray.app.proxyman.command.HandlerService/AlterOutbound': grpclib.const.Handler(
+ self.AlterOutbound,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterOutboundResponse,
+ ),
+ }
+
+
+class HandlerServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.AddInbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/AddInbound',
+ xray_api.proto.app.proxyman.command.command_pb2.AddInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AddInboundResponse,
+ )
+ self.RemoveInbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/RemoveInbound',
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveInboundResponse,
+ )
+ self.AlterInbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/AlterInbound',
+ xray_api.proto.app.proxyman.command.command_pb2.AlterInboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterInboundResponse,
+ )
+ self.AddOutbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/AddOutbound',
+ xray_api.proto.app.proxyman.command.command_pb2.AddOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AddOutboundResponse,
+ )
+ self.RemoveOutbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/RemoveOutbound',
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.RemoveOutboundResponse,
+ )
+ self.AlterOutbound = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.proxyman.command.HandlerService/AlterOutbound',
+ xray_api.proto.app.proxyman.command.command_pb2.AlterOutboundRequest,
+ xray_api.proto.app.proxyman.command.command_pb2.AlterOutboundResponse,
+ )
diff --git a/xray_api/proto/app/proxyman/command/command_pb2.py b/xray_api/proto/app/proxyman/command/command_pb2.py
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/proxyman/command/command.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+from xray_api.proto.core import config_pb2 as core_dot_config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"app/proxyman/command/command.proto\x12\x19xray.app.proxyman.command\x1a\x1a\x63ommon/protocol/user.proto\x1a!common/serial/typed_message.proto\x1a\x11\x63ore/config.proto\"<\n\x10\x41\x64\x64UserOperation\x12(\n\x04user\x18\x01 \x01(\x0b\x32\x1a.xray.common.protocol.User\"$\n\x13RemoveUserOperation\x12\r\n\x05\x65mail\x18\x01 \x01(\t\"E\n\x11\x41\x64\x64InboundRequest\x12\x30\n\x07inbound\x18\x01 \x01(\x0b\x32\x1f.xray.core.InboundHandlerConfig\"\x14\n\x12\x41\x64\x64InboundResponse\"#\n\x14RemoveInboundRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\"\x17\n\x15RemoveInboundResponse\"W\n\x13\x41lterInboundRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x33\n\toperation\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\x16\n\x14\x41lterInboundResponse\"H\n\x12\x41\x64\x64OutboundRequest\x12\x32\n\x08outbound\x18\x01 \x01(\x0b\x32 .xray.core.OutboundHandlerConfig\"\x15\n\x13\x41\x64\x64OutboundResponse\"$\n\x15RemoveOutboundRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\"\x18\n\x16RemoveOutboundResponse\"X\n\x14\x41lterOutboundRequest\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x33\n\toperation\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\x17\n\x15\x41lterOutboundResponse\"\x08\n\x06\x43onfig2\xc5\x05\n\x0eHandlerService\x12k\n\nAddInbound\x12,.xray.app.proxyman.command.AddInboundRequest\x1a-.xray.app.proxyman.command.AddInboundResponse\"\x00\x12t\n\rRemoveInbound\x12/.xray.app.proxyman.command.RemoveInboundRequest\x1a\x30.xray.app.proxyman.command.RemoveInboundResponse\"\x00\x12q\n\x0c\x41lterInbound\x12..xray.app.proxyman.command.AlterInboundRequest\x1a/.xray.app.proxyman.command.AlterInboundResponse\"\x00\x12n\n\x0b\x41\x64\x64Outbound\x12-.xray.app.proxyman.command.AddOutboundRequest\x1a..xray.app.proxyman.command.AddOutboundResponse\"\x00\x12w\n\x0eRemoveOutbound\x12\x30.xray.app.proxyman.command.RemoveOutboundRequest\x1a\x31.xray.app.proxyman.command.RemoveOutboundResponse\"\x00\x12t\n\rAlterOutbound\x12/.xray.app.proxyman.command.AlterOutboundRequest\x1a\x30.xray.app.proxyman.command.AlterOutboundResponse\"\x00\x42m\n\x1d\x63om.xray.app.proxyman.commandP\x01Z.github.com/xtls/xray-core/app/proxyman/command\xaa\x02\x19Xray.App.Proxyman.Commandb\x06proto3')
+
+
+
+_ADDUSEROPERATION = DESCRIPTOR.message_types_by_name['AddUserOperation']
+_REMOVEUSEROPERATION = DESCRIPTOR.message_types_by_name['RemoveUserOperation']
+_ADDINBOUNDREQUEST = DESCRIPTOR.message_types_by_name['AddInboundRequest']
+_ADDINBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['AddInboundResponse']
+_REMOVEINBOUNDREQUEST = DESCRIPTOR.message_types_by_name['RemoveInboundRequest']
+_REMOVEINBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['RemoveInboundResponse']
+_ALTERINBOUNDREQUEST = DESCRIPTOR.message_types_by_name['AlterInboundRequest']
+_ALTERINBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['AlterInboundResponse']
+_ADDOUTBOUNDREQUEST = DESCRIPTOR.message_types_by_name['AddOutboundRequest']
+_ADDOUTBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['AddOutboundResponse']
+_REMOVEOUTBOUNDREQUEST = DESCRIPTOR.message_types_by_name['RemoveOutboundRequest']
+_REMOVEOUTBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['RemoveOutboundResponse']
+_ALTEROUTBOUNDREQUEST = DESCRIPTOR.message_types_by_name['AlterOutboundRequest']
+_ALTEROUTBOUNDRESPONSE = DESCRIPTOR.message_types_by_name['AlterOutboundResponse']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+AddUserOperation = _reflection.GeneratedProtocolMessageType('AddUserOperation', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDUSEROPERATION,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AddUserOperation)
+ })
+_sym_db.RegisterMessage(AddUserOperation)
+
+RemoveUserOperation = _reflection.GeneratedProtocolMessageType('RemoveUserOperation', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEUSEROPERATION,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.RemoveUserOperation)
+ })
+_sym_db.RegisterMessage(RemoveUserOperation)
+
+AddInboundRequest = _reflection.GeneratedProtocolMessageType('AddInboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDINBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AddInboundRequest)
+ })
+_sym_db.RegisterMessage(AddInboundRequest)
+
+AddInboundResponse = _reflection.GeneratedProtocolMessageType('AddInboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDINBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AddInboundResponse)
+ })
+_sym_db.RegisterMessage(AddInboundResponse)
+
+RemoveInboundRequest = _reflection.GeneratedProtocolMessageType('RemoveInboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEINBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.RemoveInboundRequest)
+ })
+_sym_db.RegisterMessage(RemoveInboundRequest)
+
+RemoveInboundResponse = _reflection.GeneratedProtocolMessageType('RemoveInboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEINBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.RemoveInboundResponse)
+ })
+_sym_db.RegisterMessage(RemoveInboundResponse)
+
+AlterInboundRequest = _reflection.GeneratedProtocolMessageType('AlterInboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _ALTERINBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AlterInboundRequest)
+ })
+_sym_db.RegisterMessage(AlterInboundRequest)
+
+AlterInboundResponse = _reflection.GeneratedProtocolMessageType('AlterInboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _ALTERINBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AlterInboundResponse)
+ })
+_sym_db.RegisterMessage(AlterInboundResponse)
+
+AddOutboundRequest = _reflection.GeneratedProtocolMessageType('AddOutboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDOUTBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AddOutboundRequest)
+ })
+_sym_db.RegisterMessage(AddOutboundRequest)
+
+AddOutboundResponse = _reflection.GeneratedProtocolMessageType('AddOutboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDOUTBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AddOutboundResponse)
+ })
+_sym_db.RegisterMessage(AddOutboundResponse)
+
+RemoveOutboundRequest = _reflection.GeneratedProtocolMessageType('RemoveOutboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEOUTBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.RemoveOutboundRequest)
+ })
+_sym_db.RegisterMessage(RemoveOutboundRequest)
+
+RemoveOutboundResponse = _reflection.GeneratedProtocolMessageType('RemoveOutboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEOUTBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.RemoveOutboundResponse)
+ })
+_sym_db.RegisterMessage(RemoveOutboundResponse)
+
+AlterOutboundRequest = _reflection.GeneratedProtocolMessageType('AlterOutboundRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _ALTEROUTBOUNDREQUEST,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AlterOutboundRequest)
+ })
+_sym_db.RegisterMessage(AlterOutboundRequest)
+
+AlterOutboundResponse = _reflection.GeneratedProtocolMessageType('AlterOutboundResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _ALTEROUTBOUNDRESPONSE,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.AlterOutboundResponse)
+ })
+_sym_db.RegisterMessage(AlterOutboundResponse)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.proxyman.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.command.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+_HANDLERSERVICE = DESCRIPTOR.services_by_name['HandlerService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\035com.xray.app.proxyman.commandP\001Z.github.com/xtls/xray-core/app/proxyman/command\252\002\031Xray.App.Proxyman.Command'
+ _ADDUSEROPERATION._serialized_start=147
+ _ADDUSEROPERATION._serialized_end=207
+ _REMOVEUSEROPERATION._serialized_start=209
+ _REMOVEUSEROPERATION._serialized_end=245
+ _ADDINBOUNDREQUEST._serialized_start=247
+ _ADDINBOUNDREQUEST._serialized_end=316
+ _ADDINBOUNDRESPONSE._serialized_start=318
+ _ADDINBOUNDRESPONSE._serialized_end=338
+ _REMOVEINBOUNDREQUEST._serialized_start=340
+ _REMOVEINBOUNDREQUEST._serialized_end=375
+ _REMOVEINBOUNDRESPONSE._serialized_start=377
+ _REMOVEINBOUNDRESPONSE._serialized_end=400
+ _ALTERINBOUNDREQUEST._serialized_start=402
+ _ALTERINBOUNDREQUEST._serialized_end=489
+ _ALTERINBOUNDRESPONSE._serialized_start=491
+ _ALTERINBOUNDRESPONSE._serialized_end=513
+ _ADDOUTBOUNDREQUEST._serialized_start=515
+ _ADDOUTBOUNDREQUEST._serialized_end=587
+ _ADDOUTBOUNDRESPONSE._serialized_start=589
+ _ADDOUTBOUNDRESPONSE._serialized_end=610
+ _REMOVEOUTBOUNDREQUEST._serialized_start=612
+ _REMOVEOUTBOUNDREQUEST._serialized_end=648
+ _REMOVEOUTBOUNDRESPONSE._serialized_start=650
+ _REMOVEOUTBOUNDRESPONSE._serialized_end=674
+ _ALTEROUTBOUNDREQUEST._serialized_start=676
+ _ALTEROUTBOUNDREQUEST._serialized_end=764
+ _ALTEROUTBOUNDRESPONSE._serialized_start=766
+ _ALTEROUTBOUNDRESPONSE._serialized_end=789
+ _CONFIG._serialized_start=791
+ _CONFIG._serialized_end=799
+ _HANDLERSERVICE._serialized_start=802
+ _HANDLERSERVICE._serialized_end=1511
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/proxyman/config_grpc.py b/xray_api/proto/app/proxyman/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/proxyman/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/proxyman/config_pb2.py b/xray_api/proto/app/proxyman/config_pb2.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/proxyman/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+from xray_api.proto.common.net import port_pb2 as common_dot_net_dot_port__pb2
+from xray_api.proto.transport.internet import config_pb2 as transport_dot_internet_dot_config__pb2
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x61pp/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18\x63ommon/net/address.proto\x1a\x15\x63ommon/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\"\x0f\n\rInboundConfig\"\x84\x03\n\x12\x41llocationStrategy\x12\x38\n\x04type\x18\x01 \x01(\x0e\x32*.xray.app.proxyman.AllocationStrategy.Type\x12X\n\x0b\x63oncurrency\x18\x02 \x01(\x0b\x32\x43.xray.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency\x12P\n\x07refresh\x18\x03 \x01(\x0b\x32?.xray.app.proxyman.AllocationStrategy.AllocationStrategyRefresh\x1a.\n\x1d\x41llocationStrategyConcurrency\x12\r\n\x05value\x18\x01 \x01(\r\x1a*\n\x19\x41llocationStrategyRefresh\x12\r\n\x05value\x18\x01 \x01(\r\",\n\x04Type\x12\n\n\x06\x41lways\x10\x00\x12\n\n\x06Random\x10\x01\x12\x0c\n\x08\x45xternal\x10\x02\"\x84\x01\n\x0eSniffingConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x1c\n\x14\x64\x65stination_override\x18\x02 \x03(\t\x12\x18\n\x10\x64omains_excluded\x18\x03 \x03(\t\x12\x15\n\rmetadata_only\x18\x04 \x01(\x08\x12\x12\n\nroute_only\x18\x05 \x01(\x08\"\x99\x03\n\x0eReceiverConfig\x12,\n\tport_list\x18\x01 \x01(\x0b\x32\x19.xray.common.net.PortList\x12+\n\x06listen\x18\x02 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x42\n\x13\x61llocation_strategy\x18\x03 \x01(\x0b\x32%.xray.app.proxyman.AllocationStrategy\x12>\n\x0fstream_settings\x18\x04 \x01(\x0b\x32%.xray.transport.internet.StreamConfig\x12$\n\x1creceive_original_destination\x18\x05 \x01(\x08\x12>\n\x0f\x64omain_override\x18\x07 \x03(\x0e\x32!.xray.app.proxyman.KnownProtocolsB\x02\x18\x01\x12<\n\x11sniffing_settings\x18\x08 \x01(\x0b\x32!.xray.app.proxyman.SniffingConfigJ\x04\x08\x06\x10\x07\"\x9a\x01\n\x14InboundHandlerConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12;\n\x11receiver_settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x38\n\x0eproxy_settings\x18\x03 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\x10\n\x0eOutboundConfig\"\xf9\x01\n\x0cSenderConfig\x12(\n\x03via\x18\x01 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12>\n\x0fstream_settings\x18\x02 \x01(\x0b\x32%.xray.transport.internet.StreamConfig\x12<\n\x0eproxy_settings\x18\x03 \x01(\x0b\x32$.xray.transport.internet.ProxyConfig\x12\x41\n\x12multiplex_settings\x18\x04 \x01(\x0b\x32%.xray.app.proxyman.MultiplexingConfig\"l\n\x12MultiplexingConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x13\n\x0b\x63oncurrency\x18\x02 \x01(\x05\x12\x17\n\x0fxudpConcurrency\x18\x03 \x01(\x05\x12\x17\n\x0fxudpProxyUDP443\x18\x04 \x01(\t*#\n\x0eKnownProtocols\x12\x08\n\x04HTTP\x10\x00\x12\x07\n\x03TLS\x10\x01\x42U\n\x15\x63om.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3')
+
+_KNOWNPROTOCOLS = DESCRIPTOR.enum_types_by_name['KnownProtocols']
+KnownProtocols = enum_type_wrapper.EnumTypeWrapper(_KNOWNPROTOCOLS)
+HTTP = 0
+TLS = 1
+
+
+_INBOUNDCONFIG = DESCRIPTOR.message_types_by_name['InboundConfig']
+_ALLOCATIONSTRATEGY = DESCRIPTOR.message_types_by_name['AllocationStrategy']
+_ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYCONCURRENCY = _ALLOCATIONSTRATEGY.nested_types_by_name['AllocationStrategyConcurrency']
+_ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYREFRESH = _ALLOCATIONSTRATEGY.nested_types_by_name['AllocationStrategyRefresh']
+_SNIFFINGCONFIG = DESCRIPTOR.message_types_by_name['SniffingConfig']
+_RECEIVERCONFIG = DESCRIPTOR.message_types_by_name['ReceiverConfig']
+_INBOUNDHANDLERCONFIG = DESCRIPTOR.message_types_by_name['InboundHandlerConfig']
+_OUTBOUNDCONFIG = DESCRIPTOR.message_types_by_name['OutboundConfig']
+_SENDERCONFIG = DESCRIPTOR.message_types_by_name['SenderConfig']
+_MULTIPLEXINGCONFIG = DESCRIPTOR.message_types_by_name['MultiplexingConfig']
+_ALLOCATIONSTRATEGY_TYPE = _ALLOCATIONSTRATEGY.enum_types_by_name['Type']
+InboundConfig = _reflection.GeneratedProtocolMessageType('InboundConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _INBOUNDCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.InboundConfig)
+ })
+_sym_db.RegisterMessage(InboundConfig)
+
+AllocationStrategy = _reflection.GeneratedProtocolMessageType('AllocationStrategy', (_message.Message,), {
+
+ 'AllocationStrategyConcurrency' : _reflection.GeneratedProtocolMessageType('AllocationStrategyConcurrency', (_message.Message,), {
+ 'DESCRIPTOR' : _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYCONCURRENCY,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.AllocationStrategy.AllocationStrategyConcurrency)
+ })
+ ,
+
+ 'AllocationStrategyRefresh' : _reflection.GeneratedProtocolMessageType('AllocationStrategyRefresh', (_message.Message,), {
+ 'DESCRIPTOR' : _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYREFRESH,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.AllocationStrategy.AllocationStrategyRefresh)
+ })
+ ,
+ 'DESCRIPTOR' : _ALLOCATIONSTRATEGY,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.AllocationStrategy)
+ })
+_sym_db.RegisterMessage(AllocationStrategy)
+_sym_db.RegisterMessage(AllocationStrategy.AllocationStrategyConcurrency)
+_sym_db.RegisterMessage(AllocationStrategy.AllocationStrategyRefresh)
+
+SniffingConfig = _reflection.GeneratedProtocolMessageType('SniffingConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SNIFFINGCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.SniffingConfig)
+ })
+_sym_db.RegisterMessage(SniffingConfig)
+
+ReceiverConfig = _reflection.GeneratedProtocolMessageType('ReceiverConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _RECEIVERCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.ReceiverConfig)
+ })
+_sym_db.RegisterMessage(ReceiverConfig)
+
+InboundHandlerConfig = _reflection.GeneratedProtocolMessageType('InboundHandlerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _INBOUNDHANDLERCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.InboundHandlerConfig)
+ })
+_sym_db.RegisterMessage(InboundHandlerConfig)
+
+OutboundConfig = _reflection.GeneratedProtocolMessageType('OutboundConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _OUTBOUNDCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.OutboundConfig)
+ })
+_sym_db.RegisterMessage(OutboundConfig)
+
+SenderConfig = _reflection.GeneratedProtocolMessageType('SenderConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SENDERCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.SenderConfig)
+ })
+_sym_db.RegisterMessage(SenderConfig)
+
+MultiplexingConfig = _reflection.GeneratedProtocolMessageType('MultiplexingConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _MULTIPLEXINGCONFIG,
+ '__module__' : 'app.proxyman.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.proxyman.MultiplexingConfig)
+ })
+_sym_db.RegisterMessage(MultiplexingConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\025com.xray.app.proxymanP\001Z&github.com/xtls/xray-core/app/proxyman\252\002\021Xray.App.Proxyman'
+ _RECEIVERCONFIG.fields_by_name['domain_override']._options = None
+ _RECEIVERCONFIG.fields_by_name['domain_override']._serialized_options = b'\030\001'
+ _KNOWNPROTOCOLS._serialized_start=1657
+ _KNOWNPROTOCOLS._serialized_end=1692
+ _INBOUNDCONFIG._serialized_start=165
+ _INBOUNDCONFIG._serialized_end=180
+ _ALLOCATIONSTRATEGY._serialized_start=183
+ _ALLOCATIONSTRATEGY._serialized_end=571
+ _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYCONCURRENCY._serialized_start=435
+ _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYCONCURRENCY._serialized_end=481
+ _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYREFRESH._serialized_start=483
+ _ALLOCATIONSTRATEGY_ALLOCATIONSTRATEGYREFRESH._serialized_end=525
+ _ALLOCATIONSTRATEGY_TYPE._serialized_start=527
+ _ALLOCATIONSTRATEGY_TYPE._serialized_end=571
+ _SNIFFINGCONFIG._serialized_start=574
+ _SNIFFINGCONFIG._serialized_end=706
+ _RECEIVERCONFIG._serialized_start=709
+ _RECEIVERCONFIG._serialized_end=1118
+ _INBOUNDHANDLERCONFIG._serialized_start=1121
+ _INBOUNDHANDLERCONFIG._serialized_end=1275
+ _OUTBOUNDCONFIG._serialized_start=1277
+ _OUTBOUNDCONFIG._serialized_end=1293
+ _SENDERCONFIG._serialized_start=1296
+ _SENDERCONFIG._serialized_end=1545
+ _MULTIPLEXINGCONFIG._serialized_start=1547
+ _MULTIPLEXINGCONFIG._serialized_end=1655
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/reverse/config_grpc.py b/xray_api/proto/app/reverse/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/reverse/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/reverse/config_pb2.py b/xray_api/proto/app/reverse/config_pb2.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/reverse/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x61pp/reverse/config.proto\x12\x10xray.app.reverse\"i\n\x07\x43ontrol\x12.\n\x05state\x18\x01 \x01(\x0e\x32\x1f.xray.app.reverse.Control.State\x12\x0e\n\x06random\x18\x63 \x01(\x0c\"\x1e\n\x05State\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\t\n\x05\x44RAIN\x10\x01\"+\n\x0c\x42ridgeConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x0e\n\x06\x64omain\x18\x02 \x01(\t\"+\n\x0cPortalConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x0e\n\x06\x64omain\x18\x02 \x01(\t\"v\n\x06\x43onfig\x12\x35\n\rbridge_config\x18\x01 \x03(\x0b\x32\x1e.xray.app.reverse.BridgeConfig\x12\x35\n\rportal_config\x18\x02 \x03(\x0b\x32\x1e.xray.app.reverse.PortalConfigBV\n\x16\x63om.xray.proxy.reverseP\x01Z%github.com/xtls/xray-core/app/reverse\xaa\x02\x12Xray.Proxy.Reverseb\x06proto3')
+
+
+
+_CONTROL = DESCRIPTOR.message_types_by_name['Control']
+_BRIDGECONFIG = DESCRIPTOR.message_types_by_name['BridgeConfig']
+_PORTALCONFIG = DESCRIPTOR.message_types_by_name['PortalConfig']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONTROL_STATE = _CONTROL.enum_types_by_name['State']
+Control = _reflection.GeneratedProtocolMessageType('Control', (_message.Message,), {
+ 'DESCRIPTOR' : _CONTROL,
+ '__module__' : 'app.reverse.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.reverse.Control)
+ })
+_sym_db.RegisterMessage(Control)
+
+BridgeConfig = _reflection.GeneratedProtocolMessageType('BridgeConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _BRIDGECONFIG,
+ '__module__' : 'app.reverse.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.reverse.BridgeConfig)
+ })
+_sym_db.RegisterMessage(BridgeConfig)
+
+PortalConfig = _reflection.GeneratedProtocolMessageType('PortalConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _PORTALCONFIG,
+ '__module__' : 'app.reverse.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.reverse.PortalConfig)
+ })
+_sym_db.RegisterMessage(PortalConfig)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.reverse.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.reverse.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\026com.xray.proxy.reverseP\001Z%github.com/xtls/xray-core/app/reverse\252\002\022Xray.Proxy.Reverse'
+ _CONTROL._serialized_start=46
+ _CONTROL._serialized_end=151
+ _CONTROL_STATE._serialized_start=121
+ _CONTROL_STATE._serialized_end=151
+ _BRIDGECONFIG._serialized_start=153
+ _BRIDGECONFIG._serialized_end=196
+ _PORTALCONFIG._serialized_start=198
+ _PORTALCONFIG._serialized_end=241
+ _CONFIG._serialized_start=243
+ _CONFIG._serialized_end=361
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/router/command/command_grpc.py b/xray_api/proto/app/router/command/command_grpc.py
@@ -0,0 +1,57 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/router/command/command.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.common.net.network_pb2
+import xray_api.proto.app.router.command.command_pb2
+
+
+class RoutingServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def SubscribeRoutingStats(self, stream: 'grpclib.server.Stream[app.router.command.command_pb2.SubscribeRoutingStatsRequest, app.router.command.command_pb2.RoutingContext]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def TestRoute(self, stream: 'grpclib.server.Stream[app.router.command.command_pb2.TestRouteRequest, app.router.command.command_pb2.RoutingContext]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.app.router.command.RoutingService/SubscribeRoutingStats': grpclib.const.Handler(
+ self.SubscribeRoutingStats,
+ grpclib.const.Cardinality.UNARY_STREAM,
+ xray_api.proto.app.router.command.command_pb2.SubscribeRoutingStatsRequest,
+ xray_api.proto.app.router.command.command_pb2.RoutingContext,
+ ),
+ '/xray.app.router.command.RoutingService/TestRoute': grpclib.const.Handler(
+ self.TestRoute,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.router.command.command_pb2.TestRouteRequest,
+ xray_api.proto.app.router.command.command_pb2.RoutingContext,
+ ),
+ }
+
+
+class RoutingServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.SubscribeRoutingStats = grpclib.client.UnaryStreamMethod(
+ channel,
+ '/xray.app.router.command.RoutingService/SubscribeRoutingStats',
+ xray_api.proto.app.router.command.command_pb2.SubscribeRoutingStatsRequest,
+ xray_api.proto.app.router.command.command_pb2.RoutingContext,
+ )
+ self.TestRoute = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.router.command.RoutingService/TestRoute',
+ xray_api.proto.app.router.command.command_pb2.TestRouteRequest,
+ xray_api.proto.app.router.command.command_pb2.RoutingContext,
+ )
diff --git a/xray_api/proto/app/router/command/command_pb2.py b/xray_api/proto/app/router/command/command_pb2.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/router/command/command.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n app/router/command/command.proto\x12\x17xray.app.router.command\x1a\x18\x63ommon/net/network.proto\"\x83\x03\n\x0eRoutingContext\x12\x12\n\nInboundTag\x18\x01 \x01(\t\x12)\n\x07Network\x18\x02 \x01(\x0e\x32\x18.xray.common.net.Network\x12\x11\n\tSourceIPs\x18\x03 \x03(\x0c\x12\x11\n\tTargetIPs\x18\x04 \x03(\x0c\x12\x12\n\nSourcePort\x18\x05 \x01(\r\x12\x12\n\nTargetPort\x18\x06 \x01(\r\x12\x14\n\x0cTargetDomain\x18\x07 \x01(\t\x12\x10\n\x08Protocol\x18\x08 \x01(\t\x12\x0c\n\x04User\x18\t \x01(\t\x12K\n\nAttributes\x18\n \x03(\x0b\x32\x37.xray.app.router.command.RoutingContext.AttributesEntry\x12\x19\n\x11OutboundGroupTags\x18\x0b \x03(\t\x12\x13\n\x0bOutboundTag\x18\x0c \x01(\t\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"6\n\x1cSubscribeRoutingStatsRequest\x12\x16\n\x0e\x46ieldSelectors\x18\x01 \x03(\t\"\x82\x01\n\x10TestRouteRequest\x12?\n\x0eRoutingContext\x18\x01 \x01(\x0b\x32\'.xray.app.router.command.RoutingContext\x12\x16\n\x0e\x46ieldSelectors\x18\x02 \x03(\t\x12\x15\n\rPublishResult\x18\x03 \x01(\x08\"\x08\n\x06\x43onfig2\xf0\x01\n\x0eRoutingService\x12{\n\x15SubscribeRoutingStats\x12\x35.xray.app.router.command.SubscribeRoutingStatsRequest\x1a\'.xray.app.router.command.RoutingContext\"\x00\x30\x01\x12\x61\n\tTestRoute\x12).xray.app.router.command.TestRouteRequest\x1a\'.xray.app.router.command.RoutingContext\"\x00\x42g\n\x1b\x63om.xray.app.router.commandP\x01Z,github.com/xtls/xray-core/app/router/command\xaa\x02\x17Xray.App.Router.Commandb\x06proto3')
+
+
+
+_ROUTINGCONTEXT = DESCRIPTOR.message_types_by_name['RoutingContext']
+_ROUTINGCONTEXT_ATTRIBUTESENTRY = _ROUTINGCONTEXT.nested_types_by_name['AttributesEntry']
+_SUBSCRIBEROUTINGSTATSREQUEST = DESCRIPTOR.message_types_by_name['SubscribeRoutingStatsRequest']
+_TESTROUTEREQUEST = DESCRIPTOR.message_types_by_name['TestRouteRequest']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+RoutingContext = _reflection.GeneratedProtocolMessageType('RoutingContext', (_message.Message,), {
+
+ 'AttributesEntry' : _reflection.GeneratedProtocolMessageType('AttributesEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _ROUTINGCONTEXT_ATTRIBUTESENTRY,
+ '__module__' : 'app.router.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.command.RoutingContext.AttributesEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _ROUTINGCONTEXT,
+ '__module__' : 'app.router.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.command.RoutingContext)
+ })
+_sym_db.RegisterMessage(RoutingContext)
+_sym_db.RegisterMessage(RoutingContext.AttributesEntry)
+
+SubscribeRoutingStatsRequest = _reflection.GeneratedProtocolMessageType('SubscribeRoutingStatsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _SUBSCRIBEROUTINGSTATSREQUEST,
+ '__module__' : 'app.router.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.command.SubscribeRoutingStatsRequest)
+ })
+_sym_db.RegisterMessage(SubscribeRoutingStatsRequest)
+
+TestRouteRequest = _reflection.GeneratedProtocolMessageType('TestRouteRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _TESTROUTEREQUEST,
+ '__module__' : 'app.router.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.command.TestRouteRequest)
+ })
+_sym_db.RegisterMessage(TestRouteRequest)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.router.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.command.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+_ROUTINGSERVICE = DESCRIPTOR.services_by_name['RoutingService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\033com.xray.app.router.commandP\001Z,github.com/xtls/xray-core/app/router/command\252\002\027Xray.App.Router.Command'
+ _ROUTINGCONTEXT_ATTRIBUTESENTRY._options = None
+ _ROUTINGCONTEXT_ATTRIBUTESENTRY._serialized_options = b'8\001'
+ _ROUTINGCONTEXT._serialized_start=88
+ _ROUTINGCONTEXT._serialized_end=475
+ _ROUTINGCONTEXT_ATTRIBUTESENTRY._serialized_start=426
+ _ROUTINGCONTEXT_ATTRIBUTESENTRY._serialized_end=475
+ _SUBSCRIBEROUTINGSTATSREQUEST._serialized_start=477
+ _SUBSCRIBEROUTINGSTATSREQUEST._serialized_end=531
+ _TESTROUTEREQUEST._serialized_start=534
+ _TESTROUTEREQUEST._serialized_end=664
+ _CONFIG._serialized_start=666
+ _CONFIG._serialized_end=674
+ _ROUTINGSERVICE._serialized_start=677
+ _ROUTINGSERVICE._serialized_end=917
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/router/config_grpc.py b/xray_api/proto/app/router/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/router/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/router/config_pb2.py b/xray_api/proto/app/router/config_pb2.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/router/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import port_pb2 as common_dot_net_dot_port__pb2
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x61pp/router/config.proto\x12\x0fxray.app.router\x1a\x15\x63ommon/net/port.proto\x1a\x18\x63ommon/net/network.proto\"\x81\x02\n\x06\x44omain\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.xray.app.router.Domain.Type\x12\r\n\x05value\x18\x02 \x01(\t\x12\x34\n\tattribute\x18\x03 \x03(\x0b\x32!.xray.app.router.Domain.Attribute\x1aR\n\tAttribute\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x42\r\n\x0btyped_value\"2\n\x04Type\x12\t\n\x05Plain\x10\x00\x12\t\n\x05Regex\x10\x01\x12\n\n\x06\x44omain\x10\x02\x12\x08\n\x04\x46ull\x10\x03\"\"\n\x04\x43IDR\x12\n\n\x02ip\x18\x01 \x01(\x0c\x12\x0e\n\x06prefix\x18\x02 \x01(\r\"Y\n\x05GeoIP\x12\x14\n\x0c\x63ountry_code\x18\x01 \x01(\t\x12#\n\x04\x63idr\x18\x02 \x03(\x0b\x32\x15.xray.app.router.CIDR\x12\x15\n\rreverse_match\x18\x03 \x01(\x08\"2\n\tGeoIPList\x12%\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x16.xray.app.router.GeoIP\"H\n\x07GeoSite\x12\x14\n\x0c\x63ountry_code\x18\x01 \x01(\t\x12\'\n\x06\x64omain\x18\x02 \x03(\x0b\x32\x17.xray.app.router.Domain\"6\n\x0bGeoSiteList\x12\'\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x18.xray.app.router.GeoSite\"\xdd\x05\n\x0bRoutingRule\x12\r\n\x03tag\x18\x01 \x01(\tH\x00\x12\x17\n\rbalancing_tag\x18\x0c \x01(\tH\x00\x12\'\n\x06\x64omain\x18\x02 \x03(\x0b\x32\x17.xray.app.router.Domain\x12\'\n\x04\x63idr\x18\x03 \x03(\x0b\x32\x15.xray.app.router.CIDRB\x02\x18\x01\x12%\n\x05geoip\x18\n \x03(\x0b\x32\x16.xray.app.router.GeoIP\x12\x32\n\nport_range\x18\x04 \x01(\x0b\x32\x1a.xray.common.net.PortRangeB\x02\x18\x01\x12,\n\tport_list\x18\x0e \x01(\x0b\x32\x19.xray.common.net.PortList\x12\x36\n\x0cnetwork_list\x18\x05 \x01(\x0b\x32\x1c.xray.common.net.NetworkListB\x02\x18\x01\x12*\n\x08networks\x18\r \x03(\x0e\x32\x18.xray.common.net.Network\x12.\n\x0bsource_cidr\x18\x06 \x03(\x0b\x32\x15.xray.app.router.CIDRB\x02\x18\x01\x12,\n\x0csource_geoip\x18\x0b \x03(\x0b\x32\x16.xray.app.router.GeoIP\x12\x33\n\x10source_port_list\x18\x10 \x01(\x0b\x32\x19.xray.common.net.PortList\x12\x12\n\nuser_email\x18\x07 \x03(\t\x12\x13\n\x0binbound_tag\x18\x08 \x03(\t\x12\x10\n\x08protocol\x18\t \x03(\t\x12@\n\nattributes\x18\x0f \x03(\x0b\x32,.xray.app.router.RoutingRule.AttributesEntry\x12\x16\n\x0e\x64omain_matcher\x18\x11 \x01(\t\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\ntarget_tag\"I\n\rBalancingRule\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x19\n\x11outbound_selector\x18\x02 \x03(\t\x12\x10\n\x08strategy\x18\x03 \x01(\t\"\xf6\x01\n\x06\x43onfig\x12?\n\x0f\x64omain_strategy\x18\x01 \x01(\x0e\x32&.xray.app.router.Config.DomainStrategy\x12*\n\x04rule\x18\x02 \x03(\x0b\x32\x1c.xray.app.router.RoutingRule\x12\x36\n\x0e\x62\x61lancing_rule\x18\x03 \x03(\x0b\x32\x1e.xray.app.router.BalancingRule\"G\n\x0e\x44omainStrategy\x12\x08\n\x04\x41sIs\x10\x00\x12\t\n\x05UseIp\x10\x01\x12\x10\n\x0cIpIfNonMatch\x10\x02\x12\x0e\n\nIpOnDemand\x10\x03\x42O\n\x13\x63om.xray.app.routerP\x01Z$github.com/xtls/xray-core/app/router\xaa\x02\x0fXray.App.Routerb\x06proto3')
+
+
+
+_DOMAIN = DESCRIPTOR.message_types_by_name['Domain']
+_DOMAIN_ATTRIBUTE = _DOMAIN.nested_types_by_name['Attribute']
+_CIDR = DESCRIPTOR.message_types_by_name['CIDR']
+_GEOIP = DESCRIPTOR.message_types_by_name['GeoIP']
+_GEOIPLIST = DESCRIPTOR.message_types_by_name['GeoIPList']
+_GEOSITE = DESCRIPTOR.message_types_by_name['GeoSite']
+_GEOSITELIST = DESCRIPTOR.message_types_by_name['GeoSiteList']
+_ROUTINGRULE = DESCRIPTOR.message_types_by_name['RoutingRule']
+_ROUTINGRULE_ATTRIBUTESENTRY = _ROUTINGRULE.nested_types_by_name['AttributesEntry']
+_BALANCINGRULE = DESCRIPTOR.message_types_by_name['BalancingRule']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_DOMAIN_TYPE = _DOMAIN.enum_types_by_name['Type']
+_CONFIG_DOMAINSTRATEGY = _CONFIG.enum_types_by_name['DomainStrategy']
+Domain = _reflection.GeneratedProtocolMessageType('Domain', (_message.Message,), {
+
+ 'Attribute' : _reflection.GeneratedProtocolMessageType('Attribute', (_message.Message,), {
+ 'DESCRIPTOR' : _DOMAIN_ATTRIBUTE,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.Domain.Attribute)
+ })
+ ,
+ 'DESCRIPTOR' : _DOMAIN,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.Domain)
+ })
+_sym_db.RegisterMessage(Domain)
+_sym_db.RegisterMessage(Domain.Attribute)
+
+CIDR = _reflection.GeneratedProtocolMessageType('CIDR', (_message.Message,), {
+ 'DESCRIPTOR' : _CIDR,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.CIDR)
+ })
+_sym_db.RegisterMessage(CIDR)
+
+GeoIP = _reflection.GeneratedProtocolMessageType('GeoIP', (_message.Message,), {
+ 'DESCRIPTOR' : _GEOIP,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.GeoIP)
+ })
+_sym_db.RegisterMessage(GeoIP)
+
+GeoIPList = _reflection.GeneratedProtocolMessageType('GeoIPList', (_message.Message,), {
+ 'DESCRIPTOR' : _GEOIPLIST,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.GeoIPList)
+ })
+_sym_db.RegisterMessage(GeoIPList)
+
+GeoSite = _reflection.GeneratedProtocolMessageType('GeoSite', (_message.Message,), {
+ 'DESCRIPTOR' : _GEOSITE,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.GeoSite)
+ })
+_sym_db.RegisterMessage(GeoSite)
+
+GeoSiteList = _reflection.GeneratedProtocolMessageType('GeoSiteList', (_message.Message,), {
+ 'DESCRIPTOR' : _GEOSITELIST,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.GeoSiteList)
+ })
+_sym_db.RegisterMessage(GeoSiteList)
+
+RoutingRule = _reflection.GeneratedProtocolMessageType('RoutingRule', (_message.Message,), {
+
+ 'AttributesEntry' : _reflection.GeneratedProtocolMessageType('AttributesEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _ROUTINGRULE_ATTRIBUTESENTRY,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.RoutingRule.AttributesEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _ROUTINGRULE,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.RoutingRule)
+ })
+_sym_db.RegisterMessage(RoutingRule)
+_sym_db.RegisterMessage(RoutingRule.AttributesEntry)
+
+BalancingRule = _reflection.GeneratedProtocolMessageType('BalancingRule', (_message.Message,), {
+ 'DESCRIPTOR' : _BALANCINGRULE,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.BalancingRule)
+ })
+_sym_db.RegisterMessage(BalancingRule)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.router.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.router.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.app.routerP\001Z$github.com/xtls/xray-core/app/router\252\002\017Xray.App.Router'
+ _ROUTINGRULE_ATTRIBUTESENTRY._options = None
+ _ROUTINGRULE_ATTRIBUTESENTRY._serialized_options = b'8\001'
+ _ROUTINGRULE.fields_by_name['cidr']._options = None
+ _ROUTINGRULE.fields_by_name['cidr']._serialized_options = b'\030\001'
+ _ROUTINGRULE.fields_by_name['port_range']._options = None
+ _ROUTINGRULE.fields_by_name['port_range']._serialized_options = b'\030\001'
+ _ROUTINGRULE.fields_by_name['network_list']._options = None
+ _ROUTINGRULE.fields_by_name['network_list']._serialized_options = b'\030\001'
+ _ROUTINGRULE.fields_by_name['source_cidr']._options = None
+ _ROUTINGRULE.fields_by_name['source_cidr']._serialized_options = b'\030\001'
+ _DOMAIN._serialized_start=94
+ _DOMAIN._serialized_end=351
+ _DOMAIN_ATTRIBUTE._serialized_start=217
+ _DOMAIN_ATTRIBUTE._serialized_end=299
+ _DOMAIN_TYPE._serialized_start=301
+ _DOMAIN_TYPE._serialized_end=351
+ _CIDR._serialized_start=353
+ _CIDR._serialized_end=387
+ _GEOIP._serialized_start=389
+ _GEOIP._serialized_end=478
+ _GEOIPLIST._serialized_start=480
+ _GEOIPLIST._serialized_end=530
+ _GEOSITE._serialized_start=532
+ _GEOSITE._serialized_end=604
+ _GEOSITELIST._serialized_start=606
+ _GEOSITELIST._serialized_end=660
+ _ROUTINGRULE._serialized_start=663
+ _ROUTINGRULE._serialized_end=1396
+ _ROUTINGRULE_ATTRIBUTESENTRY._serialized_start=1333
+ _ROUTINGRULE_ATTRIBUTESENTRY._serialized_end=1382
+ _BALANCINGRULE._serialized_start=1398
+ _BALANCINGRULE._serialized_end=1471
+ _CONFIG._serialized_start=1474
+ _CONFIG._serialized_end=1720
+ _CONFIG_DOMAINSTRATEGY._serialized_start=1649
+ _CONFIG_DOMAINSTRATEGY._serialized_end=1720
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/stats/command/command_grpc.py b/xray_api/proto/app/stats/command/command_grpc.py
@@ -0,0 +1,72 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/stats/command/command.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.app.stats.command.command_pb2
+
+
+class StatsServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def GetStats(self, stream: 'grpclib.server.Stream[app.stats.command.command_pb2.GetStatsRequest, app.stats.command.command_pb2.GetStatsResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def QueryStats(self, stream: 'grpclib.server.Stream[app.stats.command.command_pb2.QueryStatsRequest, app.stats.command.command_pb2.QueryStatsResponse]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def GetSysStats(self, stream: 'grpclib.server.Stream[app.stats.command.command_pb2.SysStatsRequest, app.stats.command.command_pb2.SysStatsResponse]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.app.stats.command.StatsService/GetStats': grpclib.const.Handler(
+ self.GetStats,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.stats.command.command_pb2.GetStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.GetStatsResponse,
+ ),
+ '/xray.app.stats.command.StatsService/QueryStats': grpclib.const.Handler(
+ self.QueryStats,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.stats.command.command_pb2.QueryStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.QueryStatsResponse,
+ ),
+ '/xray.app.stats.command.StatsService/GetSysStats': grpclib.const.Handler(
+ self.GetSysStats,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ xray_api.proto.app.stats.command.command_pb2.SysStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.SysStatsResponse,
+ ),
+ }
+
+
+class StatsServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.GetStats = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.stats.command.StatsService/GetStats',
+ xray_api.proto.app.stats.command.command_pb2.GetStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.GetStatsResponse,
+ )
+ self.QueryStats = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.stats.command.StatsService/QueryStats',
+ xray_api.proto.app.stats.command.command_pb2.QueryStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.QueryStatsResponse,
+ )
+ self.GetSysStats = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/xray.app.stats.command.StatsService/GetSysStats',
+ xray_api.proto.app.stats.command.command_pb2.SysStatsRequest,
+ xray_api.proto.app.stats.command.command_pb2.SysStatsResponse,
+ )
diff --git a/xray_api/proto/app/stats/command/command_pb2.py b/xray_api/proto/app/stats/command/command_pb2.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/stats/command/command.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61pp/stats/command/command.proto\x12\x16xray.app.stats.command\".\n\x0fGetStatsRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"#\n\x04Stat\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x03\">\n\x10GetStatsResponse\x12*\n\x04stat\x18\x01 \x01(\x0b\x32\x1c.xray.app.stats.command.Stat\"3\n\x11QueryStatsRequest\x12\x0f\n\x07pattern\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"@\n\x12QueryStatsResponse\x12*\n\x04stat\x18\x01 \x03(\x0b\x32\x1c.xray.app.stats.command.Stat\"\x11\n\x0fSysStatsRequest\"\xc2\x01\n\x10SysStatsResponse\x12\x14\n\x0cNumGoroutine\x18\x01 \x01(\r\x12\r\n\x05NumGC\x18\x02 \x01(\r\x12\r\n\x05\x41lloc\x18\x03 \x01(\x04\x12\x12\n\nTotalAlloc\x18\x04 \x01(\x04\x12\x0b\n\x03Sys\x18\x05 \x01(\x04\x12\x0f\n\x07Mallocs\x18\x06 \x01(\x04\x12\r\n\x05\x46rees\x18\x07 \x01(\x04\x12\x13\n\x0bLiveObjects\x18\x08 \x01(\x04\x12\x14\n\x0cPauseTotalNs\x18\t \x01(\x04\x12\x0e\n\x06Uptime\x18\n \x01(\r\"\x08\n\x06\x43onfig2\xba\x02\n\x0cStatsService\x12_\n\x08GetStats\x12\'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12\x65\n\nQueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12\x62\n\x0bGetSysStats\x12\'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x42\x64\n\x1a\x63om.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3')
+
+
+
+_GETSTATSREQUEST = DESCRIPTOR.message_types_by_name['GetStatsRequest']
+_STAT = DESCRIPTOR.message_types_by_name['Stat']
+_GETSTATSRESPONSE = DESCRIPTOR.message_types_by_name['GetStatsResponse']
+_QUERYSTATSREQUEST = DESCRIPTOR.message_types_by_name['QueryStatsRequest']
+_QUERYSTATSRESPONSE = DESCRIPTOR.message_types_by_name['QueryStatsResponse']
+_SYSSTATSREQUEST = DESCRIPTOR.message_types_by_name['SysStatsRequest']
+_SYSSTATSRESPONSE = DESCRIPTOR.message_types_by_name['SysStatsResponse']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+GetStatsRequest = _reflection.GeneratedProtocolMessageType('GetStatsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETSTATSREQUEST,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.GetStatsRequest)
+ })
+_sym_db.RegisterMessage(GetStatsRequest)
+
+Stat = _reflection.GeneratedProtocolMessageType('Stat', (_message.Message,), {
+ 'DESCRIPTOR' : _STAT,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.Stat)
+ })
+_sym_db.RegisterMessage(Stat)
+
+GetStatsResponse = _reflection.GeneratedProtocolMessageType('GetStatsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _GETSTATSRESPONSE,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.GetStatsResponse)
+ })
+_sym_db.RegisterMessage(GetStatsResponse)
+
+QueryStatsRequest = _reflection.GeneratedProtocolMessageType('QueryStatsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _QUERYSTATSREQUEST,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.QueryStatsRequest)
+ })
+_sym_db.RegisterMessage(QueryStatsRequest)
+
+QueryStatsResponse = _reflection.GeneratedProtocolMessageType('QueryStatsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _QUERYSTATSRESPONSE,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.QueryStatsResponse)
+ })
+_sym_db.RegisterMessage(QueryStatsResponse)
+
+SysStatsRequest = _reflection.GeneratedProtocolMessageType('SysStatsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _SYSSTATSREQUEST,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.SysStatsRequest)
+ })
+_sym_db.RegisterMessage(SysStatsRequest)
+
+SysStatsResponse = _reflection.GeneratedProtocolMessageType('SysStatsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _SYSSTATSRESPONSE,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.SysStatsResponse)
+ })
+_sym_db.RegisterMessage(SysStatsResponse)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.stats.command.command_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.command.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+_STATSSERVICE = DESCRIPTOR.services_by_name['StatsService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\032com.xray.app.stats.commandP\001Z+github.com/xtls/xray-core/app/stats/command\252\002\026Xray.App.Stats.Command'
+ _GETSTATSREQUEST._serialized_start=59
+ _GETSTATSREQUEST._serialized_end=105
+ _STAT._serialized_start=107
+ _STAT._serialized_end=142
+ _GETSTATSRESPONSE._serialized_start=144
+ _GETSTATSRESPONSE._serialized_end=206
+ _QUERYSTATSREQUEST._serialized_start=208
+ _QUERYSTATSREQUEST._serialized_end=259
+ _QUERYSTATSRESPONSE._serialized_start=261
+ _QUERYSTATSRESPONSE._serialized_end=325
+ _SYSSTATSREQUEST._serialized_start=327
+ _SYSSTATSREQUEST._serialized_end=344
+ _SYSSTATSRESPONSE._serialized_start=347
+ _SYSSTATSRESPONSE._serialized_end=541
+ _CONFIG._serialized_start=543
+ _CONFIG._serialized_end=551
+ _STATSSERVICE._serialized_start=554
+ _STATSSERVICE._serialized_end=868
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/app/stats/config_grpc.py b/xray_api/proto/app/stats/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: app/stats/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/app/stats/config_pb2.py b/xray_api/proto/app/stats/config_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: app/stats/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x61pp/stats/config.proto\x12\x0exray.app.stats\"\x08\n\x06\x43onfig\"N\n\rChannelConfig\x12\x10\n\x08\x42locking\x18\x01 \x01(\x08\x12\x17\n\x0fSubscriberLimit\x18\x02 \x01(\x05\x12\x12\n\nBufferSize\x18\x03 \x01(\x05\x42L\n\x12\x63om.xray.app.statsP\x01Z#github.com/xtls/xray-core/app/stats\xaa\x02\x0eXray.App.Statsb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CHANNELCONFIG = DESCRIPTOR.message_types_by_name['ChannelConfig']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'app.stats.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+ChannelConfig = _reflection.GeneratedProtocolMessageType('ChannelConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CHANNELCONFIG,
+ '__module__' : 'app.stats.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.app.stats.ChannelConfig)
+ })
+_sym_db.RegisterMessage(ChannelConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\022com.xray.app.statsP\001Z#github.com/xtls/xray-core/app/stats\252\002\016Xray.App.Stats'
+ _CONFIG._serialized_start=42
+ _CONFIG._serialized_end=50
+ _CHANNELCONFIG._serialized_start=52
+ _CHANNELCONFIG._serialized_end=130
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/log/log_grpc.py b/xray_api/proto/common/log/log_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/log/log.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/log/log_pb2.py b/xray_api/proto/common/log/log_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/log/log.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x63ommon/log/log.proto\x12\x0fxray.common.log*D\n\x08Severity\x12\x0b\n\x07Unknown\x10\x00\x12\t\n\x05\x45rror\x10\x01\x12\x0b\n\x07Warning\x10\x02\x12\x08\n\x04Info\x10\x03\x12\t\n\x05\x44\x65\x62ug\x10\x04\x42O\n\x13\x63om.xray.common.logP\x01Z$github.com/xtls/xray-core/common/log\xaa\x02\x0fXray.Common.Logb\x06proto3')
+
+_SEVERITY = DESCRIPTOR.enum_types_by_name['Severity']
+Severity = enum_type_wrapper.EnumTypeWrapper(_SEVERITY)
+Unknown = 0
+Error = 1
+Warning = 2
+Info = 3
+Debug = 4
+
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.common.logP\001Z$github.com/xtls/xray-core/common/log\252\002\017Xray.Common.Log'
+ _SEVERITY._serialized_start=41
+ _SEVERITY._serialized_end=109
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/net/address_grpc.py b/xray_api/proto/common/net/address_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/net/address.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/net/address_pb2.py b/xray_api/proto/common/net/address_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/net/address.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x63ommon/net/address.proto\x12\x0fxray.common.net\"7\n\nIPOrDomain\x12\x0c\n\x02ip\x18\x01 \x01(\x0cH\x00\x12\x10\n\x06\x64omain\x18\x02 \x01(\tH\x00\x42\t\n\x07\x61\x64\x64ressBO\n\x13\x63om.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3')
+
+
+
+_IPORDOMAIN = DESCRIPTOR.message_types_by_name['IPOrDomain']
+IPOrDomain = _reflection.GeneratedProtocolMessageType('IPOrDomain', (_message.Message,), {
+ 'DESCRIPTOR' : _IPORDOMAIN,
+ '__module__' : 'common.net.address_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.net.IPOrDomain)
+ })
+_sym_db.RegisterMessage(IPOrDomain)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.common.netP\001Z$github.com/xtls/xray-core/common/net\252\002\017Xray.Common.Net'
+ _IPORDOMAIN._serialized_start=45
+ _IPORDOMAIN._serialized_end=100
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/net/destination_grpc.py b/xray_api/proto/common/net/destination_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/net/destination.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/net/destination_pb2.py b/xray_api/proto/common/net/destination_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/net/destination.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63ommon/net/destination.proto\x12\x0fxray.common.net\x1a\x18\x63ommon/net/network.proto\x1a\x18\x63ommon/net/address.proto\"q\n\x08\x45ndpoint\x12)\n\x07network\x18\x01 \x01(\x0e\x32\x18.xray.common.net.Network\x12,\n\x07\x61\x64\x64ress\x18\x02 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x0c\n\x04port\x18\x03 \x01(\rBO\n\x13\x63om.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3')
+
+
+
+_ENDPOINT = DESCRIPTOR.message_types_by_name['Endpoint']
+Endpoint = _reflection.GeneratedProtocolMessageType('Endpoint', (_message.Message,), {
+ 'DESCRIPTOR' : _ENDPOINT,
+ '__module__' : 'common.net.destination_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.net.Endpoint)
+ })
+_sym_db.RegisterMessage(Endpoint)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.common.netP\001Z$github.com/xtls/xray-core/common/net\252\002\017Xray.Common.Net'
+ _ENDPOINT._serialized_start=101
+ _ENDPOINT._serialized_end=214
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/net/network_grpc.py b/xray_api/proto/common/net/network_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/net/network.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/net/network_pb2.py b/xray_api/proto/common/net/network_pb2.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/net/network.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x63ommon/net/network.proto\x12\x0fxray.common.net\"8\n\x0bNetworkList\x12)\n\x07network\x18\x01 \x03(\x0e\x32\x18.xray.common.net.Network*B\n\x07Network\x12\x0b\n\x07Unknown\x10\x00\x12\x0e\n\x06RawTCP\x10\x01\x1a\x02\x08\x01\x12\x07\n\x03TCP\x10\x02\x12\x07\n\x03UDP\x10\x03\x12\x08\n\x04UNIX\x10\x04\x42O\n\x13\x63om.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3')
+
+_NETWORK = DESCRIPTOR.enum_types_by_name['Network']
+Network = enum_type_wrapper.EnumTypeWrapper(_NETWORK)
+Unknown = 0
+RawTCP = 1
+TCP = 2
+UDP = 3
+UNIX = 4
+
+
+_NETWORKLIST = DESCRIPTOR.message_types_by_name['NetworkList']
+NetworkList = _reflection.GeneratedProtocolMessageType('NetworkList', (_message.Message,), {
+ 'DESCRIPTOR' : _NETWORKLIST,
+ '__module__' : 'common.net.network_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.net.NetworkList)
+ })
+_sym_db.RegisterMessage(NetworkList)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.common.netP\001Z$github.com/xtls/xray-core/common/net\252\002\017Xray.Common.Net'
+ _NETWORK.values_by_name["RawTCP"]._options = None
+ _NETWORK.values_by_name["RawTCP"]._serialized_options = b'\010\001'
+ _NETWORK._serialized_start=103
+ _NETWORK._serialized_end=169
+ _NETWORKLIST._serialized_start=45
+ _NETWORKLIST._serialized_end=101
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/net/port_grpc.py b/xray_api/proto/common/net/port_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/net/port.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/net/port_pb2.py b/xray_api/proto/common/net/port_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/net/port.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63ommon/net/port.proto\x12\x0fxray.common.net\"%\n\tPortRange\x12\x0c\n\x04\x46rom\x18\x01 \x01(\r\x12\n\n\x02To\x18\x02 \x01(\r\"5\n\x08PortList\x12)\n\x05range\x18\x01 \x03(\x0b\x32\x1a.xray.common.net.PortRangeBO\n\x13\x63om.xray.common.netP\x01Z$github.com/xtls/xray-core/common/net\xaa\x02\x0fXray.Common.Netb\x06proto3')
+
+
+
+_PORTRANGE = DESCRIPTOR.message_types_by_name['PortRange']
+_PORTLIST = DESCRIPTOR.message_types_by_name['PortList']
+PortRange = _reflection.GeneratedProtocolMessageType('PortRange', (_message.Message,), {
+ 'DESCRIPTOR' : _PORTRANGE,
+ '__module__' : 'common.net.port_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.net.PortRange)
+ })
+_sym_db.RegisterMessage(PortRange)
+
+PortList = _reflection.GeneratedProtocolMessageType('PortList', (_message.Message,), {
+ 'DESCRIPTOR' : _PORTLIST,
+ '__module__' : 'common.net.port_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.net.PortList)
+ })
+_sym_db.RegisterMessage(PortList)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.common.netP\001Z$github.com/xtls/xray-core/common/net\252\002\017Xray.Common.Net'
+ _PORTRANGE._serialized_start=42
+ _PORTRANGE._serialized_end=79
+ _PORTLIST._serialized_start=81
+ _PORTLIST._serialized_end=134
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/protocol/headers_grpc.py b/xray_api/proto/common/protocol/headers_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/protocol/headers.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/protocol/headers_pb2.py b/xray_api/proto/common/protocol/headers_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/protocol/headers.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x63ommon/protocol/headers.proto\x12\x14xray.common.protocol\"B\n\x0eSecurityConfig\x12\x30\n\x04type\x18\x01 \x01(\x0e\x32\".xray.common.protocol.SecurityType*`\n\x0cSecurityType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04\x41UTO\x10\x02\x12\x0e\n\nAES128_GCM\x10\x03\x12\x15\n\x11\x43HACHA20_POLY1305\x10\x04\x12\x08\n\x04NONE\x10\x05\x12\x08\n\x04ZERO\x10\x06\x42^\n\x18\x63om.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3')
+
+_SECURITYTYPE = DESCRIPTOR.enum_types_by_name['SecurityType']
+SecurityType = enum_type_wrapper.EnumTypeWrapper(_SECURITYTYPE)
+UNKNOWN = 0
+AUTO = 2
+AES128_GCM = 3
+CHACHA20_POLY1305 = 4
+NONE = 5
+ZERO = 6
+
+
+_SECURITYCONFIG = DESCRIPTOR.message_types_by_name['SecurityConfig']
+SecurityConfig = _reflection.GeneratedProtocolMessageType('SecurityConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SECURITYCONFIG,
+ '__module__' : 'common.protocol.headers_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.protocol.SecurityConfig)
+ })
+_sym_db.RegisterMessage(SecurityConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.common.protocolP\001Z)github.com/xtls/xray-core/common/protocol\252\002\024Xray.Common.Protocol'
+ _SECURITYTYPE._serialized_start=123
+ _SECURITYTYPE._serialized_end=219
+ _SECURITYCONFIG._serialized_start=55
+ _SECURITYCONFIG._serialized_end=121
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/protocol/server_spec_grpc.py b/xray_api/proto/common/protocol/server_spec_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/protocol/server_spec.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/protocol/server_spec_pb2.py b/xray_api/proto/common/protocol/server_spec_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/protocol/server_spec.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!common/protocol/server_spec.proto\x12\x14xray.common.protocol\x1a\x18\x63ommon/net/address.proto\x1a\x1a\x63ommon/protocol/user.proto\"v\n\x0eServerEndpoint\x12,\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x0c\n\x04port\x18\x02 \x01(\r\x12(\n\x04user\x18\x03 \x03(\x0b\x32\x1a.xray.common.protocol.UserB^\n\x18\x63om.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3')
+
+
+
+_SERVERENDPOINT = DESCRIPTOR.message_types_by_name['ServerEndpoint']
+ServerEndpoint = _reflection.GeneratedProtocolMessageType('ServerEndpoint', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERENDPOINT,
+ '__module__' : 'common.protocol.server_spec_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.protocol.ServerEndpoint)
+ })
+_sym_db.RegisterMessage(ServerEndpoint)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.common.protocolP\001Z)github.com/xtls/xray-core/common/protocol\252\002\024Xray.Common.Protocol'
+ _SERVERENDPOINT._serialized_start=113
+ _SERVERENDPOINT._serialized_end=231
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/protocol/user_grpc.py b/xray_api/proto/common/protocol/user_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/protocol/user.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/protocol/user_pb2.py b/xray_api/proto/common/protocol/user_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/protocol/user.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63ommon/protocol/user.proto\x12\x14xray.common.protocol\x1a!common/serial/typed_message.proto\"W\n\x04User\x12\r\n\x05level\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x31\n\x07\x61\x63\x63ount\x18\x03 \x01(\x0b\x32 .xray.common.serial.TypedMessageB^\n\x18\x63om.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3')
+
+
+
+_USER = DESCRIPTOR.message_types_by_name['User']
+User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), {
+ 'DESCRIPTOR' : _USER,
+ '__module__' : 'common.protocol.user_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.protocol.User)
+ })
+_sym_db.RegisterMessage(User)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.common.protocolP\001Z)github.com/xtls/xray-core/common/protocol\252\002\024Xray.Common.Protocol'
+ _USER._serialized_start=87
+ _USER._serialized_end=174
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/common/serial/typed_message_grpc.py b/xray_api/proto/common/serial/typed_message_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: common/serial/typed_message.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/common/serial/typed_message_pb2.py b/xray_api/proto/common/serial/typed_message_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: common/serial/typed_message.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!common/serial/typed_message.proto\x12\x12xray.common.serial\"+\n\x0cTypedMessage\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42X\n\x16\x63om.xray.common.serialP\x01Z\'github.com/xtls/xray-core/common/serial\xaa\x02\x12Xray.Common.Serialb\x06proto3')
+
+
+
+_TYPEDMESSAGE = DESCRIPTOR.message_types_by_name['TypedMessage']
+TypedMessage = _reflection.GeneratedProtocolMessageType('TypedMessage', (_message.Message,), {
+ 'DESCRIPTOR' : _TYPEDMESSAGE,
+ '__module__' : 'common.serial.typed_message_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.common.serial.TypedMessage)
+ })
+_sym_db.RegisterMessage(TypedMessage)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\026com.xray.common.serialP\001Z\'github.com/xtls/xray-core/common/serial\252\002\022Xray.Common.Serial'
+ _TYPEDMESSAGE._serialized_start=57
+ _TYPEDMESSAGE._serialized_end=100
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/compile.py b/xray_api/proto/compile.py
@@ -0,0 +1,98 @@
+# DO NOT RUN THIS SCRIPT
+# This supposed to be used for development
+
+
+import io
+import os
+import re
+import sys
+import tarfile
+import tempfile
+from pathlib import Path
+
+import requests
+from genericpath import isdir
+
+# Compile proto files from github repository
+
+
+def compile_proto_from_source(dist):
+ tmp = tempfile.TemporaryDirectory()
+ tmp_dir = tmp.name
+
+ # download and extract source
+ print("Getting latest version...")
+ latest = requests.get('https://api.github.com/repos/XTLS/xray-core/releases/latest') \
+ .json() \
+ .get('tag_name')
+ print("Latest version is", latest)
+
+ # version = 'v4.45.2'
+ version = latest
+
+ print("Downloading source", version, "...")
+ download_url = f'https://github.com/XTLS/xray-core/archive/refs/tags/{version}.tar.gz'
+ print("Source downloaded. extracting and compiling...")
+ r = requests.get(download_url, stream=True)
+ io_bytes = io.BytesIO(r.content)
+ tar = tarfile.open(fileobj=io_bytes, mode='r')
+ tar.extractall(tmp_dir)
+ v2ray_dir = os.path.join(tmp_dir, tar.getnames()[0])
+ tar.close()
+ io_bytes.close()
+
+ # find proto files
+ proto_files = ''
+ for root, dirs, files in os.walk(tmp_dir):
+ for file in files:
+ if file.endswith(".proto"):
+ proto_files += ' ' + os.path.join(root, file)
+
+ if not proto_files:
+ raise FileNotFoundError("there's no proto file.")
+
+ command = f'{sys.executable} -m grpc.tools.protoc ' \
+ f'-I={v2ray_dir} ' \
+ f'--python_out={dist} ' \
+ f'--grpclib_python_out={dist} ' + proto_files
+
+ os.system(command)
+
+ tmp.cleanup()
+ print("Compile ended.")
+
+
+def fix_proto_imports(dist):
+ curr_dir = Path(__file__).resolve().parent
+ parent_dir = curr_dir.parent
+
+ imports = []
+ for f in os.listdir('.'):
+ if os.path.isdir(f) and not f.startswith('__'):
+ imports.append(f)
+
+ new_imp = f'{parent_dir.name}.{curr_dir.name}.' + '{}'
+
+ for root, dirs, files in os.walk('.'):
+ for file in files:
+ if file == 'compile.py':
+ continue
+
+ if file.endswith(".py"):
+ path = Path(root) / file
+ with open(path, 'r') as pyfile:
+ content = pyfile.read()
+ for imp in imports:
+ content = re.sub(fr"^([^'\r\n]*){imp}([\s\.].*)$", fr"\1{parent_dir.name}.{curr_dir.name}.{imp}\2", content, flags=re.MULTILINE)
+ content = re.sub(fr"^(.*importlib\.import_module\(\'){imp}(.*)$", fr"\1{parent_dir.name}.{curr_dir.name}.{imp}\2", content, flags=re.MULTILINE)
+
+ with open(path, 'w') as pyfile:
+ pyfile.write(content)
+
+
+if __name__ == "__main__":
+ print("Compiling source...")
+ compile_proto_from_source('.')
+ print("Fixing imports...")
+ fix_proto_imports('.')
+ print("Done.")
diff --git a/xray_api/proto/core/config_grpc.py b/xray_api/proto/core/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: core/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/core/config_pb2.py b/xray_api/proto/core/config_pb2.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: core/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+import importlib
+transport_dot_global_dot_config__pb2 = importlib.import_module('xray_api.proto.transport.global.config_pb2')
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63ore/config.proto\x12\txray.core\x1a!common/serial/typed_message.proto\x1a\x1dtransport/global/config.proto\"\x87\x02\n\x06\x43onfig\x12\x30\n\x07inbound\x18\x01 \x03(\x0b\x32\x1f.xray.core.InboundHandlerConfig\x12\x32\n\x08outbound\x18\x02 \x03(\x0b\x32 .xray.core.OutboundHandlerConfig\x12-\n\x03\x61pp\x18\x04 \x03(\x0b\x32 .xray.common.serial.TypedMessage\x12-\n\ttransport\x18\x05 \x01(\x0b\x32\x16.xray.transport.ConfigB\x02\x18\x01\x12\x33\n\textension\x18\x06 \x03(\x0b\x32 .xray.common.serial.TypedMessageJ\x04\x08\x03\x10\x04\"\x9a\x01\n\x14InboundHandlerConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12;\n\x11receiver_settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x38\n\x0eproxy_settings\x18\x03 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\xba\x01\n\x15OutboundHandlerConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x39\n\x0fsender_settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x38\n\x0eproxy_settings\x18\x03 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x0e\n\x06\x65xpire\x18\x04 \x01(\x03\x12\x0f\n\x07\x63omment\x18\x05 \x01(\tB=\n\rcom.xray.coreP\x01Z\x1egithub.com/xtls/xray-core/core\xaa\x02\tXray.Coreb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_INBOUNDHANDLERCONFIG = DESCRIPTOR.message_types_by_name['InboundHandlerConfig']
+_OUTBOUNDHANDLERCONFIG = DESCRIPTOR.message_types_by_name['OutboundHandlerConfig']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'core.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+InboundHandlerConfig = _reflection.GeneratedProtocolMessageType('InboundHandlerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _INBOUNDHANDLERCONFIG,
+ '__module__' : 'core.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.InboundHandlerConfig)
+ })
+_sym_db.RegisterMessage(InboundHandlerConfig)
+
+OutboundHandlerConfig = _reflection.GeneratedProtocolMessageType('OutboundHandlerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _OUTBOUNDHANDLERCONFIG,
+ '__module__' : 'core.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.core.OutboundHandlerConfig)
+ })
+_sym_db.RegisterMessage(OutboundHandlerConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\rcom.xray.coreP\001Z\036github.com/xtls/xray-core/core\252\002\tXray.Core'
+ _CONFIG.fields_by_name['transport']._options = None
+ _CONFIG.fields_by_name['transport']._serialized_options = b'\030\001'
+ _CONFIG._serialized_start=99
+ _CONFIG._serialized_end=362
+ _INBOUNDHANDLERCONFIG._serialized_start=365
+ _INBOUNDHANDLERCONFIG._serialized_end=519
+ _OUTBOUNDHANDLERCONFIG._serialized_start=522
+ _OUTBOUNDHANDLERCONFIG._serialized_end=708
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/blackhole/config_grpc.py b/xray_api/proto/proxy/blackhole/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/blackhole/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/blackhole/config_pb2.py b/xray_api/proto/proxy/blackhole/config_pb2.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/blackhole/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n\x0cNoneResponse\"\x0e\n\x0cHTTPResponse\"<\n\x06\x43onfig\x12\x32\n\x08response\x18\x01 \x01(\x0b\x32 .xray.common.serial.TypedMessageB^\n\x18\x63om.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3')
+
+
+
+_NONERESPONSE = DESCRIPTOR.message_types_by_name['NoneResponse']
+_HTTPRESPONSE = DESCRIPTOR.message_types_by_name['HTTPResponse']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+NoneResponse = _reflection.GeneratedProtocolMessageType('NoneResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _NONERESPONSE,
+ '__module__' : 'proxy.blackhole.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.blackhole.NoneResponse)
+ })
+_sym_db.RegisterMessage(NoneResponse)
+
+HTTPResponse = _reflection.GeneratedProtocolMessageType('HTTPResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _HTTPRESPONSE,
+ '__module__' : 'proxy.blackhole.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.blackhole.HTTPResponse)
+ })
+_sym_db.RegisterMessage(HTTPResponse)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.blackhole.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.blackhole.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.proxy.blackholeP\001Z)github.com/xtls/xray-core/proxy/blackhole\252\002\024Xray.Proxy.Blackhole'
+ _NONERESPONSE._serialized_start=89
+ _NONERESPONSE._serialized_end=103
+ _HTTPRESPONSE._serialized_start=105
+ _HTTPRESPONSE._serialized_end=119
+ _CONFIG._serialized_start=121
+ _CONFIG._serialized_end=181
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/dns/config_grpc.py b/xray_api/proto/proxy/dns/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/dns/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/dns/config_pb2.py b/xray_api/proto/proxy/dns/config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/dns/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import destination_pb2 as common_dot_net_dot_destination__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1c\x63ommon/net/destination.proto\"]\n\x06\x43onfig\x12)\n\x06server\x18\x01 \x01(\x0b\x32\x19.xray.common.net.Endpoint\x12\x12\n\nuser_level\x18\x02 \x01(\r\x12\x14\n\x0cnon_IP_query\x18\x03 \x01(\tBL\n\x12\x63om.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.dns.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\022com.xray.proxy.dnsP\001Z#github.com/xtls/xray-core/proxy/dns\252\002\016Xray.Proxy.Dns'
+ _CONFIG._serialized_start=72
+ _CONFIG._serialized_end=165
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/dokodemo/config_grpc.py b/xray_api/proto/proxy/dokodemo/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/dokodemo/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/dokodemo/config_pb2.py b/xray_api/proto/proxy/dokodemo/config_pb2.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/dokodemo/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bproxy/dokodemo/config.proto\x12\x13xray.proxy.dokodemo\x1a\x18\x63ommon/net/address.proto\x1a\x18\x63ommon/net/network.proto\"\xea\x01\n\x06\x43onfig\x12,\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x36\n\x0cnetwork_list\x18\x03 \x01(\x0b\x32\x1c.xray.common.net.NetworkListB\x02\x18\x01\x12*\n\x08networks\x18\x07 \x03(\x0e\x32\x18.xray.common.net.Network\x12\x13\n\x07timeout\x18\x04 \x01(\rB\x02\x18\x01\x12\x17\n\x0f\x66ollow_redirect\x18\x05 \x01(\x08\x12\x12\n\nuser_level\x18\x06 \x01(\rB[\n\x17\x63om.xray.proxy.dokodemoP\x01Z(github.com/xtls/xray-core/proxy/dokodemo\xaa\x02\x13Xray.Proxy.Dokodemob\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.dokodemo.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.dokodemo.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\027com.xray.proxy.dokodemoP\001Z(github.com/xtls/xray-core/proxy/dokodemo\252\002\023Xray.Proxy.Dokodemo'
+ _CONFIG.fields_by_name['network_list']._options = None
+ _CONFIG.fields_by_name['network_list']._serialized_options = b'\030\001'
+ _CONFIG.fields_by_name['timeout']._options = None
+ _CONFIG.fields_by_name['timeout']._serialized_options = b'\030\001'
+ _CONFIG._serialized_start=105
+ _CONFIG._serialized_end=339
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/freedom/config_grpc.py b/xray_api/proto/proxy/freedom/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/freedom/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/freedom/config_pb2.py b/xray_api/proto/proxy/freedom/config_pb2.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/freedom/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\"K\n\x13\x44\x65stinationOverride\x12\x34\n\x06server\x18\x01 \x01(\x0b\x32$.xray.common.protocol.ServerEndpoint\"\x88\x01\n\x08\x46ragment\x12\x14\n\x0cpackets_from\x18\x01 \x01(\x04\x12\x12\n\npackets_to\x18\x02 \x01(\x04\x12\x12\n\nlength_min\x18\x03 \x01(\x04\x12\x12\n\nlength_max\x18\x04 \x01(\x04\x12\x14\n\x0cinterval_min\x18\x05 \x01(\x04\x12\x14\n\x0cinterval_max\x18\x06 \x01(\x04\"\x98\x03\n\x06\x43onfig\x12\x42\n\x0f\x64omain_strategy\x18\x01 \x01(\x0e\x32).xray.proxy.freedom.Config.DomainStrategy\x12\x13\n\x07timeout\x18\x02 \x01(\rB\x02\x18\x01\x12\x45\n\x14\x64\x65stination_override\x18\x03 \x01(\x0b\x32\'.xray.proxy.freedom.DestinationOverride\x12\x12\n\nuser_level\x18\x04 \x01(\r\x12.\n\x08\x66ragment\x18\x05 \x01(\x0b\x32\x1c.xray.proxy.freedom.Fragment\"\xa9\x01\n\x0e\x44omainStrategy\x12\t\n\x05\x41S_IS\x10\x00\x12\n\n\x06USE_IP\x10\x01\x12\x0b\n\x07USE_IP4\x10\x02\x12\x0b\n\x07USE_IP6\x10\x03\x12\x0c\n\x08USE_IP46\x10\x04\x12\x0c\n\x08USE_IP64\x10\x05\x12\x0c\n\x08\x46ORCE_IP\x10\x06\x12\r\n\tFORCE_IP4\x10\x07\x12\r\n\tFORCE_IP6\x10\x08\x12\x0e\n\nFORCE_IP46\x10\t\x12\x0e\n\nFORCE_IP64\x10\nBX\n\x16\x63om.xray.proxy.freedomP\x01Z\'github.com/xtls/xray-core/proxy/freedom\xaa\x02\x12Xray.Proxy.Freedomb\x06proto3')
+
+
+
+_DESTINATIONOVERRIDE = DESCRIPTOR.message_types_by_name['DestinationOverride']
+_FRAGMENT = DESCRIPTOR.message_types_by_name['Fragment']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONFIG_DOMAINSTRATEGY = _CONFIG.enum_types_by_name['DomainStrategy']
+DestinationOverride = _reflection.GeneratedProtocolMessageType('DestinationOverride', (_message.Message,), {
+ 'DESCRIPTOR' : _DESTINATIONOVERRIDE,
+ '__module__' : 'proxy.freedom.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.freedom.DestinationOverride)
+ })
+_sym_db.RegisterMessage(DestinationOverride)
+
+Fragment = _reflection.GeneratedProtocolMessageType('Fragment', (_message.Message,), {
+ 'DESCRIPTOR' : _FRAGMENT,
+ '__module__' : 'proxy.freedom.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.freedom.Fragment)
+ })
+_sym_db.RegisterMessage(Fragment)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.freedom.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.freedom.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\026com.xray.proxy.freedomP\001Z\'github.com/xtls/xray-core/proxy/freedom\252\002\022Xray.Proxy.Freedom'
+ _CONFIG.fields_by_name['timeout']._options = None
+ _CONFIG.fields_by_name['timeout']._serialized_options = b'\030\001'
+ _DESTINATIONOVERRIDE._serialized_start=85
+ _DESTINATIONOVERRIDE._serialized_end=160
+ _FRAGMENT._serialized_start=163
+ _FRAGMENT._serialized_end=299
+ _CONFIG._serialized_start=302
+ _CONFIG._serialized_end=710
+ _CONFIG_DOMAINSTRATEGY._serialized_start=541
+ _CONFIG_DOMAINSTRATEGY._serialized_end=710
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/http/config_grpc.py b/xray_api/proto/proxy/http/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/http/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/http/config_pb2.py b/xray_api/proto/proxy/http/config_pb2.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/http/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17proxy/http/config.proto\x12\x0fxray.proxy.http\x1a!common/protocol/server_spec.proto\"-\n\x07\x41\x63\x63ount\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"\xc2\x01\n\x0cServerConfig\x12\x13\n\x07timeout\x18\x01 \x01(\rB\x02\x18\x01\x12=\n\x08\x61\x63\x63ounts\x18\x02 \x03(\x0b\x32+.xray.proxy.http.ServerConfig.AccountsEntry\x12\x19\n\x11\x61llow_transparent\x18\x03 \x01(\x08\x12\x12\n\nuser_level\x18\x04 \x01(\r\x1a/\n\rAccountsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"m\n\x0c\x43lientConfig\x12\x34\n\x06server\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpoint\x12\'\n\x06header\x18\x02 \x03(\x0b\x32\x17.xray.proxy.http.HeaderBO\n\x13\x63om.xray.proxy.httpP\x01Z$github.com/xtls/xray-core/proxy/http\xaa\x02\x0fXray.Proxy.Httpb\x06proto3')
+
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+_SERVERCONFIG = DESCRIPTOR.message_types_by_name['ServerConfig']
+_SERVERCONFIG_ACCOUNTSENTRY = _SERVERCONFIG.nested_types_by_name['AccountsEntry']
+_HEADER = DESCRIPTOR.message_types_by_name['Header']
+_CLIENTCONFIG = DESCRIPTOR.message_types_by_name['ClientConfig']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.http.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+ServerConfig = _reflection.GeneratedProtocolMessageType('ServerConfig', (_message.Message,), {
+
+ 'AccountsEntry' : _reflection.GeneratedProtocolMessageType('AccountsEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERCONFIG_ACCOUNTSENTRY,
+ '__module__' : 'proxy.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.http.ServerConfig.AccountsEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _SERVERCONFIG,
+ '__module__' : 'proxy.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.http.ServerConfig)
+ })
+_sym_db.RegisterMessage(ServerConfig)
+_sym_db.RegisterMessage(ServerConfig.AccountsEntry)
+
+Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
+ 'DESCRIPTOR' : _HEADER,
+ '__module__' : 'proxy.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.http.Header)
+ })
+_sym_db.RegisterMessage(Header)
+
+ClientConfig = _reflection.GeneratedProtocolMessageType('ClientConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCONFIG,
+ '__module__' : 'proxy.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.http.ClientConfig)
+ })
+_sym_db.RegisterMessage(ClientConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\023com.xray.proxy.httpP\001Z$github.com/xtls/xray-core/proxy/http\252\002\017Xray.Proxy.Http'
+ _SERVERCONFIG_ACCOUNTSENTRY._options = None
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_options = b'8\001'
+ _SERVERCONFIG.fields_by_name['timeout']._options = None
+ _SERVERCONFIG.fields_by_name['timeout']._serialized_options = b'\030\001'
+ _ACCOUNT._serialized_start=79
+ _ACCOUNT._serialized_end=124
+ _SERVERCONFIG._serialized_start=127
+ _SERVERCONFIG._serialized_end=321
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_start=274
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_end=321
+ _HEADER._serialized_start=323
+ _HEADER._serialized_end=359
+ _CLIENTCONFIG._serialized_start=361
+ _CLIENTCONFIG._serialized_end=470
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/loopback/config_grpc.py b/xray_api/proto/proxy/loopback/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/loopback/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/loopback/config_pb2.py b/xray_api/proto/proxy/loopback/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/loopback/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bproxy/loopback/config.proto\x12\x13xray.proxy.loopback\"\x1d\n\x06\x43onfig\x12\x13\n\x0binbound_tag\x18\x01 \x01(\tB[\n\x17\x63om.xray.proxy.loopbackP\x01Z(github.com/xtls/xray-core/proxy/loopback\xaa\x02\x13Xray.Proxy.Loopbackb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.loopback.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.loopback.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\027com.xray.proxy.loopbackP\001Z(github.com/xtls/xray-core/proxy/loopback\252\002\023Xray.Proxy.Loopback'
+ _CONFIG._serialized_start=52
+ _CONFIG._serialized_end=81
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/shadowsocks/config_grpc.py b/xray_api/proto/proxy/shadowsocks/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/shadowsocks/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/shadowsocks/config_pb2.py b/xray_api/proto/proxy/shadowsocks/config_pb2.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/shadowsocks/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1eproxy/shadowsocks/config.proto\x12\x16xray.proxy.shadowsocks\x1a\x18\x63ommon/net/network.proto\x1a\x1a\x63ommon/protocol/user.proto\x1a!common/protocol/server_spec.proto\"f\n\x07\x41\x63\x63ount\x12\x10\n\x08password\x18\x01 \x01(\t\x12\x37\n\x0b\x63ipher_type\x18\x02 \x01(\x0e\x32\".xray.proxy.shadowsocks.CipherType\x12\x10\n\x08iv_check\x18\x03 \x01(\x08\"d\n\x0cServerConfig\x12)\n\x05users\x18\x01 \x03(\x0b\x32\x1a.xray.common.protocol.User\x12)\n\x07network\x18\x02 \x03(\x0e\x32\x18.xray.common.net.Network\"D\n\x0c\x43lientConfig\x12\x34\n\x06server\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpoint*t\n\nCipherType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x41\x45S_128_GCM\x10\x05\x12\x0f\n\x0b\x41\x45S_256_GCM\x10\x06\x12\x15\n\x11\x43HACHA20_POLY1305\x10\x07\x12\x16\n\x12XCHACHA20_POLY1305\x10\x08\x12\x08\n\x04NONE\x10\tBd\n\x1a\x63om.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3')
+
+_CIPHERTYPE = DESCRIPTOR.enum_types_by_name['CipherType']
+CipherType = enum_type_wrapper.EnumTypeWrapper(_CIPHERTYPE)
+UNKNOWN = 0
+AES_128_GCM = 5
+AES_256_GCM = 6
+CHACHA20_POLY1305 = 7
+XCHACHA20_POLY1305 = 8
+NONE = 9
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+_SERVERCONFIG = DESCRIPTOR.message_types_by_name['ServerConfig']
+_CLIENTCONFIG = DESCRIPTOR.message_types_by_name['ClientConfig']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.shadowsocks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+ServerConfig = _reflection.GeneratedProtocolMessageType('ServerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERCONFIG,
+ '__module__' : 'proxy.shadowsocks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks.ServerConfig)
+ })
+_sym_db.RegisterMessage(ServerConfig)
+
+ClientConfig = _reflection.GeneratedProtocolMessageType('ClientConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCONFIG,
+ '__module__' : 'proxy.shadowsocks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks.ClientConfig)
+ })
+_sym_db.RegisterMessage(ClientConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\032com.xray.proxy.shadowsocksP\001Z+github.com/xtls/xray-core/proxy/shadowsocks\252\002\026Xray.Proxy.Shadowsocks'
+ _CIPHERTYPE._serialized_start=423
+ _CIPHERTYPE._serialized_end=539
+ _ACCOUNT._serialized_start=147
+ _ACCOUNT._serialized_end=249
+ _SERVERCONFIG._serialized_start=251
+ _SERVERCONFIG._serialized_end=351
+ _CLIENTCONFIG._serialized_start=353
+ _CLIENTCONFIG._serialized_end=421
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/shadowsocks_2022/config_grpc.py b/xray_api/proto/proxy/shadowsocks_2022/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/shadowsocks_2022/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/shadowsocks_2022/config_pb2.py b/xray_api/proto/proxy/shadowsocks_2022/config_pb2.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/shadowsocks_2022/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import network_pb2 as common_dot_net_dot_network__pb2
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#proxy/shadowsocks_2022/config.proto\x12\x1bxray.proxy.shadowsocks_2022\x1a\x18\x63ommon/net/network.proto\x1a\x18\x63ommon/net/address.proto\"t\n\x0cServerConfig\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\r\n\x05level\x18\x04 \x01(\x05\x12)\n\x07network\x18\x05 \x03(\x0e\x32\x18.xray.common.net.Network\"\x91\x01\n\x15MultiUserServerConfig\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x30\n\x05users\x18\x03 \x03(\x0b\x32!.xray.proxy.shadowsocks_2022.User\x12)\n\x07network\x18\x04 \x03(\x0e\x32\x18.xray.common.net.Network\"y\n\x10RelayDestination\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x07\x61\x64\x64ress\x18\x02 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\r\n\x05level\x18\x05 \x01(\x05\"\xa0\x01\n\x11RelayServerConfig\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x43\n\x0c\x64\x65stinations\x18\x03 \x03(\x0b\x32-.xray.proxy.shadowsocks_2022.RelayDestination\x12)\n\x07network\x18\x04 \x03(\x0e\x32\x18.xray.common.net.Network\"1\n\x04User\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\r\n\x05level\x18\x03 \x01(\x05\"\x9b\x01\n\x0c\x43lientConfig\x12,\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x0e\n\x06method\x18\x03 \x01(\t\x12\x0b\n\x03key\x18\x04 \x01(\t\x12\x14\n\x0cudp_over_tcp\x18\x05 \x01(\x08\x12\x1c\n\x14udp_over_tcp_version\x18\x06 \x01(\rBr\n\x1f\x63om.xray.proxy.shadowsocks_2022P\x01Z0github.com/xtls/xray-core/proxy/shadowsocks_2022\xaa\x02\x1aXray.Proxy.Shadowsocks2022b\x06proto3')
+
+
+
+_SERVERCONFIG = DESCRIPTOR.message_types_by_name['ServerConfig']
+_MULTIUSERSERVERCONFIG = DESCRIPTOR.message_types_by_name['MultiUserServerConfig']
+_RELAYDESTINATION = DESCRIPTOR.message_types_by_name['RelayDestination']
+_RELAYSERVERCONFIG = DESCRIPTOR.message_types_by_name['RelayServerConfig']
+_USER = DESCRIPTOR.message_types_by_name['User']
+_CLIENTCONFIG = DESCRIPTOR.message_types_by_name['ClientConfig']
+ServerConfig = _reflection.GeneratedProtocolMessageType('ServerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERCONFIG,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.ServerConfig)
+ })
+_sym_db.RegisterMessage(ServerConfig)
+
+MultiUserServerConfig = _reflection.GeneratedProtocolMessageType('MultiUserServerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _MULTIUSERSERVERCONFIG,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.MultiUserServerConfig)
+ })
+_sym_db.RegisterMessage(MultiUserServerConfig)
+
+RelayDestination = _reflection.GeneratedProtocolMessageType('RelayDestination', (_message.Message,), {
+ 'DESCRIPTOR' : _RELAYDESTINATION,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.RelayDestination)
+ })
+_sym_db.RegisterMessage(RelayDestination)
+
+RelayServerConfig = _reflection.GeneratedProtocolMessageType('RelayServerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _RELAYSERVERCONFIG,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.RelayServerConfig)
+ })
+_sym_db.RegisterMessage(RelayServerConfig)
+
+User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), {
+ 'DESCRIPTOR' : _USER,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.User)
+ })
+_sym_db.RegisterMessage(User)
+
+ClientConfig = _reflection.GeneratedProtocolMessageType('ClientConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCONFIG,
+ '__module__' : 'proxy.shadowsocks_2022.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.shadowsocks_2022.ClientConfig)
+ })
+_sym_db.RegisterMessage(ClientConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\037com.xray.proxy.shadowsocks_2022P\001Z0github.com/xtls/xray-core/proxy/shadowsocks_2022\252\002\032Xray.Proxy.Shadowsocks2022'
+ _SERVERCONFIG._serialized_start=120
+ _SERVERCONFIG._serialized_end=236
+ _MULTIUSERSERVERCONFIG._serialized_start=239
+ _MULTIUSERSERVERCONFIG._serialized_end=384
+ _RELAYDESTINATION._serialized_start=386
+ _RELAYDESTINATION._serialized_end=507
+ _RELAYSERVERCONFIG._serialized_start=510
+ _RELAYSERVERCONFIG._serialized_end=670
+ _USER._serialized_start=672
+ _USER._serialized_end=721
+ _CLIENTCONFIG._serialized_start=724
+ _CLIENTCONFIG._serialized_end=879
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/socks/config_grpc.py b/xray_api/proto/proxy/socks/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/socks/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/socks/config_pb2.py b/xray_api/proto/proxy/socks/config_pb2.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/socks/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.net import address_pb2 as common_dot_net_dot_address__pb2
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18proxy/socks/config.proto\x12\x10xray.proxy.socks\x1a\x18\x63ommon/net/address.proto\x1a!common/protocol/server_spec.proto\"-\n\x07\x41\x63\x63ount\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"\x9a\x02\n\x0cServerConfig\x12-\n\tauth_type\x18\x01 \x01(\x0e\x32\x1a.xray.proxy.socks.AuthType\x12>\n\x08\x61\x63\x63ounts\x18\x02 \x03(\x0b\x32,.xray.proxy.socks.ServerConfig.AccountsEntry\x12,\n\x07\x61\x64\x64ress\x18\x03 \x01(\x0b\x32\x1b.xray.common.net.IPOrDomain\x12\x13\n\x0budp_enabled\x18\x04 \x01(\x08\x12\x13\n\x07timeout\x18\x05 \x01(\rB\x02\x18\x01\x12\x12\n\nuser_level\x18\x06 \x01(\r\x1a/\n\rAccountsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"p\n\x0c\x43lientConfig\x12\x34\n\x06server\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpoint\x12*\n\x07version\x18\x02 \x01(\x0e\x32\x19.xray.proxy.socks.Version*%\n\x08\x41uthType\x12\x0b\n\x07NO_AUTH\x10\x00\x12\x0c\n\x08PASSWORD\x10\x01*.\n\x07Version\x12\n\n\x06SOCKS5\x10\x00\x12\n\n\x06SOCKS4\x10\x01\x12\x0b\n\x07SOCKS4A\x10\x02\x42R\n\x14\x63om.xray.proxy.socksP\x01Z%github.com/xtls/xray-core/proxy/socks\xaa\x02\x10Xray.Proxy.Socksb\x06proto3')
+
+_AUTHTYPE = DESCRIPTOR.enum_types_by_name['AuthType']
+AuthType = enum_type_wrapper.EnumTypeWrapper(_AUTHTYPE)
+_VERSION = DESCRIPTOR.enum_types_by_name['Version']
+Version = enum_type_wrapper.EnumTypeWrapper(_VERSION)
+NO_AUTH = 0
+PASSWORD = 1
+SOCKS5 = 0
+SOCKS4 = 1
+SOCKS4A = 2
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+_SERVERCONFIG = DESCRIPTOR.message_types_by_name['ServerConfig']
+_SERVERCONFIG_ACCOUNTSENTRY = _SERVERCONFIG.nested_types_by_name['AccountsEntry']
+_CLIENTCONFIG = DESCRIPTOR.message_types_by_name['ClientConfig']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.socks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.socks.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+ServerConfig = _reflection.GeneratedProtocolMessageType('ServerConfig', (_message.Message,), {
+
+ 'AccountsEntry' : _reflection.GeneratedProtocolMessageType('AccountsEntry', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERCONFIG_ACCOUNTSENTRY,
+ '__module__' : 'proxy.socks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.socks.ServerConfig.AccountsEntry)
+ })
+ ,
+ 'DESCRIPTOR' : _SERVERCONFIG,
+ '__module__' : 'proxy.socks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.socks.ServerConfig)
+ })
+_sym_db.RegisterMessage(ServerConfig)
+_sym_db.RegisterMessage(ServerConfig.AccountsEntry)
+
+ClientConfig = _reflection.GeneratedProtocolMessageType('ClientConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCONFIG,
+ '__module__' : 'proxy.socks.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.socks.ClientConfig)
+ })
+_sym_db.RegisterMessage(ClientConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\024com.xray.proxy.socksP\001Z%github.com/xtls/xray-core/proxy/socks\252\002\020Xray.Proxy.Socks'
+ _SERVERCONFIG_ACCOUNTSENTRY._options = None
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_options = b'8\001'
+ _SERVERCONFIG.fields_by_name['timeout']._options = None
+ _SERVERCONFIG.fields_by_name['timeout']._serialized_options = b'\030\001'
+ _AUTHTYPE._serialized_start=553
+ _AUTHTYPE._serialized_end=590
+ _VERSION._serialized_start=592
+ _VERSION._serialized_end=638
+ _ACCOUNT._serialized_start=107
+ _ACCOUNT._serialized_end=152
+ _SERVERCONFIG._serialized_start=155
+ _SERVERCONFIG._serialized_end=437
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_start=390
+ _SERVERCONFIG_ACCOUNTSENTRY._serialized_end=437
+ _CLIENTCONFIG._serialized_start=439
+ _CLIENTCONFIG._serialized_end=551
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/trojan/config_grpc.py b/xray_api/proto/proxy/trojan/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/trojan/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/trojan/config_pb2.py b/xray_api/proto/proxy/trojan/config_pb2.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/trojan/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19proxy/trojan/config.proto\x12\x11xray.proxy.trojan\x1a\x1a\x63ommon/protocol/user.proto\x1a!common/protocol/server_spec.proto\"\x1b\n\x07\x41\x63\x63ount\x12\x10\n\x08password\x18\x01 \x01(\t\"^\n\x08\x46\x61llback\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61lpn\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x0c\n\x04\x64\x65st\x18\x05 \x01(\t\x12\x0c\n\x04xver\x18\x06 \x01(\x04\"D\n\x0c\x43lientConfig\x12\x34\n\x06server\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpoint\"i\n\x0cServerConfig\x12)\n\x05users\x18\x01 \x03(\x0b\x32\x1a.xray.common.protocol.User\x12.\n\tfallbacks\x18\x02 \x03(\x0b\x32\x1b.xray.proxy.trojan.FallbackBU\n\x15\x63om.xray.proxy.trojanP\x01Z&github.com/xtls/xray-core/proxy/trojan\xaa\x02\x11Xray.Proxy.Trojanb\x06proto3')
+
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+_FALLBACK = DESCRIPTOR.message_types_by_name['Fallback']
+_CLIENTCONFIG = DESCRIPTOR.message_types_by_name['ClientConfig']
+_SERVERCONFIG = DESCRIPTOR.message_types_by_name['ServerConfig']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.trojan.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.trojan.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+Fallback = _reflection.GeneratedProtocolMessageType('Fallback', (_message.Message,), {
+ 'DESCRIPTOR' : _FALLBACK,
+ '__module__' : 'proxy.trojan.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.trojan.Fallback)
+ })
+_sym_db.RegisterMessage(Fallback)
+
+ClientConfig = _reflection.GeneratedProtocolMessageType('ClientConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCONFIG,
+ '__module__' : 'proxy.trojan.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.trojan.ClientConfig)
+ })
+_sym_db.RegisterMessage(ClientConfig)
+
+ServerConfig = _reflection.GeneratedProtocolMessageType('ServerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SERVERCONFIG,
+ '__module__' : 'proxy.trojan.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.trojan.ServerConfig)
+ })
+_sym_db.RegisterMessage(ServerConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\025com.xray.proxy.trojanP\001Z&github.com/xtls/xray-core/proxy/trojan\252\002\021Xray.Proxy.Trojan'
+ _ACCOUNT._serialized_start=111
+ _ACCOUNT._serialized_end=138
+ _FALLBACK._serialized_start=140
+ _FALLBACK._serialized_end=234
+ _CLIENTCONFIG._serialized_start=236
+ _CLIENTCONFIG._serialized_end=304
+ _SERVERCONFIG._serialized_start=306
+ _SERVERCONFIG._serialized_end=411
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vless/account_grpc.py b/xray_api/proto/proxy/vless/account_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vless/account.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vless/account_pb2.py b/xray_api/proto/proxy/vless/account_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vless/account.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19proxy/vless/account.proto\x12\x10xray.proxy.vless\"7\n\x07\x41\x63\x63ount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x66low\x18\x02 \x01(\t\x12\x12\n\nencryption\x18\x03 \x01(\tBR\n\x14\x63om.xray.proxy.vlessP\x01Z%github.com/xtls/xray-core/proxy/vless\xaa\x02\x10Xray.Proxy.Vlessb\x06proto3')
+
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.vless.account_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vless.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\024com.xray.proxy.vlessP\001Z%github.com/xtls/xray-core/proxy/vless\252\002\020Xray.Proxy.Vless'
+ _ACCOUNT._serialized_start=47
+ _ACCOUNT._serialized_end=102
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vless/encoding/addons_grpc.py b/xray_api/proto/proxy/vless/encoding/addons_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vless/encoding/addons.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vless/encoding/addons_pb2.py b/xray_api/proto/proxy/vless/encoding/addons_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vless/encoding/addons.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!proxy/vless/encoding/addons.proto\x12\x19xray.proxy.vless.encoding\"$\n\x06\x41\x64\x64ons\x12\x0c\n\x04\x46low\x18\x01 \x01(\t\x12\x0c\n\x04Seed\x18\x02 \x01(\x0c\x42m\n\x1d\x63om.xray.proxy.vless.encodingP\x01Z.github.com/xtls/xray-core/proxy/vless/encoding\xaa\x02\x19Xray.Proxy.Vless.Encodingb\x06proto3')
+
+
+
+_ADDONS = DESCRIPTOR.message_types_by_name['Addons']
+Addons = _reflection.GeneratedProtocolMessageType('Addons', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDONS,
+ '__module__' : 'proxy.vless.encoding.addons_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vless.encoding.Addons)
+ })
+_sym_db.RegisterMessage(Addons)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\035com.xray.proxy.vless.encodingP\001Z.github.com/xtls/xray-core/proxy/vless/encoding\252\002\031Xray.Proxy.Vless.Encoding'
+ _ADDONS._serialized_start=64
+ _ADDONS._serialized_end=100
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vless/inbound/config_grpc.py b/xray_api/proto/proxy/vless/inbound/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vless/inbound/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vless/inbound/config_pb2.py b/xray_api/proto/proxy/vless/inbound/config_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vless/inbound/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n proxy/vless/inbound/config.proto\x12\x18xray.proxy.vless.inbound\x1a\x1a\x63ommon/protocol/user.proto\"^\n\x08\x46\x61llback\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61lpn\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x0c\n\x04\x64\x65st\x18\x05 \x01(\t\x12\x0c\n\x04xver\x18\x06 \x01(\x04\"\x80\x01\n\x06\x43onfig\x12+\n\x07\x63lients\x18\x01 \x03(\x0b\x32\x1a.xray.common.protocol.User\x12\x12\n\ndecryption\x18\x02 \x01(\t\x12\x35\n\tfallbacks\x18\x03 \x03(\x0b\x32\".xray.proxy.vless.inbound.FallbackBj\n\x1c\x63om.xray.proxy.vless.inboundP\x01Z-github.com/xtls/xray-core/proxy/vless/inbound\xaa\x02\x18Xray.Proxy.Vless.Inboundb\x06proto3')
+
+
+
+_FALLBACK = DESCRIPTOR.message_types_by_name['Fallback']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Fallback = _reflection.GeneratedProtocolMessageType('Fallback', (_message.Message,), {
+ 'DESCRIPTOR' : _FALLBACK,
+ '__module__' : 'proxy.vless.inbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vless.inbound.Fallback)
+ })
+_sym_db.RegisterMessage(Fallback)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.vless.inbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vless.inbound.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\034com.xray.proxy.vless.inboundP\001Z-github.com/xtls/xray-core/proxy/vless/inbound\252\002\030Xray.Proxy.Vless.Inbound'
+ _FALLBACK._serialized_start=90
+ _FALLBACK._serialized_end=184
+ _CONFIG._serialized_start=187
+ _CONFIG._serialized_end=315
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vless/outbound/config_grpc.py b/xray_api/proto/proxy/vless/outbound/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vless/outbound/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vless/outbound/config_pb2.py b/xray_api/proto/proxy/vless/outbound/config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vless/outbound/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!proxy/vless/outbound/config.proto\x12\x19xray.proxy.vless.outbound\x1a!common/protocol/server_spec.proto\"=\n\x06\x43onfig\x12\x33\n\x05vnext\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpointBm\n\x1d\x63om.xray.proxy.vless.outboundP\x01Z.github.com/xtls/xray-core/proxy/vless/outbound\xaa\x02\x19Xray.Proxy.Vless.Outboundb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.vless.outbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vless.outbound.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\035com.xray.proxy.vless.outboundP\001Z.github.com/xtls/xray-core/proxy/vless/outbound\252\002\031Xray.Proxy.Vless.Outbound'
+ _CONFIG._serialized_start=99
+ _CONFIG._serialized_end=160
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vmess/account_grpc.py b/xray_api/proto/proxy/vmess/account_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vmess/account.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vmess/account_pb2.py b/xray_api/proto/proxy/vmess/account_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vmess/account.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import headers_pb2 as common_dot_protocol_dot_headers__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19proxy/vmess/account.proto\x12\x10xray.proxy.vmess\x1a\x1d\x63ommon/protocol/headers.proto\"m\n\x07\x41\x63\x63ount\x12\n\n\x02id\x18\x01 \x01(\t\x12?\n\x11security_settings\x18\x03 \x01(\x0b\x32$.xray.common.protocol.SecurityConfig\x12\x15\n\rtests_enabled\x18\x04 \x01(\tBR\n\x14\x63om.xray.proxy.vmessP\x01Z%github.com/xtls/xray-core/proxy/vmess\xaa\x02\x10Xray.Proxy.Vmessb\x06proto3')
+
+
+
+_ACCOUNT = DESCRIPTOR.message_types_by_name['Account']
+Account = _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
+ 'DESCRIPTOR' : _ACCOUNT,
+ '__module__' : 'proxy.vmess.account_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vmess.Account)
+ })
+_sym_db.RegisterMessage(Account)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\024com.xray.proxy.vmessP\001Z%github.com/xtls/xray-core/proxy/vmess\252\002\020Xray.Proxy.Vmess'
+ _ACCOUNT._serialized_start=78
+ _ACCOUNT._serialized_end=187
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vmess/inbound/config_grpc.py b/xray_api/proto/proxy/vmess/inbound/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vmess/inbound/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vmess/inbound/config_pb2.py b/xray_api/proto/proxy/vmess/inbound/config_pb2.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vmess/inbound/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import user_pb2 as common_dot_protocol_dot_user__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n proxy/vmess/inbound/config.proto\x12\x18xray.proxy.vmess.inbound\x1a\x1a\x63ommon/protocol/user.proto\"\x1a\n\x0c\x44\x65tourConfig\x12\n\n\x02to\x18\x01 \x01(\t\"\x1e\n\rDefaultConfig\x12\r\n\x05level\x18\x02 \x01(\r\"\xa4\x01\n\x06\x43onfig\x12(\n\x04user\x18\x01 \x03(\x0b\x32\x1a.xray.common.protocol.User\x12\x38\n\x07\x64\x65\x66\x61ult\x18\x02 \x01(\x0b\x32\'.xray.proxy.vmess.inbound.DefaultConfig\x12\x36\n\x06\x64\x65tour\x18\x03 \x01(\x0b\x32&.xray.proxy.vmess.inbound.DetourConfigBj\n\x1c\x63om.xray.proxy.vmess.inboundP\x01Z-github.com/xtls/xray-core/proxy/vmess/inbound\xaa\x02\x18Xray.Proxy.Vmess.Inboundb\x06proto3')
+
+
+
+_DETOURCONFIG = DESCRIPTOR.message_types_by_name['DetourConfig']
+_DEFAULTCONFIG = DESCRIPTOR.message_types_by_name['DefaultConfig']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+DetourConfig = _reflection.GeneratedProtocolMessageType('DetourConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _DETOURCONFIG,
+ '__module__' : 'proxy.vmess.inbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vmess.inbound.DetourConfig)
+ })
+_sym_db.RegisterMessage(DetourConfig)
+
+DefaultConfig = _reflection.GeneratedProtocolMessageType('DefaultConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _DEFAULTCONFIG,
+ '__module__' : 'proxy.vmess.inbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vmess.inbound.DefaultConfig)
+ })
+_sym_db.RegisterMessage(DefaultConfig)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.vmess.inbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vmess.inbound.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\034com.xray.proxy.vmess.inboundP\001Z-github.com/xtls/xray-core/proxy/vmess/inbound\252\002\030Xray.Proxy.Vmess.Inbound'
+ _DETOURCONFIG._serialized_start=90
+ _DETOURCONFIG._serialized_end=116
+ _DEFAULTCONFIG._serialized_start=118
+ _DEFAULTCONFIG._serialized_end=148
+ _CONFIG._serialized_start=151
+ _CONFIG._serialized_end=315
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/vmess/outbound/config_grpc.py b/xray_api/proto/proxy/vmess/outbound/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/vmess/outbound/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/vmess/outbound/config_pb2.py b/xray_api/proto/proxy/vmess/outbound/config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/vmess/outbound/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.protocol import server_spec_pb2 as common_dot_protocol_dot_server__spec__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!proxy/vmess/outbound/config.proto\x12\x19xray.proxy.vmess.outbound\x1a!common/protocol/server_spec.proto\"@\n\x06\x43onfig\x12\x36\n\x08Receiver\x18\x01 \x03(\x0b\x32$.xray.common.protocol.ServerEndpointBm\n\x1d\x63om.xray.proxy.vmess.outboundP\x01Z.github.com/xtls/xray-core/proxy/vmess/outbound\xaa\x02\x19Xray.Proxy.Vmess.Outboundb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'proxy.vmess.outbound.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.vmess.outbound.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\035com.xray.proxy.vmess.outboundP\001Z.github.com/xtls/xray-core/proxy/vmess/outbound\252\002\031Xray.Proxy.Vmess.Outbound'
+ _CONFIG._serialized_start=99
+ _CONFIG._serialized_end=163
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/proxy/wireguard/config_grpc.py b/xray_api/proto/proxy/wireguard/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: proxy/wireguard/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/proxy/wireguard/config_pb2.py b/xray_api/proto/proxy/wireguard/config_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: proxy/wireguard/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cproxy/wireguard/config.proto\x12\x14xray.proxy.wireguard\"s\n\nPeerConfig\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x16\n\x0epre_shared_key\x18\x02 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x03 \x01(\t\x12\x12\n\nkeep_alive\x18\x04 \x01(\r\x12\x13\n\x0b\x61llowed_ips\x18\x05 \x03(\t\"\xeb\x02\n\x0c\x44\x65viceConfig\x12\x12\n\nsecret_key\x18\x01 \x01(\t\x12\x10\n\x08\x65ndpoint\x18\x02 \x03(\t\x12/\n\x05peers\x18\x03 \x03(\x0b\x32 .xray.proxy.wireguard.PeerConfig\x12\x0b\n\x03mtu\x18\x04 \x01(\x05\x12\x13\n\x0bnum_workers\x18\x05 \x01(\x05\x12\x10\n\x08reserved\x18\x06 \x01(\x0c\x12J\n\x0f\x64omain_strategy\x18\x07 \x01(\x0e\x32\x31.xray.proxy.wireguard.DeviceConfig.DomainStrategy\x12\x11\n\tis_client\x18\x08 \x01(\x08\x12\x13\n\x0bkernel_mode\x18\t \x01(\x08\"\\\n\x0e\x44omainStrategy\x12\x0c\n\x08\x46ORCE_IP\x10\x00\x12\r\n\tFORCE_IP4\x10\x01\x12\r\n\tFORCE_IP6\x10\x02\x12\x0e\n\nFORCE_IP46\x10\x03\x12\x0e\n\nFORCE_IP64\x10\x04\x42^\n\x18\x63om.xray.proxy.wireguardP\x01Z)github.com/xtls/xray-core/proxy/wireguard\xaa\x02\x14Xray.Proxy.WireGuardb\x06proto3')
+
+
+
+_PEERCONFIG = DESCRIPTOR.message_types_by_name['PeerConfig']
+_DEVICECONFIG = DESCRIPTOR.message_types_by_name['DeviceConfig']
+_DEVICECONFIG_DOMAINSTRATEGY = _DEVICECONFIG.enum_types_by_name['DomainStrategy']
+PeerConfig = _reflection.GeneratedProtocolMessageType('PeerConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _PEERCONFIG,
+ '__module__' : 'proxy.wireguard.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.wireguard.PeerConfig)
+ })
+_sym_db.RegisterMessage(PeerConfig)
+
+DeviceConfig = _reflection.GeneratedProtocolMessageType('DeviceConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _DEVICECONFIG,
+ '__module__' : 'proxy.wireguard.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.proxy.wireguard.DeviceConfig)
+ })
+_sym_db.RegisterMessage(DeviceConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\030com.xray.proxy.wireguardP\001Z)github.com/xtls/xray-core/proxy/wireguard\252\002\024Xray.Proxy.WireGuard'
+ _PEERCONFIG._serialized_start=54
+ _PEERCONFIG._serialized_end=169
+ _DEVICECONFIG._serialized_start=172
+ _DEVICECONFIG._serialized_end=535
+ _DEVICECONFIG_DOMAINSTRATEGY._serialized_start=443
+ _DEVICECONFIG_DOMAINSTRATEGY._serialized_end=535
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/global/config_grpc.py b/xray_api/proto/transport/global/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/global/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/global/config_pb2.py b/xray_api/proto/transport/global/config_pb2.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/global/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.transport.internet import config_pb2 as transport_dot_internet_dot_config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtransport/global/config.proto\x12\x0exray.transport\x1a\x1ftransport/internet/config.proto\"R\n\x06\x43onfig\x12\x44\n\x12transport_settings\x18\x01 \x03(\x0b\x32(.xray.transport.internet.TransportConfig:\x02\x18\x01\x42\x61\n\x19\x63om.xray.transport.globalP\x01Z*github.com/xtls/xray-core/transport/global\xaa\x02\x15Xray.Transport.Globalb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.global.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\031com.xray.transport.globalP\001Z*github.com/xtls/xray-core/transport/global\252\002\025Xray.Transport.Global'
+ _CONFIG._options = None
+ _CONFIG._serialized_options = b'\030\001'
+ _CONFIG._serialized_start=82
+ _CONFIG._serialized_end=164
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/config_grpc.py b/xray_api/proto/transport/internet/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/config_pb2.py b/xray_api/proto/transport/internet/config_pb2.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\"\x9e\x01\n\x0fTransportConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x03 \x01(\t\x12\x32\n\x08settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\"\xc1\x02\n\x0cStreamConfig\x12@\n\x08protocol\x18\x01 \x01(\x0e\x32*.xray.transport.internet.TransportProtocolB\x02\x18\x01\x12\x15\n\rprotocol_name\x18\x05 \x01(\t\x12\x44\n\x12transport_settings\x18\x02 \x03(\x0b\x32(.xray.transport.internet.TransportConfig\x12\x15\n\rsecurity_type\x18\x03 \x01(\t\x12;\n\x11security_settings\x18\x04 \x03(\x0b\x32 .xray.common.serial.TypedMessage\x12>\n\x0fsocket_settings\x18\x06 \x01(\x0b\x32%.xray.transport.internet.SocketConfig\"7\n\x0bProxyConfig\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x1b\n\x13transportLayerProxy\x18\x02 \x01(\x08\"\xce\x04\n\x0cSocketConfig\x12\x0c\n\x04mark\x18\x01 \x01(\x05\x12\x0b\n\x03tfo\x18\x02 \x01(\x05\x12@\n\x06tproxy\x18\x03 \x01(\x0e\x32\x30.xray.transport.internet.SocketConfig.TProxyMode\x12%\n\x1dreceive_original_dest_address\x18\x04 \x01(\x08\x12\x14\n\x0c\x62ind_address\x18\x05 \x01(\x0c\x12\x11\n\tbind_port\x18\x06 \x01(\r\x12\x1d\n\x15\x61\x63\x63\x65pt_proxy_protocol\x18\x07 \x01(\x08\x12@\n\x0f\x64omain_strategy\x18\x08 \x01(\x0e\x32\'.xray.transport.internet.DomainStrategy\x12\x14\n\x0c\x64ialer_proxy\x18\t \x01(\t\x12\x1f\n\x17tcp_keep_alive_interval\x18\n \x01(\x05\x12\x1b\n\x13tcp_keep_alive_idle\x18\x0b \x01(\x05\x12\x16\n\x0etcp_congestion\x18\x0c \x01(\t\x12\x11\n\tinterface\x18\r \x01(\t\x12\x0e\n\x06v6only\x18\x0e \x01(\x08\x12\x18\n\x10tcp_window_clamp\x18\x0f \x01(\x05\x12\x18\n\x10tcp_user_timeout\x18\x10 \x01(\x05\x12\x13\n\x0btcp_max_seg\x18\x11 \x01(\x05\x12\x14\n\x0ctcp_no_delay\x18\x12 \x01(\x08\x12\x11\n\ttcp_mptcp\x18\x13 \x01(\x08\"/\n\nTProxyMode\x12\x07\n\x03Off\x10\x00\x12\n\n\x06TProxy\x10\x01\x12\x0c\n\x08Redirect\x10\x02*Z\n\x11TransportProtocol\x12\x07\n\x03TCP\x10\x00\x12\x07\n\x03UDP\x10\x01\x12\x08\n\x04MKCP\x10\x02\x12\r\n\tWebSocket\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x10\n\x0c\x44omainSocket\x10\x05*\xa9\x01\n\x0e\x44omainStrategy\x12\t\n\x05\x41S_IS\x10\x00\x12\n\n\x06USE_IP\x10\x01\x12\x0b\n\x07USE_IP4\x10\x02\x12\x0b\n\x07USE_IP6\x10\x03\x12\x0c\n\x08USE_IP46\x10\x04\x12\x0c\n\x08USE_IP64\x10\x05\x12\x0c\n\x08\x46ORCE_IP\x10\x06\x12\r\n\tFORCE_IP4\x10\x07\x12\r\n\tFORCE_IP6\x10\x08\x12\x0e\n\nFORCE_IP46\x10\t\x12\x0e\n\nFORCE_IP64\x10\nBg\n\x1b\x63om.xray.transport.internetP\x01Z,github.com/xtls/xray-core/transport/internet\xaa\x02\x17Xray.Transport.Internetb\x06proto3')
+
+_TRANSPORTPROTOCOL = DESCRIPTOR.enum_types_by_name['TransportProtocol']
+TransportProtocol = enum_type_wrapper.EnumTypeWrapper(_TRANSPORTPROTOCOL)
+_DOMAINSTRATEGY = DESCRIPTOR.enum_types_by_name['DomainStrategy']
+DomainStrategy = enum_type_wrapper.EnumTypeWrapper(_DOMAINSTRATEGY)
+TCP = 0
+UDP = 1
+MKCP = 2
+WebSocket = 3
+HTTP = 4
+DomainSocket = 5
+AS_IS = 0
+USE_IP = 1
+USE_IP4 = 2
+USE_IP6 = 3
+USE_IP46 = 4
+USE_IP64 = 5
+FORCE_IP = 6
+FORCE_IP4 = 7
+FORCE_IP6 = 8
+FORCE_IP46 = 9
+FORCE_IP64 = 10
+
+
+_TRANSPORTCONFIG = DESCRIPTOR.message_types_by_name['TransportConfig']
+_STREAMCONFIG = DESCRIPTOR.message_types_by_name['StreamConfig']
+_PROXYCONFIG = DESCRIPTOR.message_types_by_name['ProxyConfig']
+_SOCKETCONFIG = DESCRIPTOR.message_types_by_name['SocketConfig']
+_SOCKETCONFIG_TPROXYMODE = _SOCKETCONFIG.enum_types_by_name['TProxyMode']
+TransportConfig = _reflection.GeneratedProtocolMessageType('TransportConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _TRANSPORTCONFIG,
+ '__module__' : 'transport.internet.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.TransportConfig)
+ })
+_sym_db.RegisterMessage(TransportConfig)
+
+StreamConfig = _reflection.GeneratedProtocolMessageType('StreamConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _STREAMCONFIG,
+ '__module__' : 'transport.internet.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.StreamConfig)
+ })
+_sym_db.RegisterMessage(StreamConfig)
+
+ProxyConfig = _reflection.GeneratedProtocolMessageType('ProxyConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _PROXYCONFIG,
+ '__module__' : 'transport.internet.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.ProxyConfig)
+ })
+_sym_db.RegisterMessage(ProxyConfig)
+
+SocketConfig = _reflection.GeneratedProtocolMessageType('SocketConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _SOCKETCONFIG,
+ '__module__' : 'transport.internet.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.SocketConfig)
+ })
+_sym_db.RegisterMessage(SocketConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\033com.xray.transport.internetP\001Z,github.com/xtls/xray-core/transport/internet\252\002\027Xray.Transport.Internet'
+ _TRANSPORTCONFIG.fields_by_name['protocol']._options = None
+ _TRANSPORTCONFIG.fields_by_name['protocol']._serialized_options = b'\030\001'
+ _STREAMCONFIG.fields_by_name['protocol']._options = None
+ _STREAMCONFIG.fields_by_name['protocol']._serialized_options = b'\030\001'
+ _TRANSPORTPROTOCOL._serialized_start=1230
+ _TRANSPORTPROTOCOL._serialized_end=1320
+ _DOMAINSTRATEGY._serialized_start=1323
+ _DOMAINSTRATEGY._serialized_end=1492
+ _TRANSPORTCONFIG._serialized_start=96
+ _TRANSPORTCONFIG._serialized_end=254
+ _STREAMCONFIG._serialized_start=257
+ _STREAMCONFIG._serialized_end=578
+ _PROXYCONFIG._serialized_start=580
+ _PROXYCONFIG._serialized_end=635
+ _SOCKETCONFIG._serialized_start=638
+ _SOCKETCONFIG._serialized_end=1228
+ _SOCKETCONFIG_TPROXYMODE._serialized_start=1181
+ _SOCKETCONFIG_TPROXYMODE._serialized_end=1228
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/domainsocket/config_grpc.py b/xray_api/proto/transport/internet/domainsocket/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/domainsocket/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/domainsocket/config_pb2.py b/xray_api/proto/transport/internet/domainsocket/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/domainsocket/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,transport/internet/domainsocket/config.proto\x12$xray.transport.internet.domainsocket\"9\n\x06\x43onfig\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x10\n\x08\x61\x62stract\x18\x02 \x01(\x08\x12\x0f\n\x07padding\x18\x03 \x01(\x08\x42\x8e\x01\n(com.xray.transport.internet.domainsocketP\x01Z9github.com/xtls/xray-core/transport/internet/domainsocket\xaa\x02$Xray.Transport.Internet.DomainSocketb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.domainsocket.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.domainsocket.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n(com.xray.transport.internet.domainsocketP\001Z9github.com/xtls/xray-core/transport/internet/domainsocket\252\002$Xray.Transport.Internet.DomainSocket'
+ _CONFIG._serialized_start=86
+ _CONFIG._serialized_end=143
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/grpc/config_grpc.py b/xray_api/proto/transport/internet/grpc/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/grpc/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/grpc/config_pb2.py b/xray_api/proto/transport/internet/grpc/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/grpc/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$transport/internet/grpc/config.proto\x12%xray.transport.internet.grpc.encoding\"\xc5\x01\n\x06\x43onfig\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x14\n\x0cservice_name\x18\x02 \x01(\t\x12\x12\n\nmulti_mode\x18\x03 \x01(\x08\x12\x14\n\x0cidle_timeout\x18\x04 \x01(\x05\x12\x1c\n\x14health_check_timeout\x18\x05 \x01(\x05\x12\x1d\n\x15permit_without_stream\x18\x06 \x01(\x08\x12\x1c\n\x14initial_windows_size\x18\x07 \x01(\x05\x12\x12\n\nuser_agent\x18\x08 \x01(\tB3Z1github.com/xtls/xray-core/transport/internet/grpcb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.grpc.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.grpc.encoding.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'Z1github.com/xtls/xray-core/transport/internet/grpc'
+ _CONFIG._serialized_start=80
+ _CONFIG._serialized_end=277
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/grpc/encoding/stream_grpc.py b/xray_api/proto/transport/internet/grpc/encoding/stream_grpc.py
@@ -0,0 +1,56 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/grpc/encoding/stream.proto
+# plugin: grpclib.plugin.main
+import abc
+import typing
+
+import grpclib.const
+import grpclib.client
+if typing.TYPE_CHECKING:
+ import grpclib.server
+
+import xray_api.proto.transport.internet.grpc.encoding.stream_pb2
+
+
+class GRPCServiceBase(abc.ABC):
+
+ @abc.abstractmethod
+ async def Tun(self, stream: 'grpclib.server.Stream[transport.internet.grpc.encoding.stream_pb2.Hunk, transport.internet.grpc.encoding.stream_pb2.Hunk]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def TunMulti(self, stream: 'grpclib.server.Stream[transport.internet.grpc.encoding.stream_pb2.MultiHunk, transport.internet.grpc.encoding.stream_pb2.MultiHunk]') -> None:
+ pass
+
+ def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
+ return {
+ '/xray.transport.internet.grpc.encoding.GRPCService/Tun': grpclib.const.Handler(
+ self.Tun,
+ grpclib.const.Cardinality.STREAM_STREAM,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.Hunk,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.Hunk,
+ ),
+ '/xray.transport.internet.grpc.encoding.GRPCService/TunMulti': grpclib.const.Handler(
+ self.TunMulti,
+ grpclib.const.Cardinality.STREAM_STREAM,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.MultiHunk,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.MultiHunk,
+ ),
+ }
+
+
+class GRPCServiceStub:
+
+ def __init__(self, channel: grpclib.client.Channel) -> None:
+ self.Tun = grpclib.client.StreamStreamMethod(
+ channel,
+ '/xray.transport.internet.grpc.encoding.GRPCService/Tun',
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.Hunk,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.Hunk,
+ )
+ self.TunMulti = grpclib.client.StreamStreamMethod(
+ channel,
+ '/xray.transport.internet.grpc.encoding.GRPCService/TunMulti',
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.MultiHunk,
+ xray_api.proto.transport.internet.grpc.encoding.stream_pb2.MultiHunk,
+ )
diff --git a/xray_api/proto/transport/internet/grpc/encoding/stream_pb2.py b/xray_api/proto/transport/internet/grpc/encoding/stream_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/grpc/encoding/stream.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-transport/internet/grpc/encoding/stream.proto\x12%xray.transport.internet.grpc.encoding\"\x14\n\x04Hunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x19\n\tMultiHunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x03(\x0c\x32\xe6\x01\n\x0bGRPCService\x12\x63\n\x03Tun\x12+.xray.transport.internet.grpc.encoding.Hunk\x1a+.xray.transport.internet.grpc.encoding.Hunk(\x01\x30\x01\x12r\n\x08TunMulti\x12\x30.xray.transport.internet.grpc.encoding.MultiHunk\x1a\x30.xray.transport.internet.grpc.encoding.MultiHunk(\x01\x30\x01\x42<Z:github.com/xtls/xray-core/transport/internet/grpc/encodingb\x06proto3')
+
+
+
+_HUNK = DESCRIPTOR.message_types_by_name['Hunk']
+_MULTIHUNK = DESCRIPTOR.message_types_by_name['MultiHunk']
+Hunk = _reflection.GeneratedProtocolMessageType('Hunk', (_message.Message,), {
+ 'DESCRIPTOR' : _HUNK,
+ '__module__' : 'transport.internet.grpc.encoding.stream_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.grpc.encoding.Hunk)
+ })
+_sym_db.RegisterMessage(Hunk)
+
+MultiHunk = _reflection.GeneratedProtocolMessageType('MultiHunk', (_message.Message,), {
+ 'DESCRIPTOR' : _MULTIHUNK,
+ '__module__' : 'transport.internet.grpc.encoding.stream_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.grpc.encoding.MultiHunk)
+ })
+_sym_db.RegisterMessage(MultiHunk)
+
+_GRPCSERVICE = DESCRIPTOR.services_by_name['GRPCService']
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'Z:github.com/xtls/xray-core/transport/internet/grpc/encoding'
+ _HUNK._serialized_start=88
+ _HUNK._serialized_end=108
+ _MULTIHUNK._serialized_start=110
+ _MULTIHUNK._serialized_end=135
+ _GRPCSERVICE._serialized_start=138
+ _GRPCSERVICE._serialized_end=368
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/dns/config_grpc.py b/xray_api/proto/transport/internet/headers/dns/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/dns/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/dns/config_pb2.py b/xray_api/proto/transport/internet/headers/dns/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/dns/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+transport/internet/headers/dns/config.proto\x12#xray.transport.internet.headers.dns\"\x18\n\x06\x43onfig\x12\x0e\n\x06\x64omain\x18\x01 \x01(\tB\x8b\x01\n\'com.xray.transport.internet.headers.dnsP\x01Z8github.com/xtls/xray-core/transport/internet/headers/dns\xaa\x02#Xray.Transport.Internet.Headers.DNSb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.headers.dns.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.dns.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\'com.xray.transport.internet.headers.dnsP\001Z8github.com/xtls/xray-core/transport/internet/headers/dns\252\002#Xray.Transport.Internet.Headers.DNS'
+ _CONFIG._serialized_start=84
+ _CONFIG._serialized_end=108
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/http/config_grpc.py b/xray_api/proto/transport/internet/headers/http/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/http/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/http/config_pb2.py b/xray_api/proto/transport/internet/headers/http/config_pb2.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/http/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,transport/internet/headers/http/config.proto\x12$xray.transport.internet.headers.http\"%\n\x06Header\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x03(\t\"\x18\n\x07Version\x12\r\n\x05value\x18\x01 \x01(\t\"\x17\n\x06Method\x12\r\n\x05value\x18\x01 \x01(\t\"\xd8\x01\n\rRequestConfig\x12>\n\x07version\x18\x01 \x01(\x0b\x32-.xray.transport.internet.headers.http.Version\x12<\n\x06method\x18\x02 \x01(\x0b\x32,.xray.transport.internet.headers.http.Method\x12\x0b\n\x03uri\x18\x03 \x03(\t\x12<\n\x06header\x18\x04 \x03(\x0b\x32,.xray.transport.internet.headers.http.Header\"&\n\x06Status\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\"\xcc\x01\n\x0eResponseConfig\x12>\n\x07version\x18\x01 \x01(\x0b\x32-.xray.transport.internet.headers.http.Version\x12<\n\x06status\x18\x02 \x01(\x0b\x32,.xray.transport.internet.headers.http.Status\x12<\n\x06header\x18\x03 \x03(\x0b\x32,.xray.transport.internet.headers.http.Header\"\x96\x01\n\x06\x43onfig\x12\x44\n\x07request\x18\x01 \x01(\x0b\x32\x33.xray.transport.internet.headers.http.RequestConfig\x12\x46\n\x08response\x18\x02 \x01(\x0b\x32\x34.xray.transport.internet.headers.http.ResponseConfigB\x8e\x01\n(com.xray.transport.internet.headers.httpP\x01Z9github.com/xtls/xray-core/transport/internet/headers/http\xaa\x02$Xray.Transport.Internet.Headers.Httpb\x06proto3')
+
+
+
+_HEADER = DESCRIPTOR.message_types_by_name['Header']
+_VERSION = DESCRIPTOR.message_types_by_name['Version']
+_METHOD = DESCRIPTOR.message_types_by_name['Method']
+_REQUESTCONFIG = DESCRIPTOR.message_types_by_name['RequestConfig']
+_STATUS = DESCRIPTOR.message_types_by_name['Status']
+_RESPONSECONFIG = DESCRIPTOR.message_types_by_name['ResponseConfig']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
+ 'DESCRIPTOR' : _HEADER,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.Header)
+ })
+_sym_db.RegisterMessage(Header)
+
+Version = _reflection.GeneratedProtocolMessageType('Version', (_message.Message,), {
+ 'DESCRIPTOR' : _VERSION,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.Version)
+ })
+_sym_db.RegisterMessage(Version)
+
+Method = _reflection.GeneratedProtocolMessageType('Method', (_message.Message,), {
+ 'DESCRIPTOR' : _METHOD,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.Method)
+ })
+_sym_db.RegisterMessage(Method)
+
+RequestConfig = _reflection.GeneratedProtocolMessageType('RequestConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _REQUESTCONFIG,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.RequestConfig)
+ })
+_sym_db.RegisterMessage(RequestConfig)
+
+Status = _reflection.GeneratedProtocolMessageType('Status', (_message.Message,), {
+ 'DESCRIPTOR' : _STATUS,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.Status)
+ })
+_sym_db.RegisterMessage(Status)
+
+ResponseConfig = _reflection.GeneratedProtocolMessageType('ResponseConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _RESPONSECONFIG,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.ResponseConfig)
+ })
+_sym_db.RegisterMessage(ResponseConfig)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.headers.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.http.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n(com.xray.transport.internet.headers.httpP\001Z9github.com/xtls/xray-core/transport/internet/headers/http\252\002$Xray.Transport.Internet.Headers.Http'
+ _HEADER._serialized_start=86
+ _HEADER._serialized_end=123
+ _VERSION._serialized_start=125
+ _VERSION._serialized_end=149
+ _METHOD._serialized_start=151
+ _METHOD._serialized_end=174
+ _REQUESTCONFIG._serialized_start=177
+ _REQUESTCONFIG._serialized_end=393
+ _STATUS._serialized_start=395
+ _STATUS._serialized_end=433
+ _RESPONSECONFIG._serialized_start=436
+ _RESPONSECONFIG._serialized_end=640
+ _CONFIG._serialized_start=643
+ _CONFIG._serialized_end=793
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/noop/config_grpc.py b/xray_api/proto/transport/internet/headers/noop/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/noop/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/noop/config_pb2.py b/xray_api/proto/transport/internet/headers/noop/config_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/noop/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,transport/internet/headers/noop/config.proto\x12$xray.transport.internet.headers.noop\"\x08\n\x06\x43onfig\"\x12\n\x10\x43onnectionConfigB\x8e\x01\n(com.xray.transport.internet.headers.noopP\x01Z9github.com/xtls/xray-core/transport/internet/headers/noop\xaa\x02$Xray.Transport.Internet.Headers.Noopb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CONNECTIONCONFIG = DESCRIPTOR.message_types_by_name['ConnectionConfig']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.headers.noop.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.noop.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+ConnectionConfig = _reflection.GeneratedProtocolMessageType('ConnectionConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _CONNECTIONCONFIG,
+ '__module__' : 'transport.internet.headers.noop.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.noop.ConnectionConfig)
+ })
+_sym_db.RegisterMessage(ConnectionConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n(com.xray.transport.internet.headers.noopP\001Z9github.com/xtls/xray-core/transport/internet/headers/noop\252\002$Xray.Transport.Internet.Headers.Noop'
+ _CONFIG._serialized_start=86
+ _CONFIG._serialized_end=94
+ _CONNECTIONCONFIG._serialized_start=96
+ _CONNECTIONCONFIG._serialized_end=114
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/srtp/config_grpc.py b/xray_api/proto/transport/internet/headers/srtp/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/srtp/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/srtp/config_pb2.py b/xray_api/proto/transport/internet/headers/srtp/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/srtp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,transport/internet/headers/srtp/config.proto\x12$xray.transport.internet.headers.srtp\"w\n\x06\x43onfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x0f\n\x07padding\x18\x02 \x01(\x08\x12\x11\n\textension\x18\x03 \x01(\x08\x12\x12\n\ncsrc_count\x18\x04 \x01(\r\x12\x0e\n\x06marker\x18\x05 \x01(\x08\x12\x14\n\x0cpayload_type\x18\x06 \x01(\rB\x8e\x01\n(com.xray.transport.internet.headers.srtpP\x01Z9github.com/xtls/xray-core/transport/internet/headers/srtp\xaa\x02$Xray.Transport.Internet.Headers.Srtpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.headers.srtp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.srtp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n(com.xray.transport.internet.headers.srtpP\001Z9github.com/xtls/xray-core/transport/internet/headers/srtp\252\002$Xray.Transport.Internet.Headers.Srtp'
+ _CONFIG._serialized_start=86
+ _CONFIG._serialized_end=205
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/tls/config_grpc.py b/xray_api/proto/transport/internet/headers/tls/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/tls/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/tls/config_pb2.py b/xray_api/proto/transport/internet/headers/tls/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/tls/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+transport/internet/headers/tls/config.proto\x12#xray.transport.internet.headers.tls\"\x0e\n\x0cPacketConfigB\x8b\x01\n\'com.xray.transport.internet.headers.tlsP\x01Z8github.com/xtls/xray-core/transport/internet/headers/tls\xaa\x02#Xray.Transport.Internet.Headers.Tlsb\x06proto3')
+
+
+
+_PACKETCONFIG = DESCRIPTOR.message_types_by_name['PacketConfig']
+PacketConfig = _reflection.GeneratedProtocolMessageType('PacketConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _PACKETCONFIG,
+ '__module__' : 'transport.internet.headers.tls.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.tls.PacketConfig)
+ })
+_sym_db.RegisterMessage(PacketConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\'com.xray.transport.internet.headers.tlsP\001Z8github.com/xtls/xray-core/transport/internet/headers/tls\252\002#Xray.Transport.Internet.Headers.Tls'
+ _PACKETCONFIG._serialized_start=84
+ _PACKETCONFIG._serialized_end=98
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/utp/config_grpc.py b/xray_api/proto/transport/internet/headers/utp/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/utp/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/utp/config_pb2.py b/xray_api/proto/transport/internet/headers/utp/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/utp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+transport/internet/headers/utp/config.proto\x12#xray.transport.internet.headers.utp\"\x19\n\x06\x43onfig\x12\x0f\n\x07version\x18\x01 \x01(\rB\x8b\x01\n\'com.xray.transport.internet.headers.utpP\x01Z8github.com/xtls/xray-core/transport/internet/headers/utp\xaa\x02#Xray.Transport.Internet.Headers.Utpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.headers.utp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.utp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\'com.xray.transport.internet.headers.utpP\001Z8github.com/xtls/xray-core/transport/internet/headers/utp\252\002#Xray.Transport.Internet.Headers.Utp'
+ _CONFIG._serialized_start=84
+ _CONFIG._serialized_end=109
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/wechat/config_grpc.py b/xray_api/proto/transport/internet/headers/wechat/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/wechat/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/wechat/config_pb2.py b/xray_api/proto/transport/internet/headers/wechat/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/wechat/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.transport/internet/headers/wechat/config.proto\x12&xray.transport.internet.headers.wechat\"\r\n\x0bVideoConfigB\x94\x01\n*com.xray.transport.internet.headers.wechatP\x01Z;github.com/xtls/xray-core/transport/internet/headers/wechat\xaa\x02&Xray.Transport.Internet.Headers.Wechatb\x06proto3')
+
+
+
+_VIDEOCONFIG = DESCRIPTOR.message_types_by_name['VideoConfig']
+VideoConfig = _reflection.GeneratedProtocolMessageType('VideoConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _VIDEOCONFIG,
+ '__module__' : 'transport.internet.headers.wechat.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.wechat.VideoConfig)
+ })
+_sym_db.RegisterMessage(VideoConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n*com.xray.transport.internet.headers.wechatP\001Z;github.com/xtls/xray-core/transport/internet/headers/wechat\252\002&Xray.Transport.Internet.Headers.Wechat'
+ _VIDEOCONFIG._serialized_start=90
+ _VIDEOCONFIG._serialized_end=103
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/headers/wireguard/config_grpc.py b/xray_api/proto/transport/internet/headers/wireguard/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/headers/wireguard/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/headers/wireguard/config_pb2.py b/xray_api/proto/transport/internet/headers/wireguard/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/headers/wireguard/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n1transport/internet/headers/wireguard/config.proto\x12)xray.transport.internet.headers.wireguard\"\x11\n\x0fWireguardConfigB\x9d\x01\n-com.xray.transport.internet.headers.wireguardP\x01Z>github.com/xtls/xray-core/transport/internet/headers/wireguard\xaa\x02)Xray.Transport.Internet.Headers.Wireguardb\x06proto3')
+
+
+
+_WIREGUARDCONFIG = DESCRIPTOR.message_types_by_name['WireguardConfig']
+WireguardConfig = _reflection.GeneratedProtocolMessageType('WireguardConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _WIREGUARDCONFIG,
+ '__module__' : 'transport.internet.headers.wireguard.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.headers.wireguard.WireguardConfig)
+ })
+_sym_db.RegisterMessage(WireguardConfig)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n-com.xray.transport.internet.headers.wireguardP\001Z>github.com/xtls/xray-core/transport/internet/headers/wireguard\252\002)Xray.Transport.Internet.Headers.Wireguard'
+ _WIREGUARDCONFIG._serialized_start=96
+ _WIREGUARDCONFIG._serialized_end=113
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/http/config_grpc.py b/xray_api/proto/transport/internet/http/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/http/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/http/config_pb2.py b/xray_api/proto/transport/internet/http/config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/http/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.transport.internet.headers.http import config_pb2 as transport_dot_internet_dot_headers_dot_http_dot_config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$transport/internet/http/config.proto\x12\x1cxray.transport.internet.http\x1a,transport/internet/headers/http/config.proto\"\xa6\x01\n\x06\x43onfig\x12\x0c\n\x04host\x18\x01 \x03(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x14\n\x0cidle_timeout\x18\x03 \x01(\x05\x12\x1c\n\x14health_check_timeout\x18\x04 \x01(\x05\x12\x0e\n\x06method\x18\x05 \x01(\t\x12<\n\x06header\x18\x06 \x03(\x0b\x32,.xray.transport.internet.headers.http.HeaderBv\n com.xray.transport.internet.httpP\x01Z1github.com/xtls/xray-core/transport/internet/http\xaa\x02\x1cXray.Transport.Internet.Httpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.http.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.http.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n com.xray.transport.internet.httpP\001Z1github.com/xtls/xray-core/transport/internet/http\252\002\034Xray.Transport.Internet.Http'
+ _CONFIG._serialized_start=117
+ _CONFIG._serialized_end=283
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/kcp/config_grpc.py b/xray_api/proto/transport/internet/kcp/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/kcp/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/kcp/config_pb2.py b/xray_api/proto/transport/internet/kcp/config_pb2.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/kcp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#transport/internet/kcp/config.proto\x12\x1bxray.transport.internet.kcp\x1a!common/serial/typed_message.proto\"\x14\n\x03MTU\x12\r\n\x05value\x18\x01 \x01(\r\"\x14\n\x03TTI\x12\r\n\x05value\x18\x01 \x01(\r\"\x1f\n\x0eUplinkCapacity\x12\r\n\x05value\x18\x01 \x01(\r\"!\n\x10\x44ownlinkCapacity\x12\r\n\x05value\x18\x01 \x01(\r\"\x1b\n\x0bWriteBuffer\x12\x0c\n\x04size\x18\x01 \x01(\r\"\x1a\n\nReadBuffer\x12\x0c\n\x04size\x18\x01 \x01(\r\"!\n\x0f\x43onnectionReuse\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"\x1e\n\x0e\x45ncryptionSeed\x12\x0c\n\x04seed\x18\x01 \x01(\t\"\x82\x04\n\x06\x43onfig\x12-\n\x03mtu\x18\x01 \x01(\x0b\x32 .xray.transport.internet.kcp.MTU\x12-\n\x03tti\x18\x02 \x01(\x0b\x32 .xray.transport.internet.kcp.TTI\x12\x44\n\x0fuplink_capacity\x18\x03 \x01(\x0b\x32+.xray.transport.internet.kcp.UplinkCapacity\x12H\n\x11\x64ownlink_capacity\x18\x04 \x01(\x0b\x32-.xray.transport.internet.kcp.DownlinkCapacity\x12\x12\n\ncongestion\x18\x05 \x01(\x08\x12>\n\x0cwrite_buffer\x18\x06 \x01(\x0b\x32(.xray.transport.internet.kcp.WriteBuffer\x12<\n\x0bread_buffer\x18\x07 \x01(\x0b\x32\'.xray.transport.internet.kcp.ReadBuffer\x12\x37\n\rheader_config\x18\x08 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x39\n\x04seed\x18\n \x01(\x0b\x32+.xray.transport.internet.kcp.EncryptionSeedJ\x04\x08\t\x10\nBs\n\x1f\x63om.xray.transport.internet.kcpP\x01Z0github.com/xtls/xray-core/transport/internet/kcp\xaa\x02\x1bXray.Transport.Internet.Kcpb\x06proto3')
+
+
+
+_MTU = DESCRIPTOR.message_types_by_name['MTU']
+_TTI = DESCRIPTOR.message_types_by_name['TTI']
+_UPLINKCAPACITY = DESCRIPTOR.message_types_by_name['UplinkCapacity']
+_DOWNLINKCAPACITY = DESCRIPTOR.message_types_by_name['DownlinkCapacity']
+_WRITEBUFFER = DESCRIPTOR.message_types_by_name['WriteBuffer']
+_READBUFFER = DESCRIPTOR.message_types_by_name['ReadBuffer']
+_CONNECTIONREUSE = DESCRIPTOR.message_types_by_name['ConnectionReuse']
+_ENCRYPTIONSEED = DESCRIPTOR.message_types_by_name['EncryptionSeed']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+MTU = _reflection.GeneratedProtocolMessageType('MTU', (_message.Message,), {
+ 'DESCRIPTOR' : _MTU,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.MTU)
+ })
+_sym_db.RegisterMessage(MTU)
+
+TTI = _reflection.GeneratedProtocolMessageType('TTI', (_message.Message,), {
+ 'DESCRIPTOR' : _TTI,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.TTI)
+ })
+_sym_db.RegisterMessage(TTI)
+
+UplinkCapacity = _reflection.GeneratedProtocolMessageType('UplinkCapacity', (_message.Message,), {
+ 'DESCRIPTOR' : _UPLINKCAPACITY,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.UplinkCapacity)
+ })
+_sym_db.RegisterMessage(UplinkCapacity)
+
+DownlinkCapacity = _reflection.GeneratedProtocolMessageType('DownlinkCapacity', (_message.Message,), {
+ 'DESCRIPTOR' : _DOWNLINKCAPACITY,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.DownlinkCapacity)
+ })
+_sym_db.RegisterMessage(DownlinkCapacity)
+
+WriteBuffer = _reflection.GeneratedProtocolMessageType('WriteBuffer', (_message.Message,), {
+ 'DESCRIPTOR' : _WRITEBUFFER,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.WriteBuffer)
+ })
+_sym_db.RegisterMessage(WriteBuffer)
+
+ReadBuffer = _reflection.GeneratedProtocolMessageType('ReadBuffer', (_message.Message,), {
+ 'DESCRIPTOR' : _READBUFFER,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.ReadBuffer)
+ })
+_sym_db.RegisterMessage(ReadBuffer)
+
+ConnectionReuse = _reflection.GeneratedProtocolMessageType('ConnectionReuse', (_message.Message,), {
+ 'DESCRIPTOR' : _CONNECTIONREUSE,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.ConnectionReuse)
+ })
+_sym_db.RegisterMessage(ConnectionReuse)
+
+EncryptionSeed = _reflection.GeneratedProtocolMessageType('EncryptionSeed', (_message.Message,), {
+ 'DESCRIPTOR' : _ENCRYPTIONSEED,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.EncryptionSeed)
+ })
+_sym_db.RegisterMessage(EncryptionSeed)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.kcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.kcp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\037com.xray.transport.internet.kcpP\001Z0github.com/xtls/xray-core/transport/internet/kcp\252\002\033Xray.Transport.Internet.Kcp'
+ _MTU._serialized_start=103
+ _MTU._serialized_end=123
+ _TTI._serialized_start=125
+ _TTI._serialized_end=145
+ _UPLINKCAPACITY._serialized_start=147
+ _UPLINKCAPACITY._serialized_end=178
+ _DOWNLINKCAPACITY._serialized_start=180
+ _DOWNLINKCAPACITY._serialized_end=213
+ _WRITEBUFFER._serialized_start=215
+ _WRITEBUFFER._serialized_end=242
+ _READBUFFER._serialized_start=244
+ _READBUFFER._serialized_end=270
+ _CONNECTIONREUSE._serialized_start=272
+ _CONNECTIONREUSE._serialized_end=305
+ _ENCRYPTIONSEED._serialized_start=307
+ _ENCRYPTIONSEED._serialized_end=337
+ _CONFIG._serialized_start=340
+ _CONFIG._serialized_end=854
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/quic/config_grpc.py b/xray_api/proto/transport/internet/quic/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/quic/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/quic/config_pb2.py b/xray_api/proto/transport/internet/quic/config_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/quic/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+from xray_api.proto.common.protocol import headers_pb2 as common_dot_protocol_dot_headers__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$transport/internet/quic/config.proto\x12\x1cxray.transport.internet.quic\x1a!common/serial/typed_message.proto\x1a\x1d\x63ommon/protocol/headers.proto\"\x7f\n\x06\x43onfig\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x08security\x18\x02 \x01(\x0b\x32$.xray.common.protocol.SecurityConfig\x12\x30\n\x06header\x18\x03 \x01(\x0b\x32 .xray.common.serial.TypedMessageBv\n com.xray.transport.internet.quicP\x01Z1github.com/xtls/xray-core/transport/internet/quic\xaa\x02\x1cXray.Transport.Internet.Quicb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.quic.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.quic.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n com.xray.transport.internet.quicP\001Z1github.com/xtls/xray-core/transport/internet/quic\252\002\034Xray.Transport.Internet.Quic'
+ _CONFIG._serialized_start=136
+ _CONFIG._serialized_end=263
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/reality/config_grpc.py b/xray_api/proto/transport/internet/reality/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/reality/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/reality/config_pb2.py b/xray_api/proto/transport/internet/reality/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/reality/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'transport/internet/reality/config.proto\x12\x1fxray.transport.internet.reality\"\xb9\x02\n\x06\x43onfig\x12\x0c\n\x04show\x18\x01 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0c\n\x04xver\x18\x04 \x01(\x04\x12\x14\n\x0cserver_names\x18\x05 \x03(\t\x12\x13\n\x0bprivate_key\x18\x06 \x01(\x0c\x12\x16\n\x0emin_client_ver\x18\x07 \x01(\x0c\x12\x16\n\x0emax_client_ver\x18\x08 \x01(\x0c\x12\x15\n\rmax_time_diff\x18\t \x01(\x04\x12\x11\n\tshort_ids\x18\n \x03(\x0c\x12\x13\n\x0b\x46ingerprint\x18\x15 \x01(\t\x12\x13\n\x0bserver_name\x18\x16 \x01(\t\x12\x12\n\npublic_key\x18\x17 \x01(\x0c\x12\x10\n\x08short_id\x18\x18 \x01(\x0c\x12\x10\n\x08spider_x\x18\x19 \x01(\t\x12\x10\n\x08spider_y\x18\x1a \x03(\x03\x42\x7f\n#com.xray.transport.internet.realityP\x01Z4github.com/xtls/xray-core/transport/internet/reality\xaa\x02\x1fXray.Transport.Internet.Realityb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.reality.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.reality.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n#com.xray.transport.internet.realityP\001Z4github.com/xtls/xray-core/transport/internet/reality\252\002\037Xray.Transport.Internet.Reality'
+ _CONFIG._serialized_start=77
+ _CONFIG._serialized_end=390
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/tcp/config_grpc.py b/xray_api/proto/transport/internet/tcp/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/tcp/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/tcp/config_pb2.py b/xray_api/proto/transport/internet/tcp/config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/tcp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from xray_api.proto.common.serial import typed_message_pb2 as common_dot_serial_dot_typed__message__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#transport/internet/tcp/config.proto\x12\x1bxray.transport.internet.tcp\x1a!common/serial/typed_message.proto\"h\n\x06\x43onfig\x12\x39\n\x0fheader_settings\x18\x02 \x01(\x0b\x32 .xray.common.serial.TypedMessage\x12\x1d\n\x15\x61\x63\x63\x65pt_proxy_protocol\x18\x03 \x01(\x08J\x04\x08\x01\x10\x02\x42s\n\x1f\x63om.xray.transport.internet.tcpP\x01Z0github.com/xtls/xray-core/transport/internet/tcp\xaa\x02\x1bXray.Transport.Internet.Tcpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.tcp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.tcp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\037com.xray.transport.internet.tcpP\001Z0github.com/xtls/xray-core/transport/internet/tcp\252\002\033Xray.Transport.Internet.Tcp'
+ _CONFIG._serialized_start=103
+ _CONFIG._serialized_end=207
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/tls/config_grpc.py b/xray_api/proto/transport/internet/tls/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/tls/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/tls/config_pb2.py b/xray_api/proto/transport/internet/tls/config_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/tls/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#transport/internet/tls/config.proto\x12\x1bxray.transport.internet.tls\"\x91\x02\n\x0b\x43\x65rtificate\x12\x13\n\x0b\x63\x65rtificate\x18\x01 \x01(\x0c\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12=\n\x05usage\x18\x03 \x01(\x0e\x32..xray.transport.internet.tls.Certificate.Usage\x12\x15\n\rocsp_stapling\x18\x04 \x01(\x04\x12\x18\n\x10\x63\x65rtificate_path\x18\x05 \x01(\t\x12\x10\n\x08key_path\x18\x06 \x01(\t\x12\x18\n\x10One_time_loading\x18\x07 \x01(\x08\"D\n\x05Usage\x12\x10\n\x0c\x45NCIPHERMENT\x10\x00\x12\x14\n\x10\x41UTHORITY_VERIFY\x10\x01\x12\x13\n\x0f\x41UTHORITY_ISSUE\x10\x02\"\xc3\x03\n\x06\x43onfig\x12\x16\n\x0e\x61llow_insecure\x18\x01 \x01(\x08\x12=\n\x0b\x63\x65rtificate\x18\x02 \x03(\x0b\x32(.xray.transport.internet.tls.Certificate\x12\x13\n\x0bserver_name\x18\x03 \x01(\t\x12\x15\n\rnext_protocol\x18\x04 \x03(\t\x12!\n\x19\x65nable_session_resumption\x18\x05 \x01(\x08\x12\x1b\n\x13\x64isable_system_root\x18\x06 \x01(\x08\x12\x13\n\x0bmin_version\x18\x07 \x01(\t\x12\x13\n\x0bmax_version\x18\x08 \x01(\t\x12\x15\n\rcipher_suites\x18\t \x01(\t\x12#\n\x1bprefer_server_cipher_suites\x18\n \x01(\x08\x12\x13\n\x0b\x66ingerprint\x18\x0b \x01(\t\x12\x1a\n\x12reject_unknown_sni\x18\x0c \x01(\x08\x12,\n$pinned_peer_certificate_chain_sha256\x18\r \x03(\x0c\x12\x31\n)pinned_peer_certificate_public_key_sha256\x18\x0e \x03(\x0c\x42s\n\x1f\x63om.xray.transport.internet.tlsP\x01Z0github.com/xtls/xray-core/transport/internet/tls\xaa\x02\x1bXray.Transport.Internet.Tlsb\x06proto3')
+
+
+
+_CERTIFICATE = DESCRIPTOR.message_types_by_name['Certificate']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+_CERTIFICATE_USAGE = _CERTIFICATE.enum_types_by_name['Usage']
+Certificate = _reflection.GeneratedProtocolMessageType('Certificate', (_message.Message,), {
+ 'DESCRIPTOR' : _CERTIFICATE,
+ '__module__' : 'transport.internet.tls.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.tls.Certificate)
+ })
+_sym_db.RegisterMessage(Certificate)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.tls.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.tls.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\037com.xray.transport.internet.tlsP\001Z0github.com/xtls/xray-core/transport/internet/tls\252\002\033Xray.Transport.Internet.Tls'
+ _CERTIFICATE._serialized_start=69
+ _CERTIFICATE._serialized_end=342
+ _CERTIFICATE_USAGE._serialized_start=274
+ _CERTIFICATE_USAGE._serialized_end=342
+ _CONFIG._serialized_start=345
+ _CONFIG._serialized_end=796
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/udp/config_grpc.py b/xray_api/proto/transport/internet/udp/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/udp/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/udp/config_pb2.py b/xray_api/proto/transport/internet/udp/config_pb2.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/udp/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#transport/internet/udp/config.proto\x12\x1bxray.transport.internet.udp\"\x08\n\x06\x43onfigBs\n\x1f\x63om.xray.transport.internet.udpP\x01Z0github.com/xtls/xray-core/transport/internet/udp\xaa\x02\x1bXray.Transport.Internet.Udpb\x06proto3')
+
+
+
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.udp.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.udp.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n\037com.xray.transport.internet.udpP\001Z0github.com/xtls/xray-core/transport/internet/udp\252\002\033Xray.Transport.Internet.Udp'
+ _CONFIG._serialized_start=68
+ _CONFIG._serialized_end=76
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proto/transport/internet/websocket/config_grpc.py b/xray_api/proto/transport/internet/websocket/config_grpc.py
@@ -0,0 +1,3 @@
+# Generated by the Protocol Buffers compiler. DO NOT EDIT!
+# source: transport/internet/websocket/config.proto
+# plugin: grpclib.plugin.main
diff --git a/xray_api/proto/transport/internet/websocket/config_pb2.py b/xray_api/proto/transport/internet/websocket/config_pb2.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: transport/internet/websocket/config.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)transport/internet/websocket/config.proto\x12!xray.transport.internet.websocket\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x82\x01\n\x06\x43onfig\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x39\n\x06header\x18\x03 \x03(\x0b\x32).xray.transport.internet.websocket.Header\x12\x1d\n\x15\x61\x63\x63\x65pt_proxy_protocol\x18\x04 \x01(\x08\x12\n\n\x02\x65\x64\x18\x05 \x01(\rJ\x04\x08\x01\x10\x02\x42\x85\x01\n%com.xray.transport.internet.websocketP\x01Z6github.com/xtls/xray-core/transport/internet/websocket\xaa\x02!Xray.Transport.Internet.Websocketb\x06proto3')
+
+
+
+_HEADER = DESCRIPTOR.message_types_by_name['Header']
+_CONFIG = DESCRIPTOR.message_types_by_name['Config']
+Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
+ 'DESCRIPTOR' : _HEADER,
+ '__module__' : 'transport.internet.websocket.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.websocket.Header)
+ })
+_sym_db.RegisterMessage(Header)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), {
+ 'DESCRIPTOR' : _CONFIG,
+ '__module__' : 'transport.internet.websocket.config_pb2'
+ # @@protoc_insertion_point(class_scope:xray.xray_api.proto.transport.internet.websocket.Config)
+ })
+_sym_db.RegisterMessage(Config)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\n%com.xray.transport.internet.websocketP\001Z6github.com/xtls/xray-core/transport/internet/websocket\252\002!Xray.Transport.Internet.Websocket'
+ _HEADER._serialized_start=80
+ _HEADER._serialized_end=116
+ _CONFIG._serialized_start=119
+ _CONFIG._serialized_end=249
+# @@protoc_insertion_point(module_scope)
diff --git a/xray_api/proxyman.py b/xray_api/proxyman.py
@@ -0,0 +1,91 @@
+import grpclib
+
+from .base import XRayBase
+from .exceptions import RelatedError
+from .proto.app.proxyman.command import command_pb2, command_grpc
+from .proto.common.protocol import user_pb2
+from .types.account import Account
+from .types.message import Message, TypedMessage
+
+#try:
+# from .proto.core import config_pb2 as core_config_pb2
+#except ModuleNotFoundError:
+# from .proto import config_pb2 as core_config_pb2
+
+
+class Proxyman(XRayBase):
+ async def alter_inbound(self, tag: str, operation: TypedMessage) -> bool:
+ self.create_channel()
+ stub = command_grpc.HandlerServiceStub(self._channel)
+ try:
+ await stub.AlterInbound(command_pb2.AlterInboundRequest(tag=tag, operation=operation))
+ return True
+
+ except grpclib.exceptions.GRPCError as e:
+ raise RelatedError(e)
+
+ async def alter_outbound(self, tag: str, operation: TypedMessage) -> bool:
+ self.create_channel()
+ stub = command_grpc.HandlerServiceStub(self._channel)
+ try:
+ await stub.AlterInbound(command_pb2.AlterOutboundRequest(tag=tag, operation=operation))
+ return True
+
+ except grpclib.exceptions.GRPCError as e:
+ raise RelatedError(e)
+
+ async def add_inbound_user(self, tag: str, user: Account) -> bool:
+ return await self.alter_inbound(
+ tag=tag,
+ operation=Message(
+ command_pb2.AddUserOperation(
+ user=user_pb2.User(
+ level=user.level,
+ email=user.email,
+ account=user.message
+ )
+ )
+ ))
+
+ async def remove_inbound_user(self, tag: str, email: str) -> bool:
+ return await self.alter_inbound(
+ tag=tag,
+ operation=Message(
+ command_pb2.RemoveUserOperation(
+ email=email
+ )
+ ))
+
+ async def add_outbound_user(self, tag: str, user: Account) -> bool:
+ return await self.alter_outbound(
+ tag=tag,
+ operation=Message(
+ command_pb2.AddUserOperation(
+ user=user_pb2.User(
+ level=user.level,
+ email=user.email,
+ account=user.message
+ )
+ )
+ ))
+
+ async def remove_outbound_user(self, tag: str, email: str) -> bool:
+ return await self.alter_outbound(
+ tag=tag,
+ operation=Message(
+ command_pb2.RemoveUserOperation(
+ email=email
+ )
+ ))
+
+ async def add_inbound(self):
+ raise NotImplementedError
+
+ async def remove_inbound(self):
+ raise NotImplementedError
+
+ async def add_outbound(self):
+ raise NotImplementedError
+
+ async def remove_outbound(self):
+ raise NotImplementedError
diff --git a/xray_api/stats.py b/xray_api/stats.py
@@ -0,0 +1,126 @@
+import typing
+from dataclasses import dataclass
+
+import grpclib
+
+from .base import XRayBase
+from .exceptions import RelatedError
+from .proto.app.stats.command import command_pb2, command_grpc
+
+
+@dataclass
+class SysStatsResponse:
+ num_goroutine: int
+ num_gc: int
+ alloc: int
+ total_alloc: int
+ sys: int
+ mallocs: int
+ frees: int
+ live_objects: int
+ pause_total_ns: int
+ uptime: int
+
+
+@dataclass
+class StatResponse:
+ name: str
+ type: str
+ link: str
+ value: int
+
+
+@dataclass
+class UserStatsResponse:
+ email: str
+ uplink: int
+ downlink: int
+
+
+@dataclass
+class InboundStatsResponse:
+ tag: str
+ uplink: int
+ downlink: int
+
+
+@dataclass
+class OutboundStatsResponse:
+ tag: str
+ uplink: int
+ downlink: int
+
+
+class Stats(XRayBase):
+ async def get_sys_stats(self) -> SysStatsResponse:
+ self.create_channel()
+ try:
+ stub = command_grpc.StatsServiceStub(self._channel)
+ r = await stub.GetSysStats(command_pb2.SysStatsRequest())
+
+ except grpclib.exceptions.GRPCError as e:
+ raise RelatedError(e)
+
+ return SysStatsResponse(
+ num_goroutine=r.NumGoroutine,
+ num_gc=r.NumGC,
+ alloc=r.Alloc,
+ total_alloc=r.TotalAlloc,
+ sys=r.Sys,
+ mallocs=r.Mallocs,
+ frees=r.Frees,
+ live_objects=r.LiveObjects,
+ pause_total_ns=r.PauseTotalNs,
+ uptime=r.Uptime
+ )
+
+ async def query_stats(self, pattern: str, reset: bool = False) -> typing.Iterable[StatResponse]:
+ self.create_channel()
+ try:
+ stub = command_grpc.StatsServiceStub(self._channel)
+ r = await stub.QueryStats(command_pb2.QueryStatsRequest(pattern=pattern, reset=reset))
+
+ except grpclib.exceptions.GRPCError as e:
+ raise RelatedError(e)
+ results = []
+ for stat in r.stat:
+ typ, name, _, link = stat.name.split('>>>')
+ results.append(StatResponse(name, typ, link, stat.value))
+ return results
+
+ async def get_users_stats(self, reset: bool = False) -> typing.Iterable[StatResponse]:
+ return await self.query_stats("user>>>", reset=reset)
+
+ async def get_inbounds_stats(self, reset: bool = False) -> typing.Iterable[StatResponse]:
+ return await self.query_stats("inbound>>>", reset=reset)
+
+ async def get_outbounds_stats(self, reset: bool = False) -> typing.Iterable[StatResponse]:
+ return await self.query_stats("outbound>>>", reset=reset)
+
+ async def get_user_stats(self, email: str, reset: bool = False) -> typing.Iterable[StatResponse]:
+ uplink, downlink = 0, 0
+ for stat in await self.query_stats(f"user>>>{email}>>>", reset=reset):
+ if stat.link == 'uplink':
+ uplink = stat.value
+ if stat.link == 'downlink':
+ downlink = stat.value
+
+ return UserStatsResponse(email=email, uplink=uplink, downlink=downlink)
+
+ async def get_inbound_stats(self, tag: str, reset: bool = False) -> typing.Iterable[StatResponse]:
+ uplink, downlink = 0, 0
+ for stat in await self.query_stats(f"inbound>>>{tag}>>>", reset=reset):
+ if stat.link == 'uplink':
+ uplink = stat.value
+ if stat.link == 'downlink':
+ downlink = stat.value
+ return InboundStatsResponse(tag=tag, uplink=uplink, downlink=downlink)
+
+ async def get_outbound_stats(self, tag: str, reset: bool = False) -> typing.Iterable[StatResponse]:
+ uplink, downlink = 0, 0
+ for stat in await self.query_stats(f"outbound>>>{tag}>>>", reset=reset):
+ if stat.link == 'uplink':
+ uplink = stat.value
+ if stat.link == 'downlink':
+ downlink = stat.value
+ return OutboundStatsResponse(tag=tag, uplink=uplink, downlink=downlink)
diff --git a/xray_api/types/__init__.py b/xray_api/types/__init__.py
@@ -0,0 +1,3 @@
+from .account import (Account, ShadowsocksAccount, TrojanAccount, VLESSAccount,
+ VMessAccount)
+from .message import Message, TypedMessage
diff --git a/xray_api/types/account.py b/xray_api/types/account.py
@@ -0,0 +1,76 @@
+from abc import ABC, abstractproperty
+from enum import Enum
+from uuid import UUID
+
+from pydantic import BaseModel
+
+from ..proto.common.serial.typed_message_pb2 import TypedMessage
+from ..proto.proxy.shadowsocks.config_pb2 import \
+ Account as ShadowsocksAccountPb2
+#from ..proto.proxy.shadowsocks.config_pb2 import \
+# CipherType as ShadowsocksCiphers
+from ..proto.proxy.trojan.config_pb2 import Account as TrojanAccountPb2
+from ..proto.proxy.vless.account_pb2 import Account as VLESSAccountPb2
+from ..proto.proxy.vmess.account_pb2 import Account as VMessAccountPb2
+from .message import Message
+
+
+class Account(BaseModel, ABC):
+ email: str
+ level: int = 0
+
+ @abstractproperty
+ def message(self) -> TypedMessage:
+ pass
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.email}>"
+
+
+class VMessAccount(Account):
+ id: UUID
+
+ @property
+ def message(self):
+ return Message(VMessAccountPb2(id=str(self.id)))
+
+
+class XTLSFlows(Enum):
+ NONE = ''
+ VISION = 'xtls-rprx-vision'
+
+
+class VLESSAccount(Account):
+ id: UUID
+ flow: XTLSFlows = XTLSFlows.NONE
+
+ @property
+ def message(self):
+ return Message(VLESSAccountPb2(id=str(self.id), flow=self.flow.value))
+
+
+class TrojanAccount(Account):
+ password: str
+
+ @property
+ def message(self):
+ return Message(TrojanAccountPb2(password=self.password))
+
+
+class ShadowsocksMethods(Enum):
+ AES_128_GCM = 'aes-128-gcm'
+ AES_256_GCM = 'aes-256-gcm'
+ CHACHA20_POLY1305 = 'chacha20-ietf-poly1305'
+
+
+class ShadowsocksAccount(Account):
+ password: str
+ method: ShadowsocksMethods = ShadowsocksMethods.CHACHA20_POLY1305
+
+ @property
+ def cipher_type(self):
+ return self.method.name
+
+ @property
+ def message(self):
+ return Message(ShadowsocksAccountPb2(password=self.password, cipher_type=self.cipher_type))
diff --git a/xray_api/types/message.py b/xray_api/types/message.py
@@ -0,0 +1,9 @@
+from ..proto.common.serial.typed_message_pb2 import TypedMessage
+
+
+class Message:
+ def __new__(cls, message) -> TypedMessage:
+ return TypedMessage(
+ type=message.DESCRIPTOR.full_name,
+ value=message.SerializeToString()
+ )
diff --git a/xray_config.json b/xray_config.json
@@ -0,0 +1,189 @@
+{
+ "log": {
+ "loglevel": "info"
+ },
+ "inbounds": [
+ {
+ "tag": "VMess TCP",
+ "listen": "0.0.0.0",
+ "port": 8081,
+ "protocol": "vmess",
+ "settings": {
+ "clients": []
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "tcpSettings": {
+ "header": {
+ "type": "http",
+ "request": {
+ "method": "GET",
+ "path": [
+ "/"
+ ],
+ "headers": {
+ "Host": [
+ "google.com"
+ ]
+ }
+ },
+ "response": {}
+ }
+ },
+ "security": "none"
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls"
+ ]
+ }
+ },
+ {
+ "tag": "VMess Websocket",
+ "listen": "0.0.0.0",
+ "port": 8080,
+ "protocol": "vmess",
+ "settings": {
+ "clients": []
+ },
+ "streamSettings": {
+ "network": "ws",
+ "wsSettings": {
+ "path": "/",
+ "headers": {
+ "Host": "google.com"
+ }
+ },
+ "security": "none"
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls"
+ ]
+ }
+ },
+ {
+ "tag": "VLESS TCP REALITY",
+ "listen": "0.0.0.0",
+ "port": 8443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "tcpSettings": {},
+ "security": "reality",
+ "realitySettings": {
+ "show": false,
+ "dest": "discordapp.com:443",
+ "xver": 0,
+ "serverNames": [
+ "cdn.discordapp.com",
+ "discordapp.com"
+ ],
+ "privateKey": "MMX7m0Mj3faUstoEm5NBdegeXkHG6ZB78xzBv2n3ZUA",
+ "shortIds": [
+ "",
+ "6ba85179e30d4fc2"
+ ]
+ }
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls"
+ ]
+ }
+ },
+ {
+ "tag": "VLESS GRPC REALITY",
+ "listen": "0.0.0.0",
+ "port": 2053,
+ "protocol": "vless",
+ "settings": {
+ "clients": [],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "grpc",
+ "grpcSettings": {
+ "serviceName": "xyz"
+ },
+ "security": "reality",
+ "realitySettings": {
+ "show": false,
+ "dest": "discordapp.com:443",
+ "xver": 0,
+ "serverNames": [
+ "cdn.discordapp.com",
+ "discordapp.com"
+ ],
+ "privateKey": "MMX7m0Mj3faUstoEm5NBdegeXkHG6ZB78xzBv2n3ZUA",
+ "shortIds": [
+ "",
+ "6ba85179e30d4fc2"
+ ]
+ }
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls"
+ ]
+ }
+ },
+ {
+ "tag": "Shadowsocks TCP",
+ "listen": "0.0.0.0",
+ "port": 1080,
+ "protocol": "shadowsocks",
+ "settings": {
+ "clients": [],
+ "network": "tcp,udp"
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "DIRECT"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "BLOCK"
+ }
+ ],
+ "routing": {
+ "rules": [
+ {
+ "ip": [
+ "geoip:private"
+ ],
+ "outboundTag": "BLOCK",
+ "type": "field"
+ },
+ {
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "BLOCK",
+ "type": "field"
+ },
+ {
+ "protocol": [
+ "bittorrent"
+ ],
+ "outboundTag": "BLOCK",
+ "type": "field"
+ }
+ ]
+ }
+}