From 8aff8f2fb541c4a416570b41a599a8ffdf2c9cfc Mon Sep 17 00:00:00 2001
From: Ultradesu
Date: Thu, 18 Sep 2025 02:56:59 +0300
Subject: [PATCH] init rust. WIP: tls for inbounds
---
.env.example | 31 +
.github/workflows/main.yml | 51 -
.gitignore | 17 +-
.vscode/launch.json | 64 -
API.md | 73 +
Cargo.lock | 4252 +++++++++++++++++
Cargo.toml | 59 +
Dockerfile | 40 -
LICENSE | 13 -
LLM_PROJECT_CONTEXT.md | 132 +
README.md | 58 -
SECURITY.md | 21 -
buildx.yaml | 5 -
cleanup_analysis.sql | 15 -
cleanup_options.sql | 35 -
docker-compose.yaml | 102 -
manage.py | 22 -
mysite/__init__.py | 3 -
mysite/asgi.py | 16 -
mysite/celery.py | 40 -
mysite/context_processors.py | 42 -
mysite/middleware.py | 22 -
mysite/settings.py | 233 -
mysite/urls.py | 30 -
mysite/wsgi.py | 16 -
requirements.txt | 22 -
src/config/args.rs | 60 +
src/config/env.rs | 104 +
src/config/file.rs | 165 +
src/config/mod.rs | 244 +
src/database/entities/certificate.rs | 243 +
src/database/entities/inbound_template.rs | 278 ++
src/database/entities/inbound_users.rs | 168 +
src/database/entities/mod.rs | 16 +
src/database/entities/server.rs | 212 +
src/database/entities/server_inbound.rs | 204 +
src/database/entities/user.rs | 185 +
src/database/entities/user_access.rs | 188 +
.../m20241201_000001_create_users_table.rs | 135 +
...241201_000002_create_certificates_table.rs | 120 +
...1_000003_create_inbound_templates_table.rs | 155 +
.../m20241201_000004_create_servers_table.rs | 136 +
...201_000005_create_server_inbounds_table.rs | 195 +
...0241201_000006_create_user_access_table.rs | 196 +
...41201_000007_create_inbound_users_table.rs | 125 +
src/database/migrations/mod.rs | 26 +
src/database/mod.rs | 161 +
src/database/repository/certificate.rs | 75 +
src/database/repository/inbound_template.rs | 65 +
src/database/repository/inbound_users.rs | 132 +
src/database/repository/mod.rs | 15 +
src/database/repository/server.rs | 79 +
src/database/repository/server_inbound.rs | 159 +
src/database/repository/user.rs | 157 +
src/database/repository/user_access.rs | 118 +
src/main.rs | 186 +
src/services/certificates.rs | 41 +
src/services/events.rs | 30 +
src/services/mod.rs | 7 +
src/services/tasks.rs | 484 ++
src/services/xray/client.rs | 91 +
src/services/xray/config.rs | 285 ++
src/services/xray/inbounds.rs | 325 ++
src/services/xray/mod.rs | 213 +
src/services/xray/stats.rs | 70 +
src/services/xray/users.rs | 150 +
src/web/handlers/certificates.rs | 137 +
src/web/handlers/mod.rs | 9 +
src/web/handlers/servers.rs | 575 +++
src/web/handlers/templates.rs | 88 +
src/web/handlers/users.rs | 206 +
src/web/mod.rs | 70 +
src/web/routes/mod.rs | 27 +
src/web/routes/servers.rs | 38 +
static/admin.html | 1314 +++++
static/admin/css/main.css | 239 -
static/admin/css/vpn_admin.css | 342 --
static/admin/js/generate_link.js | 203 -
static/admin/js/server_status_check.js | 94 -
static/admin/js/xray_inbound_defaults.js | 289 --
static/index.html | 1289 +++++
telegram_bot/__init__.py | 0
telegram_bot/admin.py | 854 ----
telegram_bot/apps.py | 75 -
telegram_bot/bot.py | 1897 --------
telegram_bot/localization.py | 267 --
telegram_bot/management/__init__.py | 0
telegram_bot/management/commands/__init__.py | 0
.../management/commands/run_telegram_bot.py | 99 -
.../commands/telegram_bot_status.py | 112 -
telegram_bot/migrations/0001_initial.py | 70 -
.../0002_add_connection_settings.py | 33 -
telegram_bot/migrations/0003_accessrequest.py | 42 -
..._telegram_bo_status_cf9310_idx_and_more.py | 37 -
...ve_botsettings_welcome_message_and_more.py | 25 -
.../0006_accessrequest_desired_username.py | 18 -
...emove_botsettings_help_message_and_more.py | 27 -
...08_accessrequest_selected_existing_user.py | 28 -
...ccessrequest_selected_inbounds_and_more.py | 24 -
.../0010_botsettings_telegram_admins.py | 20 -
telegram_bot/migrations/__init__.py | 0
telegram_bot/models.py | 318 --
telegram_bot/tests.py | 3 -
telegram_bot/views.py | 3 -
telegram_bot_locks/telegram_bot.lock | 0
templates/admin/create_xray_inbound.html | 202 -
vpn/__init__.py | 0
vpn/admin.py | 169 -
vpn/admin/__init__.py | 45 -
vpn/admin/access.py | 485 --
vpn/admin/base.py | 57 -
vpn/admin/logs.py | 179 -
vpn/admin/server.py | 864 ----
vpn/admin/user.py | 702 ---
vpn/admin_minimal.py | 73 -
vpn/admin_test.py | 16 -
vpn/admin_xray.py | 1056 ----
vpn/apps.py | 109 -
vpn/forms.py | 7 -
vpn/letsencrypt/__init__.py | 13 -
vpn/letsencrypt/letsencrypt_dns.py | 403 --
vpn/management/__init__.py | 1 -
.../commands/cleanup_access_logs.py | 158 -
vpn/management/commands/cleanup_logs.py | 208 -
vpn/management/commands/create_admin.py | 17 -
vpn/management/commands/init_statistics.py | 1 -
.../commands/simple_cleanup_logs.py | 51 -
vpn/migrations/0001_initial.py | 139 -
vpn/migrations/0002_taskexecutionlog.py | 51 -
.../0003_acllink_last_access_time.py | 18 -
...r_options_alter_server_options_and_more.py | 53 -
vpn/migrations/0004_merge_20250721_1223.py | 14 -
vpn/migrations/0005_userstatistics.py | 45 -
vpn/migrations/0006_accesslog_acl_link_id.py | 22 -
...vpn_usersta_user_id_512036_idx_and_more.py | 23 -
vpn/migrations/0007_merge_20250721_1345.py | 14 -
...3c6e_idx_vpn_accessl_acl_lin_9f3bc5_idx.py | 18 -
...xraycoreserver_alter_server_server_type.py | 42 -
...ove_xraycoreserver_api_address_and_more.py | 137 -
.../0011_xrayinboundproxy_and_more.py | 34 -
...ayinboundserver_delete_xrayinboundproxy.py | 29 -
vpn/migrations/0013_add_client_hostname.py | 18 -
...xraycoreserver_client_hostname_and_more.py | 23 -
vpn/migrations/0015_remove_old_xray_models.py | 32 -
vpn/migrations/0016_add_new_xray_models.py | 127 -
..._alter_server_server_type_serverinbound.py | 52 -
...er_certificate_certificate_pem_and_more.py | 28 -
vpn/migrations/0019_certificate_acme_email.py | 18 -
.../0020_alter_inbound_full_config.py | 18 -
.../0021_remove_xray_configuration.py | 16 -
.../0022_remove_inbound_domain_field.py | 21 -
.../0023_alter_subscriptiongroup_options.py | 17 -
.../0024_add_certificate_to_serverinbound.py | 23 -
...t_name_user_telegram_last_name_and_more.py | 38 -
.../0026_alter_subscriptiongroup_options.py | 17 -
vpn/migrations/__init__.py | 1 -
vpn/models.py | 208 -
vpn/models_xray.py | 464 --
vpn/server_plugins/__init__.py | 5 -
vpn/server_plugins/generic.py | 62 -
vpn/server_plugins/outline.py | 823 ----
vpn/server_plugins/urls.py | 7 -
vpn/server_plugins/wireguard.py | 83 -
vpn/server_plugins/xray_v2.py | 966 ----
vpn/signals.py | 357 --
vpn/tasks.py | 1192 -----
vpn/templates/admin/base_site.html | 37 -
vpn/templates/admin/move_clients.html | 499 --
.../admin/polls/user/change_form.html | 10 -
vpn/templates/admin/purge_users.html | 304 --
vpn/templates/admin/simple_move_clients.html | 93 -
.../admin/vpn/acllink/change_list.html | 47 -
.../admin/vpn/certificate/change_form.html | 18 -
.../admin/vpn/certificate/change_list.html | 18 -
.../admin/vpn/inbound/change_form.html | 18 -
.../admin/vpn/inbound/change_list.html | 18 -
.../vpn/outlineserver/add_form.html.backup | 176 -
.../admin/vpn/outlineserver/change_form.html | 23 -
.../admin/vpn/server/change_form.html | 39 -
.../admin/vpn/server/change_list.html | 166 -
.../admin/vpn/server/change_list.html.backup | 11 -
.../vpn/subscriptiongroup/change_form.html | 18 -
.../vpn/subscriptiongroup/change_list.html | 18 -
.../vpn/taskexecutionlog/change_list.html | 5 -
vpn/templates/admin/vpn/user/change_form.html | 219 -
.../vpn/usersubscription/change_form.html | 18 -
.../vpn/usersubscription/change_list.html | 18 -
vpn/templates/vpn/user_portal.html | 765 ---
vpn/templates/vpn/user_portal_error.html | 148 -
vpn/tests.py | 3 -
vpn/utils.py | 21 -
vpn/views.py | 635 ---
vpn/xray_api_v2/__init__.py | 62 -
vpn/xray_api_v2/client.py | 235 -
vpn/xray_api_v2/commands/__init__.py | 0
vpn/xray_api_v2/commands/user.py | 33 -
vpn/xray_api_v2/exceptions.py | 31 -
vpn/xray_api_v2/models/__init__.py | 55 -
vpn/xray_api_v2/models/base.py | 97 -
vpn/xray_api_v2/models/inbound.py | 176 -
vpn/xray_api_v2/models/protocols.py | 266 --
vpn/xray_api_v2/models/security.py | 389 --
vpn/xray_api_v2/models/transports.py | 241 -
vpn/xray_api_v2/serializers/__init__.py | 0
vpn/xray_api_v2/stats.py | 184 -
vpn/xray_api_v2/subscription.py | 392 --
206 files changed, 14301 insertions(+), 21560 deletions(-)
create mode 100644 .env.example
delete mode 100755 .github/workflows/main.yml
delete mode 100644 .vscode/launch.json
create mode 100644 API.md
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
delete mode 100644 Dockerfile
delete mode 100755 LICENSE
create mode 100644 LLM_PROJECT_CONTEXT.md
delete mode 100755 README.md
delete mode 100644 SECURITY.md
delete mode 100755 buildx.yaml
delete mode 100644 cleanup_analysis.sql
delete mode 100644 cleanup_options.sql
delete mode 100644 docker-compose.yaml
delete mode 100755 manage.py
delete mode 100644 mysite/__init__.py
delete mode 100644 mysite/asgi.py
delete mode 100644 mysite/celery.py
delete mode 100644 mysite/context_processors.py
delete mode 100644 mysite/middleware.py
delete mode 100644 mysite/settings.py
delete mode 100644 mysite/urls.py
delete mode 100644 mysite/wsgi.py
delete mode 100644 requirements.txt
create mode 100644 src/config/args.rs
create mode 100644 src/config/env.rs
create mode 100644 src/config/file.rs
create mode 100644 src/config/mod.rs
create mode 100644 src/database/entities/certificate.rs
create mode 100644 src/database/entities/inbound_template.rs
create mode 100644 src/database/entities/inbound_users.rs
create mode 100644 src/database/entities/mod.rs
create mode 100644 src/database/entities/server.rs
create mode 100644 src/database/entities/server_inbound.rs
create mode 100644 src/database/entities/user.rs
create mode 100644 src/database/entities/user_access.rs
create mode 100644 src/database/migrations/m20241201_000001_create_users_table.rs
create mode 100644 src/database/migrations/m20241201_000002_create_certificates_table.rs
create mode 100644 src/database/migrations/m20241201_000003_create_inbound_templates_table.rs
create mode 100644 src/database/migrations/m20241201_000004_create_servers_table.rs
create mode 100644 src/database/migrations/m20241201_000005_create_server_inbounds_table.rs
create mode 100644 src/database/migrations/m20241201_000006_create_user_access_table.rs
create mode 100644 src/database/migrations/m20241201_000007_create_inbound_users_table.rs
create mode 100644 src/database/migrations/mod.rs
create mode 100644 src/database/mod.rs
create mode 100644 src/database/repository/certificate.rs
create mode 100644 src/database/repository/inbound_template.rs
create mode 100644 src/database/repository/inbound_users.rs
create mode 100644 src/database/repository/mod.rs
create mode 100644 src/database/repository/server.rs
create mode 100644 src/database/repository/server_inbound.rs
create mode 100644 src/database/repository/user.rs
create mode 100644 src/database/repository/user_access.rs
create mode 100644 src/main.rs
create mode 100644 src/services/certificates.rs
create mode 100644 src/services/events.rs
create mode 100644 src/services/mod.rs
create mode 100644 src/services/tasks.rs
create mode 100644 src/services/xray/client.rs
create mode 100644 src/services/xray/config.rs
create mode 100644 src/services/xray/inbounds.rs
create mode 100644 src/services/xray/mod.rs
create mode 100644 src/services/xray/stats.rs
create mode 100644 src/services/xray/users.rs
create mode 100644 src/web/handlers/certificates.rs
create mode 100644 src/web/handlers/mod.rs
create mode 100644 src/web/handlers/servers.rs
create mode 100644 src/web/handlers/templates.rs
create mode 100644 src/web/handlers/users.rs
create mode 100644 src/web/mod.rs
create mode 100644 src/web/routes/mod.rs
create mode 100644 src/web/routes/servers.rs
create mode 100644 static/admin.html
delete mode 100644 static/admin/css/main.css
delete mode 100644 static/admin/css/vpn_admin.css
delete mode 100644 static/admin/js/generate_link.js
delete mode 100644 static/admin/js/server_status_check.js
delete mode 100644 static/admin/js/xray_inbound_defaults.js
create mode 100644 static/index.html
delete mode 100644 telegram_bot/__init__.py
delete mode 100644 telegram_bot/admin.py
delete mode 100644 telegram_bot/apps.py
delete mode 100644 telegram_bot/bot.py
delete mode 100644 telegram_bot/localization.py
delete mode 100644 telegram_bot/management/__init__.py
delete mode 100644 telegram_bot/management/commands/__init__.py
delete mode 100644 telegram_bot/management/commands/run_telegram_bot.py
delete mode 100644 telegram_bot/management/commands/telegram_bot_status.py
delete mode 100644 telegram_bot/migrations/0001_initial.py
delete mode 100644 telegram_bot/migrations/0002_add_connection_settings.py
delete mode 100644 telegram_bot/migrations/0003_accessrequest.py
delete mode 100644 telegram_bot/migrations/0004_remove_accessrequest_telegram_bo_status_cf9310_idx_and_more.py
delete mode 100644 telegram_bot/migrations/0005_delete_botstatus_remove_botsettings_welcome_message_and_more.py
delete mode 100644 telegram_bot/migrations/0006_accessrequest_desired_username.py
delete mode 100644 telegram_bot/migrations/0007_remove_botsettings_help_message_and_more.py
delete mode 100644 telegram_bot/migrations/0008_accessrequest_selected_existing_user.py
delete mode 100644 telegram_bot/migrations/0009_accessrequest_selected_inbounds_and_more.py
delete mode 100644 telegram_bot/migrations/0010_botsettings_telegram_admins.py
delete mode 100644 telegram_bot/migrations/__init__.py
delete mode 100644 telegram_bot/models.py
delete mode 100644 telegram_bot/tests.py
delete mode 100644 telegram_bot/views.py
delete mode 100644 telegram_bot_locks/telegram_bot.lock
delete mode 100644 templates/admin/create_xray_inbound.html
delete mode 100644 vpn/__init__.py
delete mode 100644 vpn/admin.py
delete mode 100644 vpn/admin/__init__.py
delete mode 100644 vpn/admin/access.py
delete mode 100644 vpn/admin/base.py
delete mode 100644 vpn/admin/logs.py
delete mode 100644 vpn/admin/server.py
delete mode 100644 vpn/admin/user.py
delete mode 100644 vpn/admin_minimal.py
delete mode 100644 vpn/admin_test.py
delete mode 100644 vpn/admin_xray.py
delete mode 100644 vpn/apps.py
delete mode 100644 vpn/forms.py
delete mode 100644 vpn/letsencrypt/__init__.py
delete mode 100644 vpn/letsencrypt/letsencrypt_dns.py
delete mode 100644 vpn/management/__init__.py
delete mode 100644 vpn/management/commands/cleanup_access_logs.py
delete mode 100644 vpn/management/commands/cleanup_logs.py
delete mode 100644 vpn/management/commands/create_admin.py
delete mode 100644 vpn/management/commands/init_statistics.py
delete mode 100644 vpn/management/commands/simple_cleanup_logs.py
delete mode 100644 vpn/migrations/0001_initial.py
delete mode 100644 vpn/migrations/0002_taskexecutionlog.py
delete mode 100644 vpn/migrations/0003_acllink_last_access_time.py
delete mode 100644 vpn/migrations/0003_alter_outlineserver_options_alter_server_options_and_more.py
delete mode 100644 vpn/migrations/0004_merge_20250721_1223.py
delete mode 100644 vpn/migrations/0005_userstatistics.py
delete mode 100644 vpn/migrations/0006_accesslog_acl_link_id.py
delete mode 100644 vpn/migrations/0006_rename_vpn_usersta_user_id_1c7cd0_idx_vpn_usersta_user_id_512036_idx_and_more.py
delete mode 100644 vpn/migrations/0007_merge_20250721_1345.py
delete mode 100644 vpn/migrations/0008_rename_vpn_accessl_acl_lin_b23c6e_idx_vpn_accessl_acl_lin_9f3bc5_idx.py
delete mode 100644 vpn/migrations/0009_xraycoreserver_alter_server_server_type.py
delete mode 100644 vpn/migrations/0010_remove_xraycoreserver_api_address_and_more.py
delete mode 100644 vpn/migrations/0011_xrayinboundproxy_and_more.py
delete mode 100644 vpn/migrations/0012_xrayinboundserver_delete_xrayinboundproxy.py
delete mode 100644 vpn/migrations/0013_add_client_hostname.py
delete mode 100644 vpn/migrations/0014_alter_xraycoreserver_client_hostname_and_more.py
delete mode 100644 vpn/migrations/0015_remove_old_xray_models.py
delete mode 100644 vpn/migrations/0016_add_new_xray_models.py
delete mode 100644 vpn/migrations/0017_xrayserverv2_alter_server_server_type_serverinbound.py
delete mode 100644 vpn/migrations/0018_alter_certificate_certificate_pem_and_more.py
delete mode 100644 vpn/migrations/0019_certificate_acme_email.py
delete mode 100644 vpn/migrations/0020_alter_inbound_full_config.py
delete mode 100644 vpn/migrations/0021_remove_xray_configuration.py
delete mode 100644 vpn/migrations/0022_remove_inbound_domain_field.py
delete mode 100644 vpn/migrations/0023_alter_subscriptiongroup_options.py
delete mode 100644 vpn/migrations/0024_add_certificate_to_serverinbound.py
delete mode 100644 vpn/migrations/0025_user_telegram_first_name_user_telegram_last_name_and_more.py
delete mode 100644 vpn/migrations/0026_alter_subscriptiongroup_options.py
delete mode 100644 vpn/migrations/__init__.py
delete mode 100644 vpn/models.py
delete mode 100644 vpn/models_xray.py
delete mode 100644 vpn/server_plugins/__init__.py
delete mode 100644 vpn/server_plugins/generic.py
delete mode 100644 vpn/server_plugins/outline.py
delete mode 100644 vpn/server_plugins/urls.py
delete mode 100644 vpn/server_plugins/wireguard.py
delete mode 100644 vpn/server_plugins/xray_v2.py
delete mode 100644 vpn/signals.py
delete mode 100644 vpn/tasks.py
delete mode 100644 vpn/templates/admin/base_site.html
delete mode 100644 vpn/templates/admin/move_clients.html
delete mode 100644 vpn/templates/admin/polls/user/change_form.html
delete mode 100644 vpn/templates/admin/purge_users.html
delete mode 100644 vpn/templates/admin/simple_move_clients.html
delete mode 100644 vpn/templates/admin/vpn/acllink/change_list.html
delete mode 100644 vpn/templates/admin/vpn/certificate/change_form.html
delete mode 100644 vpn/templates/admin/vpn/certificate/change_list.html
delete mode 100644 vpn/templates/admin/vpn/inbound/change_form.html
delete mode 100644 vpn/templates/admin/vpn/inbound/change_list.html
delete mode 100644 vpn/templates/admin/vpn/outlineserver/add_form.html.backup
delete mode 100644 vpn/templates/admin/vpn/outlineserver/change_form.html
delete mode 100644 vpn/templates/admin/vpn/server/change_form.html
delete mode 100644 vpn/templates/admin/vpn/server/change_list.html
delete mode 100644 vpn/templates/admin/vpn/server/change_list.html.backup
delete mode 100644 vpn/templates/admin/vpn/subscriptiongroup/change_form.html
delete mode 100644 vpn/templates/admin/vpn/subscriptiongroup/change_list.html
delete mode 100644 vpn/templates/admin/vpn/taskexecutionlog/change_list.html
delete mode 100644 vpn/templates/admin/vpn/user/change_form.html
delete mode 100644 vpn/templates/admin/vpn/usersubscription/change_form.html
delete mode 100644 vpn/templates/admin/vpn/usersubscription/change_list.html
delete mode 100644 vpn/templates/vpn/user_portal.html
delete mode 100644 vpn/templates/vpn/user_portal_error.html
delete mode 100644 vpn/tests.py
delete mode 100644 vpn/utils.py
delete mode 100644 vpn/views.py
delete mode 100644 vpn/xray_api_v2/__init__.py
delete mode 100644 vpn/xray_api_v2/client.py
delete mode 100644 vpn/xray_api_v2/commands/__init__.py
delete mode 100644 vpn/xray_api_v2/commands/user.py
delete mode 100644 vpn/xray_api_v2/exceptions.py
delete mode 100644 vpn/xray_api_v2/models/__init__.py
delete mode 100644 vpn/xray_api_v2/models/base.py
delete mode 100644 vpn/xray_api_v2/models/inbound.py
delete mode 100644 vpn/xray_api_v2/models/protocols.py
delete mode 100644 vpn/xray_api_v2/models/security.py
delete mode 100644 vpn/xray_api_v2/models/transports.py
delete mode 100644 vpn/xray_api_v2/serializers/__init__.py
delete mode 100644 vpn/xray_api_v2/stats.py
delete mode 100644 vpn/xray_api_v2/subscription.py
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e07d009
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,31 @@
+# Environment Variables Example for Xray Admin Panel
+# Copy this file to .env and modify the values as needed
+
+# Database Configuration
+DATABASE_URL=postgresql://xray_admin:password@localhost:5432/xray_admin
+XRAY_ADMIN__DATABASE__MAX_CONNECTIONS=20
+XRAY_ADMIN__DATABASE__CONNECTION_TIMEOUT=30
+XRAY_ADMIN__DATABASE__AUTO_MIGRATE=true
+
+# Web Server Configuration
+XRAY_ADMIN__WEB__HOST=0.0.0.0
+XRAY_ADMIN__WEB__PORT=8080
+XRAY_ADMIN__WEB__JWT_SECRET=your-super-secret-jwt-key-change-this
+XRAY_ADMIN__WEB__JWT_EXPIRY=86400
+
+# Telegram Bot Configuration
+TELEGRAM_BOT_TOKEN=1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi
+XRAY_ADMIN__TELEGRAM__WEBHOOK_URL=https://your-domain.com/telegram/webhook
+
+# Xray Configuration
+XRAY_ADMIN__XRAY__DEFAULT_API_PORT=62789
+XRAY_ADMIN__XRAY__HEALTH_CHECK_INTERVAL=30
+
+# Logging Configuration
+XRAY_ADMIN__LOGGING__LEVEL=info
+XRAY_ADMIN__LOGGING__FILE_PATH=./logs/xray-admin.log
+XRAY_ADMIN__LOGGING__JSON_FORMAT=false
+
+# Runtime Environment
+RUST_ENV=development
+ENVIRONMENT=development
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
deleted file mode 100755
index 7f11649..0000000
--- a/.github/workflows/main.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: Docker hub build
-
-on:
- push:
- branches:
- - 'django'
-
-jobs:
- docker:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- -
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- -
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- -
- name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Set outputs
- id: vars
- run: |
- echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- echo "sha_full=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- echo "build_date=$(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT
- echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
- - name: Check outputs
- run: |
- echo "Short SHA: ${{ steps.vars.outputs.sha_short }}"
- echo "Full SHA: ${{ steps.vars.outputs.sha_full }}"
- echo "Build Date: ${{ steps.vars.outputs.build_date }}"
- echo "Branch: ${{ steps.vars.outputs.branch_name }}"
- -
- name: Build and push
- uses: docker/build-push-action@v5
- with:
- platforms: linux/amd64,linux/arm64
- push: true
- cache-from: type=registry,ref=ultradesu/outfleet:buildcache
- cache-to: type=registry,ref=ultradesu/outfleet:buildcache,mode=max
- build-args: |
- GIT_COMMIT=${{ steps.vars.outputs.sha_full }}
- GIT_COMMIT_SHORT=${{ steps.vars.outputs.sha_short }}
- BUILD_DATE=${{ steps.vars.outputs.build_date }}
- BRANCH_NAME=${{ steps.vars.outputs.branch_name }}
- tags: ultradesu/outfleet:v2,ultradesu/outfleet:${{ steps.vars.outputs.sha_short }}
diff --git a/.gitignore b/.gitignore
index b07e67a..fc7923b 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,10 @@
-db.sqlite3
-debug.log
*.swp
*.swo
-*.pyc
-staticfiles/
-*.__pycache__.*
-celerybeat-schedule*
+
+/target/
+config.toml
# macOS system files
._*
.DS_Store
-# Virtual environments
-venv/
-.venv/
-env/
-
-# Temporary files
-/tmp/
-*.tmp
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index bf46a1d..0000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Django VPN app",
- "type": "debugpy",
- "request": "launch",
- "env": {
- "POSTGRES_PORT": "5433",
- "DJANGO_SETTINGS_MODULE": "mysite.settings",
- "EXTERNAL_ADDRESS": "http://localhost:8000"
- },
- "args": [
- "runserver",
- "0.0.0.0:8000"
- ],
- "django": true,
- "autoStartBrowser": false,
- "program": "${workspaceFolder}/manage.py"
- },
- {
- "name": "Celery Worker",
- "type": "debugpy",
- "request": "launch",
- "module": "celery",
- "args": [
- "-A", "mysite",
- "worker",
- "--loglevel=info"
- ],
- "env": {
- "POSTGRES_PORT": "5433",
- "DJANGO_SETTINGS_MODULE": "mysite.settings"
- },
- "console": "integratedTerminal"
- },
- {
- "name": "Celery Beat",
- "type": "debugpy",
- "request": "launch",
- "module": "celery",
- "args": [
- "-A", "mysite",
- "beat",
- "--loglevel=info"
- ],
- "env": {
- "POSTGRES_PORT": "5433",
- "DJANGO_SETTINGS_MODULE": "mysite.settings"
- },
- "console": "integratedTerminal"
- }
- ],
- "compounds": [
- {
- "name": "Run Django, Celery Worker, and Celery Beat",
- "configurations": [
- "Django VPN app",
- "Celery Worker",
- "Celery Beat"
- ]
- }
- ]
-}
diff --git a/API.md b/API.md
new file mode 100644
index 0000000..c5be905
--- /dev/null
+++ b/API.md
@@ -0,0 +1,73 @@
+# User Management API
+
+Base URL: `http://localhost:8080/api`
+
+## Endpoints
+
+### Health Check
+- `GET /` - Service health check
+
+### Users
+
+#### List Users
+- `GET /users?page=1&per_page=20` - Get paginated list of users
+
+#### Search Users
+- `GET /users/search?q=john&page=1&per_page=20` - Search users by name
+
+#### Get User
+- `GET /users/{id}` - Get user by ID
+
+#### Create User
+- `POST /users` - Create new user
+```json
+{
+ "name": "John Doe",
+ "comment": "Admin user",
+ "telegram_id": 123456789
+}
+```
+
+#### Update User
+- `PUT /users/{id}` - Update user by ID
+```json
+{
+ "name": "Jane Doe",
+ "comment": null,
+ "telegram_id": 987654321
+}
+```
+
+#### Delete User
+- `DELETE /users/{id}` - Delete user by ID
+
+## Response Format
+
+### User Object
+```json
+{
+ "id": "uuid",
+ "name": "string",
+ "comment": "string|null",
+ "telegram_id": "number|null",
+ "created_at": "timestamp",
+ "updated_at": "timestamp"
+}
+```
+
+### Users List Response
+```json
+{
+ "users": [UserObject],
+ "total": 100,
+ "page": 1,
+ "per_page": 20
+}
+```
+
+## Status Codes
+- `200` - Success
+- `201` - Created
+- `404` - Not Found
+- `409` - Conflict (duplicate telegram_id)
+- `500` - Internal Server Error
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..16f72c0
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,4252 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "axum-macros",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower 0.5.2",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
+[[package]]
+name = "bigdecimal"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013"
+dependencies = [
+ "autocfg",
+ "libm",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "borsh"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
+dependencies = [
+ "borsh-derive",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+dependencies = [
+ "async-trait",
+ "convert_case",
+ "json5",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "cron"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07"
+dependencies = [
+ "chrono",
+ "nom",
+ "once_cell",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.7+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.11.3",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash 0.8.12",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+dependencies = [
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "libc",
+ "pin-project-lite",
+ "socket2 0.6.0",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "inherent"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libredox"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
+dependencies = [
+ "bitflags",
+ "libc",
+ "redox_syscall",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "ouroboros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "static_assertions",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "pem"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pest"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.16",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "petgraph"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+dependencies = [
+ "fixedbitset",
+ "indexmap 2.11.3",
+]
+
+[[package]]
+name = "pgvector"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit 0.23.5",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
+dependencies = [
+ "heck 0.5.0",
+ "itertools",
+ "log",
+ "multimap",
+ "once_cell",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 2.0.106",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rcgen"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1"
+dependencies = [
+ "pem",
+ "ring",
+ "time",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sea-bae"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "sea-orm"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335d87ec8e5c6eb4b2afb866dc53ed57a5cba314af63ce288db83047aa0fed4d"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "bigdecimal",
+ "chrono",
+ "futures-util",
+ "log",
+ "ouroboros",
+ "pgvector",
+ "rust_decimal",
+ "sea-orm-macros",
+ "sea-query",
+ "sea-query-binder",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "strum",
+ "thiserror 2.0.16",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sea-orm-cli"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d4ff77d7c27f64942273513e7c103a1594c88deba33dfb2f490b362ea220b4"
+dependencies = [
+ "chrono",
+ "clap",
+ "dotenvy",
+ "glob",
+ "regex",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+]
+
+[[package]]
+name = "sea-orm-macros"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68de7a2258410fd5e6ba319a4fe6c4af7811507fc714bbd76534ae6caa60f95f"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "sea-bae",
+ "syn 2.0.106",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sea-orm-migration"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4649155dbfd88f92e2aa9f5defe0b020e43d7c4d52a2189658d8813e26b61bd"
+dependencies = [
+ "async-trait",
+ "clap",
+ "dotenvy",
+ "sea-orm",
+ "sea-orm-cli",
+ "sea-schema",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "sea-query"
+version = "0.32.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c"
+dependencies = [
+ "bigdecimal",
+ "chrono",
+ "inherent",
+ "ordered-float",
+ "rust_decimal",
+ "sea-query-derive",
+ "serde_json",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "sea-query-binder"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608"
+dependencies = [
+ "bigdecimal",
+ "chrono",
+ "rust_decimal",
+ "sea-query",
+ "serde_json",
+ "sqlx",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "sea-query-derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
+dependencies = [
+ "darling",
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "thiserror 2.0.16",
+]
+
+[[package]]
+name = "sea-schema"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338"
+dependencies = [
+ "futures",
+ "sea-query",
+ "sea-schema-derive",
+]
+
+[[package]]
+name = "sea-schema-derive"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "serde"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.225"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap 2.11.3",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64 0.22.1",
+ "bigdecimal",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.5",
+ "hashlink 0.10.0",
+ "indexmap 2.11.3",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rust_decimal",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.16",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.5.0",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 2.0.106",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bigdecimal",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "rust_decimal",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.16",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bigdecimal",
+ "bitflags",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "num-bigint",
+ "once_cell",
+ "rand",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.16",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror 2.0.16",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
+dependencies = [
+ "thiserror-impl 2.0.16",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2 0.6.0",
+ "tokio-macros",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio-cron-scheduler"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c2e3a88f827f597799cf70a6f673074e62f3fc5ba5993b2873345c618a29af"
+dependencies = [
+ "chrono",
+ "cron",
+ "num-derive",
+ "num-traits",
+ "tokio",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime 0.6.11",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap 2.11.3",
+ "serde",
+ "serde_spanned",
+ "toml_datetime 0.6.11",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9"
+dependencies = [
+ "indexmap 2.11.3",
+ "toml_datetime 0.7.1",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tonic"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.22.1",
+ "bytes",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-timeout",
+ "hyper-util",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "socket2 0.5.10",
+ "tokio",
+ "tokio-stream",
+ "tower 0.4.13",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "prost-types",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna 1.1.0",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "validator"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e"
+dependencies = [
+ "idna 0.5.0",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+ "validator_derive",
+]
+
+[[package]]
+name = "validator_derive"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
+dependencies = [
+ "darling",
+ "once_cell",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.7+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
+dependencies = [
+ "wasip2",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.2",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.0",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[package]]
+name = "windows-result"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "xray-admin"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "axum",
+ "chrono",
+ "clap",
+ "config",
+ "hyper",
+ "log",
+ "prost",
+ "rcgen",
+ "sea-orm",
+ "sea-orm-migration",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-cron-scheduler",
+ "toml",
+ "tonic",
+ "tower 0.4.13",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "urlencoding",
+ "uuid",
+ "validator",
+ "xray-core",
+]
+
+[[package]]
+name = "xray-core"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa3d06322be1f519b4ac6a12f5cb5fcebcd9b3ea1122c31bb26b2aaa2a829a1"
+dependencies = [
+ "hyper-util",
+ "prost",
+ "prost-build",
+ "prost-types",
+ "tokio",
+ "tonic",
+ "tonic-build",
+ "tower 0.5.2",
+]
+
+[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink 0.8.4",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..e048dd8
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,59 @@
+[package]
+name = "xray-admin"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+# Async runtime
+tokio = { version = "1.0", features = ["full"] }
+tokio-cron-scheduler = "0.10"
+
+# Serialization/deserialization
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_yaml = "0.9"
+toml = "0.8"
+
+# Configuration
+config = "0.14"
+clap = { version = "4.0", features = ["derive", "env"] }
+
+# Logging
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+
+# Utilities
+anyhow = "1.0"
+thiserror = "1.0"
+
+# Validation
+validator = { version = "0.18", features = ["derive"] }
+
+# URL parsing
+url = "2.5"
+
+# Database and ORM
+sea-orm = { version = "1.0", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] }
+sea-orm-migration = "1.0"
+
+# Additional utilities
+uuid = { version = "1.0", features = ["v4", "serde"] }
+chrono = { version = "0.4", features = ["serde"] }
+async-trait = "0.1"
+log = "0.4"
+urlencoding = "2.1"
+
+# Web server
+axum = { version = "0.7", features = ["macros", "json"] }
+tower = "0.4"
+tower-http = { version = "0.5", features = ["cors", "fs"] }
+hyper = { version = "1.0", features = ["full"] }
+
+# Xray integration
+xray-core = "0.2.1" # gRPC client for Xray
+tonic = "0.12" # gRPC client/server framework
+prost = "0.13" # Protocol Buffers implementation
+rcgen = "0.12" # For self-signed certificates
+
+[dev-dependencies]
+tempfile = "3.0"
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index b645813..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,40 +0,0 @@
-FROM python:3-alpine
-
-# Build arguments
-ARG GIT_COMMIT="development"
-ARG GIT_COMMIT_SHORT="dev"
-ARG BUILD_DATE="unknown"
-ARG BRANCH_NAME="unknown"
-
-# Environment variables from build args
-ENV GIT_COMMIT=${GIT_COMMIT}
-ENV GIT_COMMIT_SHORT=${GIT_COMMIT_SHORT}
-ENV BUILD_DATE=${BUILD_DATE}
-ENV BRANCH_NAME=${BRANCH_NAME}
-
-WORKDIR /app
-
-# Install system dependencies first (this layer will be cached)
-RUN apk update && apk add git curl unzip
-
-# Copy and install Python dependencies (this layer will be cached when requirements.txt doesn't change)
-COPY ./requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-
-# Install Xray-core
-RUN XRAY_VERSION=$(curl -s https://api.github.com/repos/XTLS/Xray-core/releases/latest | sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p') && \
- curl -L -o /tmp/xray.zip "https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/Xray-linux-64.zip" && \
- cd /tmp && unzip xray.zip && \
- ls -la /tmp/ && \
- find /tmp -name "xray" -type f && \
- cp xray /usr/local/bin/xray && \
- chmod +x /usr/local/bin/xray && \
- rm -rf /tmp/xray.zip /tmp/xray
-
-# Copy the rest of the application code (this layer will change frequently)
-COPY . .
-
-# Run collectstatic
-RUN python manage.py collectstatic --noinput
-
-CMD [ "python", "./manage.py", "runserver", "0.0.0.0:8000" ]
diff --git a/LICENSE b/LICENSE
deleted file mode 100755
index ff9e935..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,13 +0,0 @@
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- Version 2, December 2004
-
-Copyright (C) 2004 Sam Hocevar
-
-Everyone is permitted to copy and distribute verbatim or modified
-copies of this license document, and changing it is allowed as long
-as the name is changed.
-
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/LLM_PROJECT_CONTEXT.md b/LLM_PROJECT_CONTEXT.md
new file mode 100644
index 0000000..097d1e2
--- /dev/null
+++ b/LLM_PROJECT_CONTEXT.md
@@ -0,0 +1,132 @@
+# LLM Project Context - Xray Admin Panel
+
+## Project Overview
+Rust-based administration panel for managing xray-core VPN proxy servers. Uses real gRPC integration with xray-core library for server communication.
+
+## Current Architecture
+
+### Core Technologies
+- **Language**: Rust (edition 2021)
+- **Web Framework**: Axum with tower-http
+- **Database**: PostgreSQL with Sea-ORM
+- **Xray Integration**: xray-core 0.2.1 library with real gRPC communication
+- **Frontend**: Vanilla HTML/CSS/JS with toast notifications
+
+### Module Structure
+```
+src/
+├── config/ # Configuration management (args, env, file)
+├── database/ # Sea-ORM entities, repositories, migrations
+├── services/ # Business logic (xray gRPC client, certificates)
+├── web/ # Axum handlers and routes
+└── main.rs # Application entry point
+```
+
+## Key Features Implemented
+
+### 1. Database Entities
+- **Users**: Basic user management
+- **Servers**: Xray server definitions with gRPC endpoints
+- **Certificates**: TLS certificates with PEM storage (binary format)
+- **InboundTemplates**: Reusable inbound configurations
+- **ServerInbounds**: Template bindings to servers with ports/certificates
+
+### 2. Xray gRPC Integration
+**Location**: `src/services/xray/client.rs`
+- Real xray-core library integration (NOT mock/CLI)
+- Methods: `add_inbound_with_certificate()`, `remove_inbound()`, `get_stats()`
+- **CRITICAL**: TLS certificate configuration via streamSettings with proper protobuf messages
+- Supports VLESS, VMess, Trojan, Shadowsocks protocols
+
+### 3. Certificate Management
+**Location**: `src/database/entities/certificate.rs`
+- Self-signed certificate generation using rcgen
+- Binary storage (cert_data, key_data as Vec)
+- PEM conversion methods: `certificate_pem()`, `private_key_pem()`
+- Separate endpoints: `/certificates/{id}` (basic) and `/certificates/{id}/details` (with PEM)
+
+### 4. Template-Based Architecture
+Templates define reusable inbound configurations that can be bound to servers with:
+- Port overrides
+- Certificate assignments
+- Active/inactive states
+
+## Current Status & Issues
+
+### ✅ Working Features
+- Complete CRUD for all entities
+- Real xray gRPC communication with TLS certificate support
+- Toast notification system (absolute positioning)
+- Modal-based editing interface
+- Password masking in database URL logging
+- Certificate details display with PEM content
+
+### 🔧 Recent Fixes
+- **StreamConfig Integration**: Fixed TLS certificate configuration in xray gRPC calls
+- **Certificate Display**: Added `/certificates/{id}/details` endpoint for PEM viewing
+- **Active/Inactive Management**: Inbounds automatically added/removed from xray when toggled
+
+### ⚠️ Current Issue
+User reported certificate details still showing "Not available" - this was just fixed with the new `/certificates/{id}/details` endpoint.
+
+## API Structure
+
+### Endpoints
+```
+/api/users/* # User management
+/api/servers/* # Server management
+/api/servers/{id}/inbounds/* # Server inbound management
+/api/certificates/* # Certificate management (basic)
+/api/certificates/{id}/details # Certificate details with PEM
+/api/templates/* # Template management
+```
+
+## Configuration
+- **Default port**: 8080 (user tested on 8082)
+- **Database**: PostgreSQL with auto-migration
+- **Environment variables**: XRAY_ADMIN__* prefix
+- **Config file**: config.toml support
+
+## Testing Commands
+```bash
+# Run application
+cargo run -- --host 0.0.0.0 --port 8082
+
+# Test xray integration
+xray api lsi --server 100.91.97.36:10085
+
+# Check compilation
+cargo check
+```
+
+## Key Implementation Details
+
+### Xray TLS Configuration
+**Location**: `src/services/xray/client.rs:185-194`
+```rust
+let stream_config = StreamConfig {
+ protocol_name: "tcp".to_string(),
+ security_type: "tls".to_string(),
+ security_settings: vec![tls_message],
+ // ... other fields
+};
+```
+
+### Certificate Data Flow
+1. User creates certificate via web interface
+2. PEM data stored as binary in database (cert_data, key_data)
+3. When creating inbound, certificate fetched and converted back to PEM
+4. PEM passed to xray gRPC client for TLS configuration
+
+### Database Migrations
+Auto-migration enabled by default. All entities use UUID primary keys with timestamps.
+
+## Development Notes
+- **User prefers English in code/comments**
+- **No emoji usage unless explicitly requested**
+- **Prefer editing existing files over creating new ones**
+- **Real xray-core integration required** (user specifically asked not to abandon it)
+- **Application tested with actual xray server at 100.91.97.36:10085**
+
+## Last Working State
+All features implemented and compiling. StreamConfig properly configured for TLS certificate transmission to xray servers. Certificate viewing endpoint fixed for PEM display.
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100755
index fc225a3..0000000
--- a/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
OutFleet: Master Your OutLine VPN
-
-
- Streamline OutLine VPN experience. OutFleet offers centralized key control for many servers, users and always-updated Dynamic Access Keys instead of ss:// links
-
-
- Request Feature
-
-
-
-  
-
-
-
-## About The Project
-
-### Key Features
-
-* Centralized Key Management
-Administer user keys from one unified dashboard. Add, delete, and allocate users to specific servers effortlessly.
-
-* 
-Distribute ssconf:// links that are always up-to-date with your current server configurations. Eliminate the need for manual link updates.
-
-### Why OutFleet?
-Tired of juggling multiple home servers and the headache of individually managing users on each? OutFleet was born out of the frustration of not finding a suitable tool for efficiently managing a bunch of home servers.
-
-## Built With
-
-Django, Postgres SQL and hassle-free deployment using Kubernetes or docker-compose
-
-### Installation
-
-#### Docker compose
-Docker deploy is easy:
-```
-docker-compose up -d
-```
-#### Kubernetes
-I use ArgoCD for deployment. [Take a look](https://gt.hexor.cy/ab/homelab/src/branch/main/k8s/apps/vpn) to `outfleet.yaml` file for manifests.
-
-
-#### Setup sslocal service on Windows
-Shadowsocks servers can be used directly with **sslocal**. For automatic and regular password updates, you can create a Task Scheduler job to rotate the passwords when they change, as OutFleet manages the passwords automatically.
-You may run script in Admin PowerShell to create Task for autorun **sslocal** and update connection details automatically using Outfleet API
-```PowerShell
-Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force; Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/house-of-vanity/OutFleet/refs/heads/master/tools/windows-helper.ps1" -UseBasicParsing).Content
-```
-[Firefox PluginProxy Switcher and Manager](https://addons.mozilla.org/en-US/firefox/addon/proxy-switcher-and-manager/) && [Chrome plugin Proxy Switcher and Manager](https://chromewebstore.google.com/detail/proxy-switcher-and-manage/onnfghpihccifgojkpnnncpagjcdbjod)
-
-Keep in mind that all user keys are stored in a single **config.yaml** file. If this file is lost, user keys will remain on the servers, but OutFleet will lose the ability to manage them. Handle with extreme caution and use backups.
-
-## Authors
-
-* **UltraDesu** - *Humble amateur developer* - [UltraDesu](https://github.com/house-of-vanity) - *Author*
-* **Contributors**
-* * @Sanapach
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index 034e848..0000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Security Policy
-
-## Supported Versions
-
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
-
-| Version | Supported |
-| ------- | ------------------ |
-| 5.1.x | :white_check_mark: |
-| 5.0.x | :x: |
-| 4.0.x | :white_check_mark: |
-| < 4.0 | :x: |
-
-## Reporting a Vulnerability
-
-Use this section to tell people how to report a vulnerability.
-
-Tell them where to go, how often they can expect to get an update on a
-reported vulnerability, what to expect if the vulnerability is accepted or
-declined, etc.
diff --git a/buildx.yaml b/buildx.yaml
deleted file mode 100755
index 7e576e6..0000000
--- a/buildx.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-platforms:
- - name: amd64
- architecture: amd64
- - name: arm64
- architecture: arm64
diff --git a/cleanup_analysis.sql b/cleanup_analysis.sql
deleted file mode 100644
index b4f444a..0000000
--- a/cleanup_analysis.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Проверить количество записей без acl_link_id
-SELECT COUNT(*) as total_without_link
-FROM vpn_accesslog
-WHERE acl_link_id IS NULL OR acl_link_id = '';
-
--- Проверить общее количество записей
-SELECT COUNT(*) as total_records FROM vpn_accesslog;
-
--- Показать распределение по датам (последние записи без ссылок)
-SELECT DATE(timestamp) as date, COUNT(*) as count
-FROM vpn_accesslog
-WHERE acl_link_id IS NULL OR acl_link_id = ''
-GROUP BY DATE(timestamp)
-ORDER BY date DESC
-LIMIT 10;
diff --git a/cleanup_options.sql b/cleanup_options.sql
deleted file mode 100644
index adae61a..0000000
--- a/cleanup_options.sql
+++ /dev/null
@@ -1,35 +0,0 @@
--- ВАРИАНТ 1: Удалить ВСЕ записи без acl_link_id
--- ОСТОРОЖНО! Это удалит все старые логи
-DELETE FROM vpn_accesslog
-WHERE acl_link_id IS NULL OR acl_link_id = '';
-
--- ВАРИАНТ 2: Удалить записи без acl_link_id старше 30 дней
--- Более безопасный вариант
-DELETE FROM vpn_accesslog
-WHERE (acl_link_id IS NULL OR acl_link_id = '')
- AND timestamp < NOW() - INTERVAL 30 DAY;
-
--- ВАРИАНТ 3: Удалить записи без acl_link_id старше 7 дней
--- Еще более консервативный подход
-DELETE FROM vpn_accesslog
-WHERE (acl_link_id IS NULL OR acl_link_id = '')
- AND timestamp < NOW() - INTERVAL 7 DAY;
-
--- ВАРИАНТ 4: Оставить только последние 1000 записей без ссылок (для истории)
-DELETE FROM vpn_accesslog
-WHERE (acl_link_id IS NULL OR acl_link_id = '')
- AND id NOT IN (
- SELECT id FROM (
- SELECT id FROM vpn_accesslog
- WHERE acl_link_id IS NULL OR acl_link_id = ''
- ORDER BY timestamp DESC
- LIMIT 1000
- ) AS recent_logs
- );
-
--- ВАРИАНТ 5: Поэтапное удаление (для больших БД)
--- Удаляем по 10000 записей за раз
-DELETE FROM vpn_accesslog
-WHERE (acl_link_id IS NULL OR acl_link_id = '')
- AND timestamp < NOW() - INTERVAL 30 DAY
-LIMIT 10000;
diff --git a/docker-compose.yaml b/docker-compose.yaml
deleted file mode 100644
index 30f4d3d..0000000
--- a/docker-compose.yaml
+++ /dev/null
@@ -1,102 +0,0 @@
-services:
- web_ui:
- image: outfleet:local
- container_name: outfleet-web
- build:
- context: .
- ports:
- - "8000:8000"
- environment:
- - POSTGRES_HOST=postgres
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
- - EXTERNAL_ADDRESS=http://127.0.0.1:8000
- - CELERY_BROKER_URL=redis://redis:6379/0
- depends_on:
- postgres:
- condition: service_healthy
- redis:
- condition: service_healthy
- volumes:
- - .:/app
- working_dir: /app
- command: >
- sh -c "sleep 1 &&
- python manage.py makemigrations &&
- python manage.py migrate &&
- python manage.py create_admin &&
- python manage.py runserver 0.0.0.0:8000"
-
- worker:
- image: outfleet:local
- container_name: outfleet-worker
- build:
- context: .
- environment:
- - POSTGRES_HOST=postgres
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
- - CELERY_BROKER_URL=redis://redis:6379/0
- depends_on:
- postgres:
- condition: service_healthy
- redis:
- condition: service_healthy
- volumes:
- - .:/app
- working_dir: /app
- command: >
- sh -c "sleep 3 && celery -A mysite worker"
-
- beat:
- image: outfleet:local
- container_name: outfleet-beat
- build:
- context: .
- environment:
- - POSTGRES_HOST=postgres
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
- - CELERY_BROKER_URL=redis://redis:6379/0
- depends_on:
- postgres:
- condition: service_healthy
- redis:
- condition: service_healthy
- volumes:
- - .:/app
- working_dir: /app
- command: >
- sh -c "sleep 3 && celery -A mysite beat"
-
- postgres:
- image: postgres:15
- container_name: postgres
- environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: outfleet
- ports:
- - "5432:5432"
- volumes:
- - postgres_data:/var/lib/postgresql/data
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres"]
- interval: 10s
- timeout: 5s
- retries: 5
-
- redis:
- image: redis:7
- container_name: redis
- ports:
- - "6379:6379"
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 10s
- timeout: 5s
- retries: 3
-
-volumes:
- postgres_data:
-
diff --git a/manage.py b/manage.py
deleted file mode 100755
index a7da667..0000000
--- a/manage.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python
-"""Django's command-line utility for administrative tasks."""
-import os
-import sys
-
-
-def main():
- """Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
- try:
- from django.core.management import execute_from_command_line
- except ImportError as exc:
- raise ImportError(
- "Couldn't import Django. Are you sure it's installed and "
- "available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
- ) from exc
- execute_from_command_line(sys.argv)
-
-
-if __name__ == '__main__':
- main()
diff --git a/mysite/__init__.py b/mysite/__init__.py
deleted file mode 100644
index 9e0d95f..0000000
--- a/mysite/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .celery import app as celery_app
-
-__all__ = ('celery_app',)
\ No newline at end of file
diff --git a/mysite/asgi.py b/mysite/asgi.py
deleted file mode 100644
index 44c7dff..0000000
--- a/mysite/asgi.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-ASGI config for mysite project.
-
-It exposes the ASGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
-"""
-
-import os
-
-from django.core.asgi import get_asgi_application
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
-
-application = get_asgi_application()
diff --git a/mysite/celery.py b/mysite/celery.py
deleted file mode 100644
index 0c7530d..0000000
--- a/mysite/celery.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import logging
-import os
-
-from celery import Celery
-from celery import shared_task
-from celery.schedules import crontab
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
-logger = logging.getLogger(__name__)
-app = Celery('mysite')
-
-app.conf.beat_schedule = {
- 'periodical_servers_sync': {
- 'task': 'sync_all_servers',
- 'schedule': crontab(minute=0, hour='*/3'), # Every 3 hours
- },
- 'cleanup_old_task_logs': {
- 'task': 'cleanup_task_logs',
- 'schedule': crontab(hour=2, minute=0), # Daily at 2 AM
- },
-}
-
-
-app.config_from_object('django.conf:settings', namespace='CELERY')
-
-# Additional celery settings for better logging and performance
-app.conf.update(
- # Keep detailed results for debugging
- result_expires=3600, # 1 hour
- task_always_eager=False,
- task_eager_propagates=True,
- # Improve task tracking
- task_track_started=True,
- task_send_sent_event=True,
- # Clean up settings
- result_backend_cleanup_interval=300, # Clean up every 5 minutes
-)
-
-app.autodiscover_tasks()
-
diff --git a/mysite/context_processors.py b/mysite/context_processors.py
deleted file mode 100644
index e570b0e..0000000
--- a/mysite/context_processors.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from django.conf import settings
-import subprocess
-from pathlib import Path
-
-def version_info(request):
- """Add version information to template context"""
-
- git_commit = getattr(settings, 'GIT_COMMIT', None)
- git_commit_short = getattr(settings, 'GIT_COMMIT_SHORT', None)
- build_date = getattr(settings, 'BUILD_DATE', None)
-
- if not git_commit or git_commit == 'development':
- try:
- base_dir = getattr(settings, 'BASE_DIR', Path(__file__).resolve().parent.parent)
- result = subprocess.run(['git', 'rev-parse', 'HEAD'],
- capture_output=True, text=True, cwd=base_dir, timeout=5)
- if result.returncode == 0:
- git_commit = result.stdout.strip()
- git_commit_short = git_commit[:7]
-
- date_result = subprocess.run(['git', 'log', '-1', '--format=%ci'],
- capture_output=True, text=True, cwd=base_dir, timeout=5)
- if date_result.returncode == 0:
- build_date = date_result.stdout.strip()
- except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
- pass
-
- if not git_commit:
- git_commit = 'development'
- if not git_commit_short:
- git_commit_short = 'dev'
- if not build_date:
- build_date = 'unknown'
-
- return {
- 'VERSION_INFO': {
- 'git_commit': git_commit,
- 'git_commit_short': git_commit_short,
- 'build_date': build_date,
- 'is_development': git_commit_short == 'dev'
- }
- }
diff --git a/mysite/middleware.py b/mysite/middleware.py
deleted file mode 100644
index eb765a5..0000000
--- a/mysite/middleware.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from django.contrib.auth import authenticate, login
-from django.utils.deprecation import MiddlewareMixin
-
-class RequestLogger:
- def __init__(self, get_response):
- self.get_response = get_response
-
- def __call__(self, request):
- print(f"Original: {request.build_absolute_uri()}")
- print(f"Path : {request.path}")
-
- response = self.get_response(request)
-
- return response
-
-
-class AutoLoginMiddleware(MiddlewareMixin):
- def process_request(self, request):
- if not request.user.is_authenticated:
- user = authenticate(username='admin', password='admin')
- if user:
- login(request, user)
diff --git a/mysite/settings.py b/mysite/settings.py
deleted file mode 100644
index 4d6211e..0000000
--- a/mysite/settings.py
+++ /dev/null
@@ -1,233 +0,0 @@
-from pathlib import Path
-import os
-import environ
-from django.core.management.utils import get_random_secret_key
-
-
-ENV = environ.Env(
- DEBUG=(bool, False)
-)
-
-environ.Env.read_env()
-
-BASE_DIR = Path(__file__).resolve().parent.parent
-SECRET_KEY=ENV('SECRET_KEY', default='django-insecure-change-me-in-production')
-TIME_ZONE = ENV('TIMEZONE', default='Asia/Nicosia')
-EXTERNAL_ADDRESS = ENV('EXTERNAL_ADDRESS', default='https://example.org')
-
-CELERY_BROKER_URL = ENV('CELERY_BROKER_URL', default='redis://localhost:6379/0')
-CELERY_RESULT_BACKEND = 'django-db'
-CELERY_TIMEZONE = ENV('TIMEZONE', default='Asia/Nicosia')
-CELERY_ACCEPT_CONTENT = ['json']
-CELERY_TASK_SERIALIZER = 'json'
-CELERY_RESULT_SERIALIZER = 'json'
-CELERY_RESULT_EXTENDED = True
-
-# Celery Beat Schedule
-from celery.schedules import crontab
-CELERY_BEAT_SCHEDULE = {
- 'update-user-statistics': {
- 'task': 'update_user_statistics',
- 'schedule': crontab(minute='*/5'), # Every 5 minutes
- },
- 'cleanup-task-logs': {
- 'task': 'cleanup_task_logs',
- 'schedule': crontab(hour=2, minute=0), # Daily at 2 AM
- },
-}
-
-AUTH_USER_MODEL = "vpn.User"
-
-DEBUG = ENV('DEBUG')
-
-ALLOWED_HOSTS = ENV.list('ALLOWED_HOSTS', default=["*"])
-
-CORS_ALLOW_ALL_ORIGINS = True
-CORS_ALLOW_CREDENTIALS = True
-CSRF_TRUSTED_ORIGINS = ENV.list('CSRF_TRUSTED_ORIGINS', default=[])
-
-STATIC_ROOT = BASE_DIR / "staticfiles"
-
-LOGIN_REDIRECT_URL = '/'
-
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
-
-LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'formatters': {
- 'verbose': {
- 'format': '[{asctime}] {levelname} {name} {message}',
- 'style': '{',
- },
- 'simple': {
- 'format': '{levelname} {message}',
- 'style': '{',
- },
- },
- 'handlers': {
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- 'formatter': 'verbose',
- },
- 'file': {
- 'level': 'DEBUG',
- 'class': 'logging.FileHandler',
- 'filename': os.path.join(BASE_DIR, 'debug.log'),
- 'formatter': 'verbose',
- },
- },
- 'loggers': {
- 'django': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': True,
- },
- 'vpn': {
- 'handlers': ['console'],
- 'level': 'DEBUG',
- 'propagate': False,
- },
- 'telegram_bot': {
- 'handlers': ['console'],
- 'level': 'DEBUG',
- 'propagate': False,
- },
- 'requests': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': False,
- },
- 'urllib3': {
- 'handlers': ['console'],
- 'level': 'INFO',
- 'propagate': False,
- },
- },
-}
-
-INSTALLED_APPS = [
- 'jazzmin',
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'polymorphic',
- 'corsheaders',
- 'django_celery_results',
- 'django_celery_beat',
- 'vpn',
- 'telegram_bot',
-]
-
-MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'whitenoise.middleware.WhiteNoiseMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'mysite.middleware.AutoLoginMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
- 'corsheaders.middleware.CorsMiddleware',
-
-]
-
-ROOT_URLCONF = 'mysite.urls'
-
-GIT_COMMIT = ENV('GIT_COMMIT', default='development')
-GIT_COMMIT_SHORT = ENV('GIT_COMMIT_SHORT', default='dev')
-BUILD_DATE = ENV('BUILD_DATE', default='unknown')
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [
- os.path.join(BASE_DIR, 'templates'),
- os.path.join(BASE_DIR, 'vpn', 'templates')
- ],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- 'mysite.context_processors.version_info',
- ],
- },
- },
-]
-
-
-WSGI_APPLICATION = 'mysite.wsgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
-
-# CREATE USER outfleet WITH PASSWORD 'password';
-# GRANT ALL PRIVILEGES ON DATABASE outfleet TO outfleet;
-# ALTER DATABASE outfleet OWNER TO outfleet;
-
-DATABASES = {
- 'sqlite': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
- },
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql',
- 'NAME': ENV('POSTGRES_DB', default="outfleet"),
- 'USER': ENV('POSTGRES_USER', default="outfleet"),
- 'PASSWORD': ENV('POSTGRES_PASSWORD', default="outfleet"),
- 'HOST': ENV('POSTGRES_HOST', default='localhost'),
- 'PORT': ENV('POSTGRES_PORT', default='5432'),
- }
-}
-
-# Password validation
-# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/5.1/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-
-
-USE_I18N = True
-
-USE_TZ = True
-
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/5.1/howto/static-files/
-
-STATIC_URL = '/static/'
-STATICFILES_DIRS = [
- BASE_DIR / 'static',
-]
-# Default primary key field type
-# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
-
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/mysite/urls.py b/mysite/urls.py
deleted file mode 100644
index b6f6c0a..0000000
--- a/mysite/urls.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-URL configuration for mysite project.
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/5.1/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.urls import include, path
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
-"""
-from django.contrib import admin
-from django.urls import path, include
-from django.views.generic import RedirectView
-from vpn.views import shadowsocks, userFrontend, userPortal, xray_subscription
-
-urlpatterns = [
- path('admin/', admin.site.urls),
- path('ss/', shadowsocks, name='shadowsocks'),
- path('dynamic/', shadowsocks, name='shadowsocks'),
- path('xray/', xray_subscription, name='xray_subscription'),
- path('stat/', userFrontend, name='userFrontend'),
- path('u/', userPortal, name='userPortal'),
- path('', RedirectView.as_view(url='/admin/', permanent=False)),
-]
diff --git a/mysite/wsgi.py b/mysite/wsgi.py
deleted file mode 100644
index 61b0d9d..0000000
--- a/mysite/wsgi.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-WSGI config for mysite project.
-
-It exposes the WSGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
-"""
-
-import os
-
-from django.core.wsgi import get_wsgi_application
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
-
-application = get_wsgi_application()
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 5d87110..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-django-environ==0.12.0
-Django==5.1.7
-celery==5.4.0
-django-jazzmin==3.0.1
-django-polymorphic==3.1.0
-django-cors-headers==4.5.0
-django-celery-results==2.5.1
-git+https://github.com/celery/django-celery-beat#egg=django-celery-beat
-requests==2.32.3
-PyYaml==6.0.2
-Markdown==3.7
-outline-vpn-api==6.3.0
-Redis==5.2.1
-whitenoise==6.9.0
-psycopg2-binary==2.9.10
-setuptools==75.2.0
-shortuuid==1.0.13
-cryptography==45.0.5
-acme>=2.0.0
-cloudflare>=4.3.1
-josepy>=2.0.0
-python-telegram-bot==21.10
diff --git a/src/config/args.rs b/src/config/args.rs
new file mode 100644
index 0000000..9f8e31b
--- /dev/null
+++ b/src/config/args.rs
@@ -0,0 +1,60 @@
+use clap::Parser;
+use std::path::PathBuf;
+
+#[derive(Parser, Debug)]
+#[command(name = "xray-admin")]
+#[command(about = "A web admin panel for managing xray-core VPN proxy servers")]
+#[command(version)]
+pub struct Args {
+ /// Configuration file path
+ #[arg(short, long, value_name = "FILE")]
+ pub config: Option,
+
+ /// Database connection URL
+ #[arg(long, env = "DATABASE_URL")]
+ pub database_url: Option,
+
+ /// Web server host address
+ #[arg(long, default_value = "127.0.0.1")]
+ pub host: Option,
+
+ /// Web server port
+ #[arg(short, long)]
+ pub port: Option,
+
+ /// Log level (trace, debug, info, warn, error)
+ #[arg(long, default_value = "info")]
+ pub log_level: Option,
+
+
+ /// Validate configuration and exit
+ #[arg(long)]
+ pub validate_config: bool,
+
+ /// Print default configuration and exit
+ #[arg(long)]
+ pub print_default_config: bool,
+}
+
+pub fn parse_args() -> Args {
+ Args::parse()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_args_parsing() {
+ let args = Args::try_parse_from(&[
+ "xray-admin",
+ "--config", "test.toml",
+ "--port", "9090",
+ "--log-level", "debug"
+ ]).unwrap();
+
+ assert_eq!(args.config, Some(PathBuf::from("test.toml")));
+ assert_eq!(args.port, Some(9090));
+ assert_eq!(args.log_level, Some("debug".to_string()));
+ }
+}
\ No newline at end of file
diff --git a/src/config/env.rs b/src/config/env.rs
new file mode 100644
index 0000000..058d2df
--- /dev/null
+++ b/src/config/env.rs
@@ -0,0 +1,104 @@
+use std::env;
+
+/// Environment variable utilities
+pub struct EnvVars;
+
+impl EnvVars {
+ /// Get environment variable with fallback
+ #[allow(dead_code)]
+ pub fn get_or_default(key: &str, default: &str) -> String {
+ env::var(key).unwrap_or_else(|_| default.to_string())
+ }
+
+ /// Get required environment variable
+ #[allow(dead_code)]
+ pub fn get_required(key: &str) -> Result {
+ env::var(key)
+ }
+
+ /// Check if running in development mode
+ #[allow(dead_code)]
+ pub fn is_development() -> bool {
+ matches!(
+ env::var("RUST_ENV").as_deref(),
+ Ok("development") | Ok("dev")
+ ) || matches!(
+ env::var("ENVIRONMENT").as_deref(),
+ Ok("development") | Ok("dev")
+ )
+ }
+
+ /// Check if running in production mode
+ #[allow(dead_code)]
+ pub fn is_production() -> bool {
+ matches!(
+ env::var("RUST_ENV").as_deref(),
+ Ok("production") | Ok("prod")
+ ) || matches!(
+ env::var("ENVIRONMENT").as_deref(),
+ Ok("production") | Ok("prod")
+ )
+ }
+
+ /// Get database URL from environment
+ #[allow(dead_code)]
+ pub fn database_url() -> Option {
+ env::var("DATABASE_URL").ok()
+ .or_else(|| env::var("XRAY_ADMIN__DATABASE__URL").ok())
+ }
+
+ /// Get telegram bot token from environment
+ #[allow(dead_code)]
+ pub fn telegram_token() -> Option {
+ env::var("TELEGRAM_BOT_TOKEN").ok()
+ .or_else(|| env::var("XRAY_ADMIN__TELEGRAM__BOT_TOKEN").ok())
+ }
+
+ /// Get JWT secret from environment
+ #[allow(dead_code)]
+ pub fn jwt_secret() -> Option {
+ env::var("JWT_SECRET").ok()
+ .or_else(|| env::var("XRAY_ADMIN__WEB__JWT_SECRET").ok())
+ }
+
+ /// Print environment info for debugging
+ pub fn print_env_info() {
+ tracing::debug!("Environment information:");
+ tracing::debug!(" RUST_ENV: {:?}", env::var("RUST_ENV"));
+ tracing::debug!(" ENVIRONMENT: {:?}", env::var("ENVIRONMENT"));
+ tracing::debug!(" DATABASE_URL: {}",
+ if env::var("DATABASE_URL").is_ok() { "set" } else { "not set" }
+ );
+ tracing::debug!(" TELEGRAM_BOT_TOKEN: {}",
+ if env::var("TELEGRAM_BOT_TOKEN").is_ok() { "set" } else { "not set" }
+ );
+ tracing::debug!(" JWT_SECRET: {}",
+ if env::var("JWT_SECRET").is_ok() { "set" } else { "not set" }
+ );
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+
+ #[test]
+ fn test_get_or_default() {
+ let result = EnvVars::get_or_default("NON_EXISTENT_VAR", "default_value");
+ assert_eq!(result, "default_value");
+ }
+
+ #[test]
+ fn test_environment_detection() {
+ env::set_var("RUST_ENV", "development");
+ assert!(EnvVars::is_development());
+ assert!(!EnvVars::is_production());
+
+ env::set_var("RUST_ENV", "production");
+ assert!(!EnvVars::is_development());
+ assert!(EnvVars::is_production());
+
+ env::remove_var("RUST_ENV");
+ }
+}
\ No newline at end of file
diff --git a/src/config/file.rs b/src/config/file.rs
new file mode 100644
index 0000000..3192e79
--- /dev/null
+++ b/src/config/file.rs
@@ -0,0 +1,165 @@
+use anyhow::{Context, Result};
+use std::fs;
+use std::path::Path;
+
+use super::AppConfig;
+
+/// Configuration file utilities
+#[allow(dead_code)]
+pub struct ConfigFile;
+
+#[allow(dead_code)]
+impl ConfigFile {
+ /// Load configuration from TOML file
+ pub fn load_toml>(path: P) -> Result {
+ let content = fs::read_to_string(&path)
+ .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
+
+ let config: AppConfig = toml::from_str(&content)
+ .with_context(|| format!("Failed to parse TOML config file: {}", path.as_ref().display()))?;
+
+ Ok(config)
+ }
+
+ /// Load configuration from YAML file
+ pub fn load_yaml>(path: P) -> Result {
+ let content = fs::read_to_string(&path)
+ .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
+
+ let config: AppConfig = serde_yaml::from_str(&content)
+ .with_context(|| format!("Failed to parse YAML config file: {}", path.as_ref().display()))?;
+
+ Ok(config)
+ }
+
+ /// Load configuration from JSON file
+ pub fn load_json>(path: P) -> Result {
+ let content = fs::read_to_string(&path)
+ .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
+
+ let config: AppConfig = serde_json::from_str(&content)
+ .with_context(|| format!("Failed to parse JSON config file: {}", path.as_ref().display()))?;
+
+ Ok(config)
+ }
+
+ /// Auto-detect format and load configuration file
+ pub fn load_auto>(path: P) -> Result {
+ let path = path.as_ref();
+
+ match path.extension().and_then(|ext| ext.to_str()) {
+ Some("toml") => Self::load_toml(path),
+ Some("yaml") | Some("yml") => Self::load_yaml(path),
+ Some("json") => Self::load_json(path),
+ _ => {
+ // Try TOML first, then YAML, then JSON
+ Self::load_toml(path)
+ .or_else(|_| Self::load_yaml(path))
+ .or_else(|_| Self::load_json(path))
+ .with_context(|| {
+ format!(
+ "Failed to load config file '{}' - tried TOML, YAML, and JSON formats",
+ path.display()
+ )
+ })
+ }
+ }
+ }
+
+ /// Save configuration to TOML file
+ pub fn save_toml>(config: &AppConfig, path: P) -> Result<()> {
+ let content = toml::to_string_pretty(config)
+ .context("Failed to serialize config to TOML")?;
+
+ fs::write(&path, content)
+ .with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
+
+ Ok(())
+ }
+
+ /// Save configuration to YAML file
+ pub fn save_yaml>(config: &AppConfig, path: P) -> Result<()> {
+ let content = serde_yaml::to_string(config)
+ .context("Failed to serialize config to YAML")?;
+
+ fs::write(&path, content)
+ .with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
+
+ Ok(())
+ }
+
+ /// Save configuration to JSON file
+ pub fn save_json>(config: &AppConfig, path: P) -> Result<()> {
+ let content = serde_json::to_string_pretty(config)
+ .context("Failed to serialize config to JSON")?;
+
+ fs::write(&path, content)
+ .with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
+
+ Ok(())
+ }
+
+ /// Check if config file exists and is readable
+ pub fn exists_and_readable>(path: P) -> bool {
+ let path = path.as_ref();
+ path.exists() && path.is_file() && fs::metadata(path).map(|m| !m.permissions().readonly()).unwrap_or(false)
+ }
+
+ /// Find default config file in common locations
+ pub fn find_default() -> Option {
+ let candidates = [
+ "config.toml",
+ "config.yaml",
+ "config.yml",
+ "config.json",
+ "xray-admin.toml",
+ "xray-admin.yaml",
+ "xray-admin.yml",
+ "/etc/xray-admin/config.toml",
+ "/etc/xray-admin/config.yaml",
+ "~/.config/xray-admin/config.toml",
+ ];
+
+ for candidate in &candidates {
+ let path = std::path::Path::new(candidate);
+ if Self::exists_and_readable(path) {
+ return Some(path.to_path_buf());
+ }
+ }
+
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::NamedTempFile;
+
+ #[test]
+ fn test_save_and_load_toml() -> Result<()> {
+ let config = AppConfig::default();
+ let temp_file = NamedTempFile::new()?;
+
+ ConfigFile::save_toml(&config, temp_file.path())?;
+ let loaded_config = ConfigFile::load_toml(temp_file.path())?;
+
+ assert_eq!(config.web.port, loaded_config.web.port);
+ assert_eq!(config.database.max_connections, loaded_config.database.max_connections);
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_auto_detect_format() -> Result<()> {
+ let config = AppConfig::default();
+
+ // Test with .toml extension
+ let temp_file = NamedTempFile::with_suffix(".toml")?;
+ ConfigFile::save_toml(&config, temp_file.path())?;
+ let loaded_config = ConfigFile::load_auto(temp_file.path())?;
+ assert_eq!(config.web.port, loaded_config.web.port);
+
+ Ok(())
+ }
+}
\ No newline at end of file
diff --git a/src/config/mod.rs b/src/config/mod.rs
new file mode 100644
index 0000000..7e3d8a0
--- /dev/null
+++ b/src/config/mod.rs
@@ -0,0 +1,244 @@
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use validator::Validate;
+
+pub mod args;
+pub mod env;
+pub mod file;
+
+#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
+pub struct AppConfig {
+ pub database: DatabaseConfig,
+ pub web: WebConfig,
+ pub telegram: TelegramConfig,
+ pub xray: XrayConfig,
+ pub logging: LoggingConfig,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
+pub struct DatabaseConfig {
+ #[validate(url)]
+ pub url: String,
+ #[validate(range(min = 1, max = 100))]
+ pub max_connections: u32,
+ #[validate(range(min = 1))]
+ pub connection_timeout: u64,
+ pub auto_migrate: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
+pub struct WebConfig {
+ #[validate(ip)]
+ pub host: String,
+ #[validate(range(min = 1024, max = 65535))]
+ pub port: u16,
+ pub cors_origins: Vec,
+ pub jwt_secret: String,
+ #[validate(range(min = 3600))]
+ pub jwt_expiry: u64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
+pub struct TelegramConfig {
+ pub bot_token: String,
+ pub webhook_url: Option,
+ pub admin_chat_ids: Vec,
+ pub allowed_users: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
+pub struct XrayConfig {
+ pub default_api_port: u16,
+ pub config_template_path: PathBuf,
+ pub certificates_path: PathBuf,
+ #[validate(range(min = 1))]
+ pub health_check_interval: u64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct LoggingConfig {
+ pub level: String,
+ pub file_path: Option,
+ pub json_format: bool,
+ pub max_file_size: Option,
+ pub max_files: Option,
+}
+
+impl Default for DatabaseConfig {
+ fn default() -> Self {
+ Self {
+ url: "postgresql://xray:password@localhost/xray_admin".to_string(),
+ max_connections: 10,
+ connection_timeout: 30,
+ auto_migrate: true,
+ }
+ }
+}
+
+impl Default for WebConfig {
+ fn default() -> Self {
+ Self {
+ host: "127.0.0.1".to_string(),
+ port: 8080,
+ cors_origins: vec!["http://localhost:3000".to_string()],
+ jwt_secret: "your-secret-key-change-in-production".to_string(),
+ jwt_expiry: 86400, // 24 hours
+ }
+ }
+}
+
+impl Default for TelegramConfig {
+ fn default() -> Self {
+ Self {
+ bot_token: "".to_string(),
+ webhook_url: None,
+ admin_chat_ids: vec![],
+ allowed_users: vec![],
+ }
+ }
+}
+
+impl Default for XrayConfig {
+ fn default() -> Self {
+ Self {
+ default_api_port: 62789,
+ config_template_path: PathBuf::from("./templates"),
+ certificates_path: PathBuf::from("./certs"),
+ health_check_interval: 30,
+ }
+ }
+}
+
+impl Default for LoggingConfig {
+ fn default() -> Self {
+ Self {
+ level: "info".to_string(),
+ file_path: None,
+ json_format: false,
+ max_file_size: Some(10 * 1024 * 1024), // 10MB
+ max_files: Some(5),
+ }
+ }
+}
+
+impl Default for AppConfig {
+ fn default() -> Self {
+ Self {
+ database: DatabaseConfig::default(),
+ web: WebConfig::default(),
+ telegram: TelegramConfig::default(),
+ xray: XrayConfig::default(),
+ logging: LoggingConfig::default(),
+ }
+ }
+}
+
+impl AppConfig {
+ /// Load configuration from multiple sources with priority:
+ /// 1. Command line arguments (highest)
+ /// 2. Environment variables
+ /// 3. Configuration file
+ /// 4. Default values (lowest)
+ pub fn load() -> Result {
+ let args = args::parse_args();
+
+ let mut builder = config::Config::builder()
+ // Start with defaults
+ .add_source(config::Config::try_from(&AppConfig::default())?);
+
+ // Add configuration file if specified or exists
+ if let Some(config_file) = &args.config {
+ builder = builder.add_source(config::File::from(config_file.as_path()));
+ } else if std::path::Path::new("config.toml").exists() {
+ builder = builder.add_source(config::File::with_name("config"));
+ }
+
+ // Add environment variables with prefix
+ builder = builder.add_source(
+ config::Environment::with_prefix("XRAY_ADMIN")
+ .separator("__")
+ .try_parsing(true)
+ );
+
+ // Override with command line arguments
+ if let Some(host) = &args.host {
+ builder = builder.set_override("web.host", host.as_str())?;
+ }
+ if let Some(port) = args.port {
+ builder = builder.set_override("web.port", port)?;
+ }
+ if let Some(db_url) = &args.database_url {
+ builder = builder.set_override("database.url", db_url.as_str())?;
+ }
+ if let Some(log_level) = &args.log_level {
+ builder = builder.set_override("logging.level", log_level.as_str())?;
+ }
+
+ let config: AppConfig = builder.build()?.try_deserialize()?;
+
+ // Validate configuration
+ config.validate()?;
+
+ Ok(config)
+ }
+
+ pub fn display_summary(&self) {
+ tracing::info!("Configuration loaded:");
+ tracing::info!(" Database URL: {}", mask_sensitive(&self.database.url));
+ tracing::info!(" Web server: {}:{}", self.web.host, self.web.port);
+ tracing::info!(" Log level: {}", self.logging.level);
+ tracing::info!(" Telegram bot: {}", if self.telegram.bot_token.is_empty() { "disabled" } else { "enabled" });
+ tracing::info!(" Xray config path: {}", self.xray.config_template_path.display());
+ }
+}
+
+/// Mask sensitive information in URLs for logging
+fn mask_sensitive(url: &str) -> String {
+ // Simple string-based approach to mask passwords
+ if let Some(scheme_end) = url.find("://") {
+ let after_scheme = &url[scheme_end + 3..];
+ if let Some(at_pos) = after_scheme.find('@') {
+ let auth_part = &after_scheme[..at_pos];
+ if let Some(colon_pos) = auth_part.find(':') {
+ // Found user:password@host pattern
+ let user = &auth_part[..colon_pos];
+ let host_part = &after_scheme[at_pos..];
+ return format!("{}://{}:***{}", &url[..scheme_end], user, host_part);
+ }
+ }
+ }
+
+ // Fallback to URL parsing if simple approach fails
+ if let Ok(parsed) = url::Url::parse(url) {
+ if parsed.password().is_some() {
+ let mut masked = parsed.clone();
+ masked.set_password(Some("***")).unwrap();
+ masked.to_string()
+ } else {
+ url.to_string()
+ }
+ } else {
+ url.to_string()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_default_config_validation() {
+ let config = AppConfig::default();
+ // Default configuration should be valid
+ assert!(config.validate().is_ok());
+ }
+
+ #[test]
+ fn test_mask_sensitive() {
+ let url = "postgresql://user:password@localhost/db";
+ let masked = mask_sensitive(url);
+ assert!(masked.contains("***"));
+ assert!(!masked.contains("password"));
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/certificate.rs b/src/database/entities/certificate.rs
new file mode 100644
index 0000000..f42aa98
--- /dev/null
+++ b/src/database/entities/certificate.rs
@@ -0,0 +1,243 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "certificates")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ pub name: String,
+
+ #[sea_orm(column_name = "cert_type")]
+ pub cert_type: String,
+
+ pub domain: String,
+
+ #[serde(skip_serializing)]
+ pub cert_data: Vec,
+
+ #[serde(skip_serializing)]
+ pub key_data: Vec,
+
+ #[serde(skip_serializing)]
+ pub chain_data: Option>,
+
+ pub expires_at: DateTimeUtc,
+
+ pub auto_renew: bool,
+
+ pub created_at: DateTimeUtc,
+
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(has_many = "super::server::Entity")]
+ Servers,
+ #[sea_orm(has_many = "super::server_inbound::Entity")]
+ ServerInbounds,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Servers.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::ServerInbounds.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum CertificateType {
+ SelfSigned,
+ Imported,
+}
+
+impl From for String {
+ fn from(cert_type: CertificateType) -> Self {
+ match cert_type {
+ CertificateType::SelfSigned => "self_signed".to_string(),
+ CertificateType::Imported => "imported".to_string(),
+ }
+ }
+}
+
+impl From for CertificateType {
+ fn from(s: String) -> Self {
+ match s.as_str() {
+ "self_signed" => CertificateType::SelfSigned,
+ "imported" => CertificateType::Imported,
+ _ => CertificateType::SelfSigned,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateCertificateDto {
+ pub name: String,
+ pub cert_type: String,
+ pub domain: String,
+ pub auto_renew: bool,
+ #[serde(default)]
+ pub certificate_pem: String,
+ #[serde(default)]
+ pub private_key: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateCertificateDto {
+ pub name: Option,
+ pub auto_renew: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CertificateResponse {
+ pub id: Uuid,
+ pub name: String,
+ pub cert_type: String,
+ pub domain: String,
+ pub expires_at: DateTimeUtc,
+ pub auto_renew: bool,
+ pub created_at: DateTimeUtc,
+ pub updated_at: DateTimeUtc,
+ pub has_cert_data: bool,
+ pub has_key_data: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CertificateDetailsResponse {
+ pub id: Uuid,
+ pub name: String,
+ pub cert_type: String,
+ pub domain: String,
+ pub expires_at: DateTimeUtc,
+ pub auto_renew: bool,
+ pub created_at: DateTimeUtc,
+ pub updated_at: DateTimeUtc,
+ pub certificate_pem: String,
+ pub has_private_key: bool,
+}
+
+impl From for CertificateResponse {
+ fn from(cert: Model) -> Self {
+ Self {
+ id: cert.id,
+ name: cert.name,
+ cert_type: cert.cert_type,
+ domain: cert.domain,
+ expires_at: cert.expires_at,
+ auto_renew: cert.auto_renew,
+ created_at: cert.created_at,
+ updated_at: cert.updated_at,
+ has_cert_data: !cert.cert_data.is_empty(),
+ has_key_data: !cert.key_data.is_empty(),
+ }
+ }
+}
+
+impl From for CertificateDetailsResponse {
+ fn from(cert: Model) -> Self {
+ let certificate_pem = cert.certificate_pem();
+ let has_private_key = !cert.key_data.is_empty();
+
+ Self {
+ id: cert.id,
+ name: cert.name,
+ cert_type: cert.cert_type,
+ domain: cert.domain,
+ expires_at: cert.expires_at,
+ auto_renew: cert.auto_renew,
+ created_at: cert.created_at,
+ updated_at: cert.updated_at,
+ certificate_pem,
+ has_private_key,
+ }
+ }
+}
+
+impl Model {
+ #[allow(dead_code)]
+ pub fn is_expired(&self) -> bool {
+ self.expires_at < chrono::Utc::now()
+ }
+
+ #[allow(dead_code)]
+ pub fn expires_soon(&self, days: i64) -> bool {
+ let threshold = chrono::Utc::now() + chrono::Duration::days(days);
+ self.expires_at < threshold
+ }
+
+ /// Get certificate data as PEM string
+ pub fn certificate_pem(&self) -> String {
+ String::from_utf8_lossy(&self.cert_data).to_string()
+ }
+
+ /// Get private key data as PEM string
+ pub fn private_key_pem(&self) -> String {
+ String::from_utf8_lossy(&self.key_data).to_string()
+ }
+
+ pub fn apply_update(self, dto: UpdateCertificateDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(name) = dto.name {
+ active_model.name = Set(name);
+ }
+ if let Some(auto_renew) = dto.auto_renew {
+ active_model.auto_renew = Set(auto_renew);
+ }
+
+ active_model
+ }
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateCertificateDto) -> Self {
+ Self {
+ name: Set(dto.name),
+ cert_type: Set(dto.cert_type),
+ domain: Set(dto.domain),
+ cert_data: Set(dto.certificate_pem.into_bytes()),
+ key_data: Set(dto.private_key.into_bytes()),
+ chain_data: Set(None),
+ expires_at: Set(chrono::Utc::now() + chrono::Duration::days(90)), // Default 90 days
+ auto_renew: Set(dto.auto_renew),
+ ..Self::new()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/inbound_template.rs b/src/database/entities/inbound_template.rs
new file mode 100644
index 0000000..df2aff1
--- /dev/null
+++ b/src/database/entities/inbound_template.rs
@@ -0,0 +1,278 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "inbound_templates")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ pub name: String,
+
+ pub description: Option,
+
+ pub protocol: String,
+
+ pub default_port: i32,
+
+ pub base_settings: Value,
+
+ pub stream_settings: Value,
+
+ pub requires_tls: bool,
+
+ pub requires_domain: bool,
+
+ pub variables: Value,
+
+ pub is_active: bool,
+
+ pub created_at: DateTimeUtc,
+
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(has_many = "super::server_inbound::Entity")]
+ ServerInbounds,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::ServerInbounds.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum Protocol {
+ Vless,
+ Vmess,
+ Trojan,
+ Shadowsocks,
+}
+
+impl From for String {
+ fn from(protocol: Protocol) -> Self {
+ match protocol {
+ Protocol::Vless => "vless".to_string(),
+ Protocol::Vmess => "vmess".to_string(),
+ Protocol::Trojan => "trojan".to_string(),
+ Protocol::Shadowsocks => "shadowsocks".to_string(),
+ }
+ }
+}
+
+impl From for Protocol {
+ fn from(s: String) -> Self {
+ match s.as_str() {
+ "vless" => Protocol::Vless,
+ "vmess" => Protocol::Vmess,
+ "trojan" => Protocol::Trojan,
+ "shadowsocks" => Protocol::Shadowsocks,
+ _ => Protocol::Vless,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TemplateVariable {
+ pub key: String,
+ pub var_type: VariableType,
+ pub required: bool,
+ pub default_value: Option,
+ pub description: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum VariableType {
+ String,
+ Number,
+ Path,
+ Domain,
+ Port,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateInboundTemplateDto {
+ pub name: String,
+ pub protocol: String,
+ pub default_port: i32,
+ pub requires_tls: bool,
+ pub config_template: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateInboundTemplateDto {
+ pub name: Option,
+ pub description: Option,
+ pub default_port: Option,
+ pub base_settings: Option,
+ pub stream_settings: Option,
+ pub requires_tls: Option,
+ pub requires_domain: Option,
+ pub variables: Option>,
+ pub is_active: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct InboundTemplateResponse {
+ pub id: Uuid,
+ pub name: String,
+ pub description: Option,
+ pub protocol: String,
+ pub default_port: i32,
+ pub base_settings: Value,
+ pub stream_settings: Value,
+ pub requires_tls: bool,
+ pub requires_domain: bool,
+ pub variables: Vec,
+ pub is_active: bool,
+ pub created_at: DateTimeUtc,
+ pub updated_at: DateTimeUtc,
+}
+
+impl From for InboundTemplateResponse {
+ fn from(template: Model) -> Self {
+ let variables = template.get_variables();
+ Self {
+ id: template.id,
+ name: template.name,
+ description: template.description,
+ protocol: template.protocol,
+ default_port: template.default_port,
+ base_settings: template.base_settings,
+ stream_settings: template.stream_settings,
+ requires_tls: template.requires_tls,
+ requires_domain: template.requires_domain,
+ variables,
+ is_active: template.is_active,
+ created_at: template.created_at,
+ updated_at: template.updated_at,
+ }
+ }
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateInboundTemplateDto) -> Self {
+ // Parse config_template as JSON or use default
+ let config_json: Value = serde_json::from_str(&dto.config_template)
+ .unwrap_or_else(|_| serde_json::json!({}));
+
+ Self {
+ name: Set(dto.name),
+ description: Set(None),
+ protocol: Set(dto.protocol),
+ default_port: Set(dto.default_port),
+ base_settings: Set(config_json.clone()),
+ stream_settings: Set(serde_json::json!({})),
+ requires_tls: Set(dto.requires_tls),
+ requires_domain: Set(false),
+ variables: Set(Value::Array(vec![])),
+ is_active: Set(true),
+ ..Self::new()
+ }
+ }
+}
+
+impl Model {
+ pub fn get_variables(&self) -> Vec {
+ serde_json::from_value(self.variables.clone()).unwrap_or_default()
+ }
+
+ #[allow(dead_code)]
+ pub fn apply_variables(&self, values: &serde_json::Map) -> Result<(Value, Value), String> {
+ let base_settings = self.base_settings.clone();
+ let stream_settings = self.stream_settings.clone();
+
+ // Replace variables in JSON using simple string replacement
+ let base_str = base_settings.to_string();
+ let stream_str = stream_settings.to_string();
+
+ let mut result_base = base_str;
+ let mut result_stream = stream_str;
+
+ for (key, value) in values {
+ let placeholder = format!("${{{}}}", key);
+ let replacement = match value {
+ Value::String(s) => s.clone(),
+ Value::Number(n) => n.to_string(),
+ _ => value.to_string(),
+ };
+ result_base = result_base.replace(&placeholder, &replacement);
+ result_stream = result_stream.replace(&placeholder, &replacement);
+ }
+
+ let final_base: Value = serde_json::from_str(&result_base)
+ .map_err(|e| format!("Invalid base settings after variable substitution: {}", e))?;
+ let final_stream: Value = serde_json::from_str(&result_stream)
+ .map_err(|e| format!("Invalid stream settings after variable substitution: {}", e))?;
+
+ Ok((final_base, final_stream))
+ }
+
+ pub fn apply_update(self, dto: UpdateInboundTemplateDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(name) = dto.name {
+ active_model.name = Set(name);
+ }
+ if let Some(description) = dto.description {
+ active_model.description = Set(Some(description));
+ }
+ if let Some(default_port) = dto.default_port {
+ active_model.default_port = Set(default_port);
+ }
+ if let Some(base_settings) = dto.base_settings {
+ active_model.base_settings = Set(base_settings);
+ }
+ if let Some(stream_settings) = dto.stream_settings {
+ active_model.stream_settings = Set(stream_settings);
+ }
+ if let Some(requires_tls) = dto.requires_tls {
+ active_model.requires_tls = Set(requires_tls);
+ }
+ if let Some(requires_domain) = dto.requires_domain {
+ active_model.requires_domain = Set(requires_domain);
+ }
+ if let Some(variables) = dto.variables {
+ active_model.variables = Set(serde_json::to_value(variables).unwrap_or(Value::Array(vec![])));
+ }
+ if let Some(is_active) = dto.is_active {
+ active_model.is_active = Set(is_active);
+ }
+
+ active_model
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/inbound_users.rs b/src/database/entities/inbound_users.rs
new file mode 100644
index 0000000..9d288f0
--- /dev/null
+++ b/src/database/entities/inbound_users.rs
@@ -0,0 +1,168 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "inbound_users")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ pub server_inbound_id: Uuid,
+
+ pub username: String,
+
+ pub email: String,
+
+ pub xray_user_id: String,
+
+ pub level: i32,
+
+ pub is_active: bool,
+
+ pub created_at: DateTimeUtc,
+
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::server_inbound::Entity",
+ from = "Column::ServerInboundId",
+ to = "super::server_inbound::Column::Id"
+ )]
+ ServerInbound,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::ServerInbound.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+}
+
+/// Inbound user creation data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateInboundUserDto {
+ pub server_inbound_id: Uuid,
+ pub username: String,
+ pub level: Option,
+}
+
+impl CreateInboundUserDto {
+ /// Generate email in format: username@OutFleet
+ pub fn generate_email(&self) -> String {
+ format!("{}@OutFleet", self.username)
+ }
+
+ /// Generate UUID for xray user
+ pub fn generate_xray_user_id(&self) -> String {
+ Uuid::new_v4().to_string()
+ }
+}
+
+/// Inbound user update data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateInboundUserDto {
+ pub username: Option,
+ pub level: Option,
+ pub is_active: Option,
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateInboundUserDto) -> Self {
+ let email = dto.generate_email();
+ let xray_user_id = dto.generate_xray_user_id();
+
+ Self {
+ server_inbound_id: Set(dto.server_inbound_id),
+ username: Set(dto.username),
+ email: Set(email),
+ xray_user_id: Set(xray_user_id),
+ level: Set(dto.level.unwrap_or(0)),
+ is_active: Set(true),
+ ..Self::new()
+ }
+ }
+}
+
+impl Model {
+ /// Update this model with data from UpdateInboundUserDto
+ pub fn apply_update(self, dto: UpdateInboundUserDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(username) = dto.username {
+ let new_email = format!("{}@OutFleet", username);
+ active_model.username = Set(username);
+ active_model.email = Set(new_email);
+ }
+ if let Some(level) = dto.level {
+ active_model.level = Set(level);
+ }
+ if let Some(is_active) = dto.is_active {
+ active_model.is_active = Set(is_active);
+ }
+
+ active_model
+ }
+}
+
+/// Response model for inbound user
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct InboundUserResponse {
+ pub id: Uuid,
+ pub server_inbound_id: Uuid,
+ pub username: String,
+ pub email: String,
+ pub xray_user_id: String,
+ pub level: i32,
+ pub is_active: bool,
+ pub created_at: String,
+ pub updated_at: String,
+}
+
+impl From for InboundUserResponse {
+ fn from(model: Model) -> Self {
+ Self {
+ id: model.id,
+ server_inbound_id: model.server_inbound_id,
+ username: model.username,
+ email: model.email,
+ xray_user_id: model.xray_user_id,
+ level: model.level,
+ is_active: model.is_active,
+ created_at: model.created_at.to_rfc3339(),
+ updated_at: model.updated_at.to_rfc3339(),
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/mod.rs b/src/database/entities/mod.rs
new file mode 100644
index 0000000..9b62d50
--- /dev/null
+++ b/src/database/entities/mod.rs
@@ -0,0 +1,16 @@
+pub mod user;
+pub mod certificate;
+pub mod inbound_template;
+pub mod server;
+pub mod server_inbound;
+pub mod user_access;
+pub mod inbound_users;
+
+pub mod prelude {
+ pub use super::certificate::Entity as Certificate;
+ pub use super::inbound_template::Entity as InboundTemplate;
+ pub use super::server::Entity as Server;
+ pub use super::server_inbound::Entity as ServerInbound;
+ pub use super::user_access::Entity as UserAccess;
+ pub use super::inbound_users::Entity as InboundUsers;
+}
\ No newline at end of file
diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs
new file mode 100644
index 0000000..b38861a
--- /dev/null
+++ b/src/database/entities/server.rs
@@ -0,0 +1,212 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "servers")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ pub name: String,
+
+ pub hostname: String,
+
+ pub grpc_port: i32,
+
+ #[serde(skip_serializing)]
+ pub api_credentials: Option,
+
+ pub status: String,
+
+ pub default_certificate_id: Option,
+
+ pub created_at: DateTimeUtc,
+
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::certificate::Entity",
+ from = "Column::DefaultCertificateId",
+ to = "super::certificate::Column::Id"
+ )]
+ DefaultCertificate,
+ #[sea_orm(has_many = "super::server_inbound::Entity")]
+ ServerInbounds,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::DefaultCertificate.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::ServerInbounds.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ status: Set(ServerStatus::Unknown.into()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum ServerStatus {
+ Unknown,
+ Online,
+ Offline,
+ Error,
+ Connecting,
+}
+
+impl From for String {
+ fn from(status: ServerStatus) -> Self {
+ match status {
+ ServerStatus::Unknown => "unknown".to_string(),
+ ServerStatus::Online => "online".to_string(),
+ ServerStatus::Offline => "offline".to_string(),
+ ServerStatus::Error => "error".to_string(),
+ ServerStatus::Connecting => "connecting".to_string(),
+ }
+ }
+}
+
+impl From for ServerStatus {
+ fn from(s: String) -> Self {
+ match s.as_str() {
+ "online" => ServerStatus::Online,
+ "offline" => ServerStatus::Offline,
+ "error" => ServerStatus::Error,
+ "connecting" => ServerStatus::Connecting,
+ _ => ServerStatus::Unknown,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateServerDto {
+ pub name: String,
+ pub hostname: String,
+ pub grpc_port: Option,
+ pub api_credentials: Option,
+ pub default_certificate_id: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateServerDto {
+ pub name: Option,
+ pub hostname: Option,
+ pub grpc_port: Option,
+ pub api_credentials: Option,
+ pub status: Option,
+ pub default_certificate_id: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ServerResponse {
+ pub id: Uuid,
+ pub name: String,
+ pub hostname: String,
+ pub grpc_port: i32,
+ pub status: String,
+ pub default_certificate_id: Option,
+ pub created_at: DateTimeUtc,
+ pub updated_at: DateTimeUtc,
+ pub has_credentials: bool,
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateServerDto) -> Self {
+ Self {
+ name: Set(dto.name),
+ hostname: Set(dto.hostname),
+ grpc_port: Set(dto.grpc_port.unwrap_or(2053)),
+ api_credentials: Set(dto.api_credentials),
+ status: Set("unknown".to_string()),
+ default_certificate_id: Set(dto.default_certificate_id),
+ ..Self::new()
+ }
+ }
+}
+
+impl From for ServerResponse {
+ fn from(server: Model) -> Self {
+ Self {
+ id: server.id,
+ name: server.name,
+ hostname: server.hostname,
+ grpc_port: server.grpc_port,
+ status: server.status,
+ default_certificate_id: server.default_certificate_id,
+ created_at: server.created_at,
+ updated_at: server.updated_at,
+ has_credentials: server.api_credentials.is_some(),
+ }
+ }
+}
+
+impl Model {
+ pub fn apply_update(self, dto: UpdateServerDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(name) = dto.name {
+ active_model.name = Set(name);
+ }
+ if let Some(hostname) = dto.hostname {
+ active_model.hostname = Set(hostname);
+ }
+ if let Some(grpc_port) = dto.grpc_port {
+ active_model.grpc_port = Set(grpc_port);
+ }
+ if let Some(api_credentials) = dto.api_credentials {
+ active_model.api_credentials = Set(Some(api_credentials));
+ }
+ if let Some(status) = dto.status {
+ active_model.status = Set(status);
+ }
+ if let Some(default_certificate_id) = dto.default_certificate_id {
+ active_model.default_certificate_id = Set(Some(default_certificate_id));
+ }
+
+ active_model
+ }
+
+ pub fn get_grpc_endpoint(&self) -> String {
+ format!("{}:{}", self.hostname, self.grpc_port)
+ }
+
+ #[allow(dead_code)]
+ pub fn get_status(&self) -> ServerStatus {
+ self.status.clone().into()
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/server_inbound.rs b/src/database/entities/server_inbound.rs
new file mode 100644
index 0000000..32a71d8
--- /dev/null
+++ b/src/database/entities/server_inbound.rs
@@ -0,0 +1,204 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "server_inbounds")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ pub server_id: Uuid,
+
+ pub template_id: Uuid,
+
+ pub tag: String,
+
+ pub port_override: Option,
+
+ pub certificate_id: Option,
+
+ pub variable_values: Value,
+
+ pub is_active: bool,
+
+ pub created_at: DateTimeUtc,
+
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::server::Entity",
+ from = "Column::ServerId",
+ to = "super::server::Column::Id"
+ )]
+ Server,
+ #[sea_orm(
+ belongs_to = "super::inbound_template::Entity",
+ from = "Column::TemplateId",
+ to = "super::inbound_template::Column::Id"
+ )]
+ Template,
+ #[sea_orm(
+ belongs_to = "super::certificate::Entity",
+ from = "Column::CertificateId",
+ to = "super::certificate::Column::Id"
+ )]
+ Certificate,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Server.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Template.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Certificate.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateServerInboundDto {
+ pub template_id: Uuid,
+ pub port: i32,
+ pub certificate_id: Option,
+ pub is_active: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateServerInboundDto {
+ pub tag: Option,
+ pub port_override: Option,
+ pub certificate_id: Option,
+ pub variable_values: Option>,
+ pub is_active: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ServerInboundResponse {
+ pub id: Uuid,
+ pub server_id: Uuid,
+ pub template_id: Uuid,
+ pub tag: String,
+ pub port: i32,
+ pub certificate_id: Option,
+ pub variable_values: Value,
+ pub is_active: bool,
+ pub created_at: DateTimeUtc,
+ pub updated_at: DateTimeUtc,
+ // Populated by joins (simplified for now)
+ pub template_name: Option,
+ pub certificate_name: Option,
+}
+
+impl From for ServerInboundResponse {
+ fn from(inbound: Model) -> Self {
+ Self {
+ id: inbound.id,
+ server_id: inbound.server_id,
+ template_id: inbound.template_id,
+ tag: inbound.tag,
+ port: inbound.port_override.unwrap_or(443), // Default port if not set
+ certificate_id: inbound.certificate_id,
+ variable_values: inbound.variable_values,
+ is_active: inbound.is_active,
+ created_at: inbound.created_at,
+ updated_at: inbound.updated_at,
+ template_name: None, // Will be filled by repository if needed
+ certificate_name: None, // Will be filled by repository if needed
+ }
+ }
+}
+
+impl Model {
+ pub fn apply_update(self, dto: UpdateServerInboundDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(tag) = dto.tag {
+ active_model.tag = Set(tag);
+ }
+ if let Some(port_override) = dto.port_override {
+ active_model.port_override = Set(Some(port_override));
+ }
+ if let Some(certificate_id) = dto.certificate_id {
+ active_model.certificate_id = Set(Some(certificate_id));
+ }
+ if let Some(variable_values) = dto.variable_values {
+ active_model.variable_values = Set(Value::Object(variable_values));
+ }
+ if let Some(is_active) = dto.is_active {
+ active_model.is_active = Set(is_active);
+ }
+
+ active_model
+ }
+
+ #[allow(dead_code)]
+ pub fn get_variable_values(&self) -> serde_json::Map {
+ if let Value::Object(map) = &self.variable_values {
+ map.clone()
+ } else {
+ serde_json::Map::new()
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_effective_port(&self, template_default_port: i32) -> i32 {
+ self.port_override.unwrap_or(template_default_port)
+ }
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateServerInboundDto) -> Self {
+ Self {
+ template_id: Set(dto.template_id),
+ tag: Set(format!("inbound-{}", Uuid::new_v4())), // Generate unique tag
+ port_override: Set(Some(dto.port)),
+ certificate_id: Set(dto.certificate_id),
+ variable_values: Set(Value::Object(serde_json::Map::new())),
+ is_active: Set(dto.is_active),
+ ..Self::new()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs
new file mode 100644
index 0000000..25ee8b3
--- /dev/null
+++ b/src/database/entities/user.rs
@@ -0,0 +1,185 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "users")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ /// User display name
+ pub name: String,
+
+ /// Optional comment/description about the user
+ #[sea_orm(column_type = "Text")]
+ pub comment: Option,
+
+ /// Optional Telegram user ID for bot integration
+ pub telegram_id: Option,
+
+ /// When the user was registered/created
+ pub created_at: DateTimeUtc,
+
+ /// Last time user record was updated
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {
+ /// Called before insert and update
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ /// Called before update
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+}
+
+/// User creation data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateUserDto {
+ pub name: String,
+ pub comment: Option,
+ pub telegram_id: Option,
+}
+
+/// User update data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateUserDto {
+ pub name: Option,
+ pub comment: Option,
+ pub telegram_id: Option,
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateUserDto) -> Self {
+ Self {
+ name: Set(dto.name),
+ comment: Set(dto.comment),
+ telegram_id: Set(dto.telegram_id),
+ ..Self::new()
+ }
+ }
+}
+
+impl Model {
+ /// Update this model with data from UpdateUserDto
+ pub fn apply_update(self, dto: UpdateUserDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(name) = dto.name {
+ active_model.name = Set(name);
+ }
+ if let Some(comment) = dto.comment {
+ active_model.comment = Set(Some(comment));
+ } else if dto.comment.is_some() {
+ // Explicitly set to None if Some(None) was passed
+ active_model.comment = Set(None);
+ }
+ if dto.telegram_id.is_some() {
+ active_model.telegram_id = Set(dto.telegram_id);
+ }
+
+ active_model
+ }
+
+ /// Check if user has Telegram integration
+ #[allow(dead_code)]
+ pub fn has_telegram(&self) -> bool {
+ self.telegram_id.is_some()
+ }
+
+ /// Get display name with optional comment
+ #[allow(dead_code)]
+ pub fn display_name(&self) -> String {
+ match &self.comment {
+ Some(comment) if !comment.is_empty() => format!("{} ({})", self.name, comment),
+ _ => self.name.clone(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_create_user_dto_conversion() {
+ let dto = CreateUserDto {
+ name: "Test User".to_string(),
+ comment: Some("Test comment".to_string()),
+ telegram_id: Some(123456789),
+ };
+
+ let active_model: ActiveModel = dto.into();
+
+ assert_eq!(active_model.name.unwrap(), "Test User");
+ assert_eq!(active_model.comment.unwrap(), Some("Test comment".to_string()));
+ assert_eq!(active_model.telegram_id.unwrap(), Some(123456789));
+ }
+
+ #[test]
+ fn test_user_display_name() {
+ let user = Model {
+ id: Uuid::new_v4(),
+ name: "John Doe".to_string(),
+ comment: Some("Admin user".to_string()),
+ telegram_id: None,
+ created_at: chrono::Utc::now(),
+ updated_at: chrono::Utc::now(),
+ };
+
+ assert_eq!(user.display_name(), "John Doe (Admin user)");
+
+ let user_no_comment = Model {
+ comment: None,
+ ..user
+ };
+
+ assert_eq!(user_no_comment.display_name(), "John Doe");
+ }
+
+ #[test]
+ fn test_has_telegram() {
+ let user_with_telegram = Model {
+ id: Uuid::new_v4(),
+ name: "User".to_string(),
+ comment: None,
+ telegram_id: Some(123456789),
+ created_at: chrono::Utc::now(),
+ updated_at: chrono::Utc::now(),
+ };
+
+ let user_without_telegram = Model {
+ telegram_id: None,
+ ..user_with_telegram.clone()
+ };
+
+ assert!(user_with_telegram.has_telegram());
+ assert!(!user_without_telegram.has_telegram());
+ }
+}
\ No newline at end of file
diff --git a/src/database/entities/user_access.rs b/src/database/entities/user_access.rs
new file mode 100644
index 0000000..2a222cf
--- /dev/null
+++ b/src/database/entities/user_access.rs
@@ -0,0 +1,188 @@
+use sea_orm::entity::prelude::*;
+use sea_orm::{Set, ActiveModelTrait};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+#[sea_orm(table_name = "user_access")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: Uuid,
+
+ /// User ID this access is for
+ pub user_id: Uuid,
+
+ /// Server ID this access applies to
+ pub server_id: Uuid,
+
+ /// Server inbound ID this access applies to
+ pub server_inbound_id: Uuid,
+
+ /// User's unique identifier in xray (UUID for VLESS/VMess, password for Trojan)
+ pub xray_user_id: String,
+
+ /// User's email in xray
+ pub xray_email: String,
+
+ /// User level in xray (0-255)
+ pub level: i32,
+
+ /// Whether this access is currently active
+ pub is_active: bool,
+
+ /// When this access was created
+ pub created_at: DateTimeUtc,
+
+ /// Last time this access was updated
+ pub updated_at: DateTimeUtc,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::user::Entity",
+ from = "Column::UserId",
+ to = "super::user::Column::Id"
+ )]
+ User,
+ #[sea_orm(
+ belongs_to = "super::server::Entity",
+ from = "Column::ServerId",
+ to = "super::server::Column::Id"
+ )]
+ Server,
+ #[sea_orm(
+ belongs_to = "super::server_inbound::Entity",
+ from = "Column::ServerInboundId",
+ to = "super::server_inbound::Column::Id"
+ )]
+ ServerInbound,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::User.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Server.def()
+ }
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::ServerInbound.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {
+ fn new() -> Self {
+ Self {
+ id: Set(Uuid::new_v4()),
+ created_at: Set(chrono::Utc::now()),
+ updated_at: Set(chrono::Utc::now()),
+ ..ActiveModelTrait::default()
+ }
+ }
+
+ fn before_save<'life0, 'async_trait, C>(
+ mut self,
+ _db: &'life0 C,
+ insert: bool,
+ ) -> core::pin::Pin> + Send + 'async_trait>>
+ where
+ 'life0: 'async_trait,
+ C: 'async_trait + ConnectionTrait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ if !insert {
+ self.updated_at = Set(chrono::Utc::now());
+ }
+ Ok(self)
+ })
+ }
+
+}
+
+/// User access creation data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateUserAccessDto {
+ pub user_id: Uuid,
+ pub server_id: Uuid,
+ pub server_inbound_id: Uuid,
+ pub xray_user_id: String,
+ pub xray_email: String,
+ pub level: Option,
+}
+
+/// User access update data transfer object
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UpdateUserAccessDto {
+ pub is_active: Option,
+ pub level: Option,
+}
+
+impl From for ActiveModel {
+ fn from(dto: CreateUserAccessDto) -> Self {
+ Self {
+ user_id: Set(dto.user_id),
+ server_id: Set(dto.server_id),
+ server_inbound_id: Set(dto.server_inbound_id),
+ xray_user_id: Set(dto.xray_user_id),
+ xray_email: Set(dto.xray_email),
+ level: Set(dto.level.unwrap_or(0)),
+ is_active: Set(true),
+ ..Self::new()
+ }
+ }
+}
+
+impl Model {
+ /// Update this model with data from UpdateUserAccessDto
+ pub fn apply_update(self, dto: UpdateUserAccessDto) -> ActiveModel {
+ let mut active_model: ActiveModel = self.into();
+
+ if let Some(is_active) = dto.is_active {
+ active_model.is_active = Set(is_active);
+ }
+ if let Some(level) = dto.level {
+ active_model.level = Set(level);
+ }
+
+ active_model
+ }
+}
+
+/// Response model for user access
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UserAccessResponse {
+ pub id: Uuid,
+ pub user_id: Uuid,
+ pub server_id: Uuid,
+ pub server_inbound_id: Uuid,
+ pub xray_user_id: String,
+ pub xray_email: String,
+ pub level: i32,
+ pub is_active: bool,
+ pub created_at: String,
+ pub updated_at: String,
+}
+
+impl From for UserAccessResponse {
+ fn from(model: Model) -> Self {
+ Self {
+ id: model.id,
+ user_id: model.user_id,
+ server_id: model.server_id,
+ server_inbound_id: model.server_inbound_id,
+ xray_user_id: model.xray_user_id,
+ xray_email: model.xray_email,
+ level: model.level,
+ is_active: model.is_active,
+ created_at: model.created_at.to_rfc3339(),
+ updated_at: model.updated_at.to_rfc3339(),
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000001_create_users_table.rs b/src/database/migrations/m20241201_000001_create_users_table.rs
new file mode 100644
index 0000000..4e38563
--- /dev/null
+++ b/src/database/migrations/m20241201_000001_create_users_table.rs
@@ -0,0 +1,135 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ // Create users table
+ manager
+ .create_table(
+ Table::create()
+ .table(Users::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(Users::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(Users::Name)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Users::Comment)
+ .text()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(Users::TelegramId)
+ .big_integer()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(Users::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Users::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Create index on name for faster searches
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_users_name")
+ .table(Users::Table)
+ .col(Users::Name)
+ .to_owned(),
+ )
+ .await?;
+
+ // Create unique index on telegram_id (if not null)
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_users_telegram_id")
+ .table(Users::Table)
+ .col(Users::TelegramId)
+ .unique()
+ .to_owned(),
+ )
+ .await?;
+
+ // Create index on created_at for sorting
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_users_created_at")
+ .table(Users::Table)
+ .col(Users::CreatedAt)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ // Drop indexes first
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_users_created_at")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_users_telegram_id")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_users_name")
+ .to_owned(),
+ )
+ .await?;
+
+ // Drop table
+ manager
+ .drop_table(Table::drop().table(Users::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum Users {
+ Table,
+ Id,
+ Name,
+ Comment,
+ TelegramId,
+ CreatedAt,
+ UpdatedAt,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000002_create_certificates_table.rs b/src/database/migrations/m20241201_000002_create_certificates_table.rs
new file mode 100644
index 0000000..a4b6722
--- /dev/null
+++ b/src/database/migrations/m20241201_000002_create_certificates_table.rs
@@ -0,0 +1,120 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(Certificates::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(Certificates::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(Certificates::Name)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::CertType)
+ .string_len(50)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::Domain)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::CertData)
+ .blob()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::KeyData)
+ .blob()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::ChainData)
+ .blob()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::ExpiresAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::AutoRenew)
+ .boolean()
+ .default(false)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Certificates::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Index on domain for faster lookups
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_certificates_domain")
+ .table(Certificates::Table)
+ .col(Certificates::Domain)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_certificates_domain")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_table(Table::drop().table(Certificates::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum Certificates {
+ Table,
+ Id,
+ Name,
+ CertType,
+ Domain,
+ CertData,
+ KeyData,
+ ChainData,
+ ExpiresAt,
+ AutoRenew,
+ CreatedAt,
+ UpdatedAt,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000003_create_inbound_templates_table.rs b/src/database/migrations/m20241201_000003_create_inbound_templates_table.rs
new file mode 100644
index 0000000..ab83340
--- /dev/null
+++ b/src/database/migrations/m20241201_000003_create_inbound_templates_table.rs
@@ -0,0 +1,155 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(InboundTemplates::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(InboundTemplates::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::Name)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::Description)
+ .text()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::Protocol)
+ .string_len(50)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::DefaultPort)
+ .integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::BaseSettings)
+ .json()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::StreamSettings)
+ .json()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::RequiresTls)
+ .boolean()
+ .default(false)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::RequiresDomain)
+ .boolean()
+ .default(false)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::Variables)
+ .json()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::IsActive)
+ .boolean()
+ .default(true)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundTemplates::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Index on name for searches
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_inbound_templates_name")
+ .table(InboundTemplates::Table)
+ .col(InboundTemplates::Name)
+ .to_owned(),
+ )
+ .await?;
+
+ // Index on protocol
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_inbound_templates_protocol")
+ .table(InboundTemplates::Table)
+ .col(InboundTemplates::Protocol)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_inbound_templates_protocol")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_inbound_templates_name")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_table(Table::drop().table(InboundTemplates::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum InboundTemplates {
+ Table,
+ Id,
+ Name,
+ Description,
+ Protocol,
+ DefaultPort,
+ BaseSettings,
+ StreamSettings,
+ RequiresTls,
+ RequiresDomain,
+ Variables,
+ IsActive,
+ CreatedAt,
+ UpdatedAt,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000004_create_servers_table.rs b/src/database/migrations/m20241201_000004_create_servers_table.rs
new file mode 100644
index 0000000..c044c6c
--- /dev/null
+++ b/src/database/migrations/m20241201_000004_create_servers_table.rs
@@ -0,0 +1,136 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(Servers::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(Servers::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(Servers::Name)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Servers::Hostname)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Servers::GrpcPort)
+ .integer()
+ .default(2053)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Servers::ApiCredentials)
+ .text()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(Servers::Status)
+ .string_len(50)
+ .default("unknown")
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Servers::DefaultCertificateId)
+ .uuid()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(Servers::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(Servers::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Foreign key to certificates
+ manager
+ .create_foreign_key(
+ ForeignKey::create()
+ .name("fk_servers_default_certificate")
+ .from(Servers::Table, Servers::DefaultCertificateId)
+ .to(Certificates::Table, Certificates::Id)
+ .on_delete(ForeignKeyAction::SetNull)
+ .to_owned(),
+ )
+ .await?;
+
+ // Index on hostname
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_servers_hostname")
+ .table(Servers::Table)
+ .col(Servers::Hostname)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_foreign_key(
+ ForeignKey::drop()
+ .name("fk_servers_default_certificate")
+ .table(Servers::Table)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_servers_hostname")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_table(Table::drop().table(Servers::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum Servers {
+ Table,
+ Id,
+ Name,
+ Hostname,
+ GrpcPort,
+ ApiCredentials,
+ Status,
+ DefaultCertificateId,
+ CreatedAt,
+ UpdatedAt,
+}
+
+#[derive(DeriveIden)]
+enum Certificates {
+ Table,
+ Id,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000005_create_server_inbounds_table.rs b/src/database/migrations/m20241201_000005_create_server_inbounds_table.rs
new file mode 100644
index 0000000..f342fda
--- /dev/null
+++ b/src/database/migrations/m20241201_000005_create_server_inbounds_table.rs
@@ -0,0 +1,195 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(ServerInbounds::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(ServerInbounds::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::ServerId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::TemplateId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::Tag)
+ .string_len(255)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::PortOverride)
+ .integer()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::CertificateId)
+ .uuid()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::VariableValues)
+ .json()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::IsActive)
+ .boolean()
+ .default(true)
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(ServerInbounds::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Foreign keys
+ manager
+ .create_foreign_key(
+ ForeignKey::create()
+ .name("fk_server_inbounds_server")
+ .from(ServerInbounds::Table, ServerInbounds::ServerId)
+ .to(Servers::Table, Servers::Id)
+ .on_delete(ForeignKeyAction::Cascade)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .create_foreign_key(
+ ForeignKey::create()
+ .name("fk_server_inbounds_template")
+ .from(ServerInbounds::Table, ServerInbounds::TemplateId)
+ .to(InboundTemplates::Table, InboundTemplates::Id)
+ .on_delete(ForeignKeyAction::Restrict)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .create_foreign_key(
+ ForeignKey::create()
+ .name("fk_server_inbounds_certificate")
+ .from(ServerInbounds::Table, ServerInbounds::CertificateId)
+ .to(Certificates::Table, Certificates::Id)
+ .on_delete(ForeignKeyAction::SetNull)
+ .to_owned(),
+ )
+ .await?;
+
+ // Unique constraint on server_id + tag
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_server_inbounds_server_tag")
+ .table(ServerInbounds::Table)
+ .col(ServerInbounds::ServerId)
+ .col(ServerInbounds::Tag)
+ .unique()
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_foreign_key(
+ ForeignKey::drop()
+ .name("fk_server_inbounds_certificate")
+ .table(ServerInbounds::Table)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_foreign_key(
+ ForeignKey::drop()
+ .name("fk_server_inbounds_template")
+ .table(ServerInbounds::Table)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_foreign_key(
+ ForeignKey::drop()
+ .name("fk_server_inbounds_server")
+ .table(ServerInbounds::Table)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_server_inbounds_server_tag")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_table(Table::drop().table(ServerInbounds::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum ServerInbounds {
+ Table,
+ Id,
+ ServerId,
+ TemplateId,
+ Tag,
+ PortOverride,
+ CertificateId,
+ VariableValues,
+ IsActive,
+ CreatedAt,
+ UpdatedAt,
+}
+
+#[derive(DeriveIden)]
+enum Servers {
+ Table,
+ Id,
+}
+
+#[derive(DeriveIden)]
+enum InboundTemplates {
+ Table,
+ Id,
+}
+
+#[derive(DeriveIden)]
+enum Certificates {
+ Table,
+ Id,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000006_create_user_access_table.rs b/src/database/migrations/m20241201_000006_create_user_access_table.rs
new file mode 100644
index 0000000..f247302
--- /dev/null
+++ b/src/database/migrations/m20241201_000006_create_user_access_table.rs
@@ -0,0 +1,196 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(UserAccess::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(UserAccess::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::UserId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::ServerId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::ServerInboundId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::XrayUserId)
+ .string()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::XrayEmail)
+ .string()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::Level)
+ .integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::IsActive)
+ .boolean()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(UserAccess::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .foreign_key(
+ ForeignKey::create()
+ .name("fk_user_access_user_id")
+ .from(UserAccess::Table, UserAccess::UserId)
+ .to(Users::Table, Users::Id)
+ .on_delete(ForeignKeyAction::Cascade),
+ )
+ .foreign_key(
+ ForeignKey::create()
+ .name("fk_user_access_server_id")
+ .from(UserAccess::Table, UserAccess::ServerId)
+ .to(Servers::Table, Servers::Id)
+ .on_delete(ForeignKeyAction::Cascade),
+ )
+ .foreign_key(
+ ForeignKey::create()
+ .name("fk_user_access_server_inbound_id")
+ .from(UserAccess::Table, UserAccess::ServerInboundId)
+ .to(ServerInbounds::Table, ServerInbounds::Id)
+ .on_delete(ForeignKeyAction::Cascade),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Create indexes separately
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_user_access_server_inbound")
+ .table(UserAccess::Table)
+ .col(UserAccess::ServerId)
+ .col(UserAccess::ServerInboundId)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_user_access_user_server")
+ .table(UserAccess::Table)
+ .col(UserAccess::UserId)
+ .col(UserAccess::ServerId)
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_user_access_xray_email")
+ .table(UserAccess::Table)
+ .col(UserAccess::XrayEmail)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ // Drop indexes first
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_user_access_xray_email")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_user_access_user_server")
+ .to_owned(),
+ )
+ .await?;
+
+ manager
+ .drop_index(
+ Index::drop()
+ .if_exists()
+ .name("idx_user_access_server_inbound")
+ .to_owned(),
+ )
+ .await?;
+
+ // Drop table
+ manager
+ .drop_table(Table::drop().table(UserAccess::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum UserAccess {
+ Table,
+ Id,
+ UserId,
+ ServerId,
+ ServerInboundId,
+ XrayUserId,
+ XrayEmail,
+ Level,
+ IsActive,
+ CreatedAt,
+ UpdatedAt,
+}
+
+#[derive(DeriveIden)]
+enum Users {
+ Table,
+ Id,
+}
+
+#[derive(DeriveIden)]
+enum Servers {
+ Table,
+ Id,
+}
+
+#[derive(DeriveIden)]
+enum ServerInbounds {
+ Table,
+ Id,
+}
\ No newline at end of file
diff --git a/src/database/migrations/m20241201_000007_create_inbound_users_table.rs b/src/database/migrations/m20241201_000007_create_inbound_users_table.rs
new file mode 100644
index 0000000..c801546
--- /dev/null
+++ b/src/database/migrations/m20241201_000007_create_inbound_users_table.rs
@@ -0,0 +1,125 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(InboundUsers::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(InboundUsers::Id)
+ .uuid()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::ServerInboundId)
+ .uuid()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::Username)
+ .string()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::Email)
+ .string()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::XrayUserId)
+ .string()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::Level)
+ .integer()
+ .not_null()
+ .default(0),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::IsActive)
+ .boolean()
+ .not_null()
+ .default(true),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(InboundUsers::UpdatedAt)
+ .timestamp_with_time_zone()
+ .not_null(),
+ )
+ .foreign_key(
+ ForeignKey::create()
+ .name("fk_inbound_users_server_inbound")
+ .from(InboundUsers::Table, InboundUsers::ServerInboundId)
+ .to(ServerInbounds::Table, ServerInbounds::Id)
+ .on_delete(ForeignKeyAction::Cascade),
+ )
+ .to_owned(),
+ )
+ .await?;
+
+ // Create unique constraint: one user per inbound
+ manager
+ .create_index(
+ Index::create()
+ .name("idx_inbound_users_unique_user_per_inbound")
+ .table(InboundUsers::Table)
+ .col(InboundUsers::ServerInboundId)
+ .col(InboundUsers::Username)
+ .unique()
+ .to_owned(),
+ )
+ .await?;
+
+ // Create index on email for faster lookups
+ manager
+ .create_index(
+ Index::create()
+ .name("idx_inbound_users_email")
+ .table(InboundUsers::Table)
+ .col(InboundUsers::Email)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_table(Table::drop().table(InboundUsers::Table).to_owned())
+ .await
+ }
+}
+
+#[derive(DeriveIden)]
+enum InboundUsers {
+ Table,
+ Id,
+ ServerInboundId,
+ Username,
+ Email,
+ XrayUserId,
+ Level,
+ IsActive,
+ CreatedAt,
+ UpdatedAt,
+}
+
+#[derive(DeriveIden)]
+enum ServerInbounds {
+ Table,
+ Id,
+}
\ No newline at end of file
diff --git a/src/database/migrations/mod.rs b/src/database/migrations/mod.rs
new file mode 100644
index 0000000..f7c7692
--- /dev/null
+++ b/src/database/migrations/mod.rs
@@ -0,0 +1,26 @@
+use sea_orm_migration::prelude::*;
+
+mod m20241201_000001_create_users_table;
+mod m20241201_000002_create_certificates_table;
+mod m20241201_000003_create_inbound_templates_table;
+mod m20241201_000004_create_servers_table;
+mod m20241201_000005_create_server_inbounds_table;
+mod m20241201_000006_create_user_access_table;
+mod m20241201_000007_create_inbound_users_table;
+
+pub struct Migrator;
+
+#[async_trait::async_trait]
+impl MigratorTrait for Migrator {
+ fn migrations() -> Vec> {
+ vec![
+ Box::new(m20241201_000001_create_users_table::Migration),
+ Box::new(m20241201_000002_create_certificates_table::Migration),
+ Box::new(m20241201_000003_create_inbound_templates_table::Migration),
+ Box::new(m20241201_000004_create_servers_table::Migration),
+ Box::new(m20241201_000005_create_server_inbounds_table::Migration),
+ Box::new(m20241201_000006_create_user_access_table::Migration),
+ Box::new(m20241201_000007_create_inbound_users_table::Migration),
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/database/mod.rs b/src/database/mod.rs
new file mode 100644
index 0000000..4a57c7d
--- /dev/null
+++ b/src/database/mod.rs
@@ -0,0 +1,161 @@
+use anyhow::Result;
+use sea_orm::{Database, DatabaseConnection, ConnectOptions, Statement, DatabaseBackend, ConnectionTrait};
+use sea_orm_migration::MigratorTrait;
+use std::time::Duration;
+use tracing::{info, warn};
+
+use crate::config::DatabaseConfig;
+
+pub mod entities;
+pub mod migrations;
+pub mod repository;
+
+use migrations::Migrator;
+
+/// Database connection and management
+#[derive(Clone)]
+pub struct DatabaseManager {
+ connection: DatabaseConnection,
+}
+
+impl DatabaseManager {
+ /// Create a new database connection
+ pub async fn new(config: &DatabaseConfig) -> Result {
+ info!("Connecting to database...");
+
+ // URL-encode the connection string to handle special characters in passwords
+ let encoded_url = Self::encode_database_url(&config.url)?;
+
+ let mut opt = ConnectOptions::new(&encoded_url);
+ opt.max_connections(config.max_connections)
+ .min_connections(1)
+ .connect_timeout(Duration::from_secs(config.connection_timeout))
+ .acquire_timeout(Duration::from_secs(config.connection_timeout))
+ .idle_timeout(Duration::from_secs(600))
+ .max_lifetime(Duration::from_secs(3600))
+ .sqlx_logging(tracing::level_enabled!(tracing::Level::DEBUG))
+ .sqlx_logging_level(log::LevelFilter::Debug);
+
+ let connection = Database::connect(opt).await?;
+
+ info!("Database connection established successfully");
+
+ let manager = Self { connection };
+
+ // Run migrations if auto_migrate is enabled
+ if config.auto_migrate {
+ manager.migrate().await?;
+ }
+
+ Ok(manager)
+ }
+
+ /// Get database connection
+ pub fn connection(&self) -> &DatabaseConnection {
+ &self.connection
+ }
+
+ /// Run database migrations
+ pub async fn migrate(&self) -> Result<()> {
+ info!("Running database migrations...");
+
+ match Migrator::up(&self.connection, None).await {
+ Ok(_) => {
+ info!("Database migrations completed successfully");
+ Ok(())
+ }
+ Err(e) => {
+ warn!("Migration error: {}", e);
+ Err(e.into())
+ }
+ }
+ }
+
+ /// Check database connection health
+ pub async fn health_check(&self) -> Result {
+ let stmt = Statement::from_string(DatabaseBackend::Postgres, "SELECT 1".to_owned());
+ match self.connection.execute(stmt).await {
+ Ok(_) => Ok(true),
+ Err(e) => {
+ warn!("Database health check failed: {}", e);
+ Ok(false)
+ }
+ }
+ }
+
+ /// Get database schema information
+ pub async fn get_schema_version(&self) -> Result