From d8567704d7727b44819212f8ee3b7f2354283117 Mon Sep 17 00:00:00 2001 From: trustedinster Date: Thu, 18 Jun 2026 12:20:58 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E9=AB=98=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=A8=A1=E5=9D=97=E5=8C=96=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- .env.example | 165 +- README.md | 298 +- alembic.ini | 43 + alembic/env.py | 63 + alembic/script.py.mako | 26 + .../versions/208e77e631c4_initial_schema.py | 841 ++++ app/__init__.py | 2 + app/api/__init__.py | 1 + app/api/deps.py | 20 + app/api/v1/__init__.py | 1 + app/api/v1/audit.py | 35 + app/api/v1/hosts.py | 122 + app/api/v1/operations.py | 139 + app/api/v1/tickets.py | 73 + app/auth/__init__.py | 1 + app/auth/dependencies.py | 114 + app/auth/routes.py | 233 + app/auth/schemas.py | 46 + app/cache/__init__.py | 1 + app/cache/app_shell.py | 100 + app/cache/fragments.py | 77 + app/cache/keys.py | 90 + app/core/__init__.py | 1 + app/core/config.py | 154 + app/core/db.py | 50 + app/core/exceptions.py | 87 + app/core/logging.py | 51 + app/core/redis.py | 34 + app/main.py | 113 + app/models/__init__.py | 135 + app/models/audit.py | 128 + app/models/base.py | 50 + app/models/bootstrap.py | 93 + app/models/certificate.py | 96 + app/models/host.py | 171 + app/models/operations.py | 352 ++ app/models/plugin.py | 54 + app/models/task.py | 66 + app/models/tenant.py | 163 + app/models/theme.py | 56 + app/models/ticket.py | 184 + app/models/user.py | 322 ++ app/plugins/__init__.py | 54 + app/plugins/base.py | 257 + app/plugins/example/__init__.py | 5 + app/plugins/example/plugin.py | 67 + app/plugins/loader.py | 257 + app/plugins/manager.py | 238 + app/plugins/registry.py | 80 + app/security/__init__.py | 10 + app/security/ban_version.py | 48 + app/security/crypto.py | 198 + app/security/field_cipher.py | 52 + app/security/jwt_auth.py | 102 + app/security/password.py | 47 + app/security/refresh_token.py | 74 + app/static/css/base.css | 51 + app/static/js/auth.js | 46 + app/static/vendor/htmx.min.js | 20 + app/tasks/__init__.py | 7 + app/tasks/hosts.py | 91 + app/tasks/huey_app.py | 21 + app/tasks/operations.py | 94 + app/templates/__init__.py | 45 + .../fragments/my_cloud_computers.html | 22 + app/templates/fragments/nav.html | 27 + app/templates/fragments/product_groups.html | 25 + app/templates/fragments/stats.html | 19 + app/templates/layouts/app_shell.html | 47 + app/templates/pages/dashboard.html | 24 + app/templates/pages/login.html | 59 + app/templates/pages/register.html | 55 + app/tenant/__init__.py | 1 + app/tenant/dependencies.py | 49 + app/tenant/middleware.py | 49 + app/tenant/resolver.py | 175 + app/web/__init__.py | 1 + app/web/fragments.py | 185 + app/web/shell.py | 80 + app/winrm/__init__.py | 26 + app/winrm/client.py | 844 ++++ app/winrm/commands.py | 149 + app/winrm/transport.py | 281 ++ apps/__init__.py | 3 - apps/accounts/__init__.py | 4 - apps/accounts/admin.py | 1 - apps/accounts/apps.py | 19 - apps/accounts/captcha_service.py | 59 - apps/accounts/email_service.py | 136 - apps/accounts/forms.py | 281 -- apps/accounts/forms_admin.py | 152 - apps/accounts/forms_superadmin.py | 73 - .../commands/create_demo_superuser.py | 73 - .../management/commands/setup_demo_users.py | 100 - .../commands/setup_provider_group.py | 294 -- apps/accounts/migrations/0001_initial.py | 104 - .../migrations/0002_groupprofile_model.py | 30 - .../migrations/0003_default_groups.py | 98 - .../migrations/0004_normal_user_group.py | 41 - apps/accounts/migrations/0005_auto_staff.py | 46 - .../migrations/0006_registrationlink.py | 37 - .../0007_add_registrationlink_max_uses.py | 53 - .../migrations/0008_user_site_groups.py | 25 - apps/accounts/migrations/0009_useremail.py | 83 - .../migrations/0010_userbanhistory_userban.py | 127 - apps/accounts/migrations/__init__.py | 0 apps/accounts/models.py | 550 -- apps/accounts/provider_decorators.py | 207 - apps/accounts/rate_limit.py | 107 - apps/accounts/signals.py | 28 - apps/accounts/tasks.py | 277 - apps/accounts/urls.py | 79 - apps/accounts/urls_admin.py | 49 - apps/accounts/urls_admin_groups.py | 12 - apps/accounts/urls_admin_providers.py | 33 - apps/accounts/urls_admin_reglinks.py | 11 - apps/accounts/urls_admin_users.py | 31 - apps/accounts/urls_provider.py | 46 - apps/accounts/user_service.py | 371 -- apps/accounts/views.py | 1231 ----- apps/accounts/views_admin.py | 380 -- apps/accounts/views_admin_groups.py | 127 - apps/accounts/views_admin_reglinks.py | 158 - apps/accounts/views_admin_users.py | 195 - apps/accounts/views_provider.py | 50 - apps/accounts/views_superadmin.py | 175 - apps/audit/admin.py | 1 - apps/audit/apps.py | 11 - apps/audit/decorators.py | 281 -- apps/audit/forms_admin.py | 8 - apps/audit/migrations/0001_initial.py | 104 - .../0002_auditlog_tunnel_actions.py | 49 - .../migrations/0003_alter_auditlog_action.py | 18 - ...ditlog_user_agent_alter_auditlog_action.py | 23 - apps/audit/migrations/__init__.py | 0 apps/audit/models.py | 222 - apps/audit/signals.py | 10 - apps/audit/tests/__init__.py | 0 apps/audit/urls.py | 15 - apps/audit/urls_admin.py | 10 - apps/audit/views.py | 568 --- apps/audit/views_admin.py | 91 - apps/bootstrap/README.md | 100 - apps/bootstrap/admin.py | 1 - apps/bootstrap/apps.py | 11 - .../commands/cleanup_expired_sessions.py | 73 - apps/bootstrap/middleware.py | 122 - apps/bootstrap/migrations/0001_initial.py | 39 - ...aired_bootstraptoken_paired_at_and_more.py | 40 - .../0003_bootstraptoken_totp_secret.py | 18 - ...ve_bootstraptoken_pairing_code_and_more.py | 59 - .../migrations/0005_delete_bootstraptoken.py | 16 - .../0006_alter_activesession_expires_at.py | 18 - .../0007_update_initialtoken_for_pairing.py | 33 - .../migrations/0008_add_pairing_attempts.py | 18 - .../0009_initialtoken_host_nullable.py | 20 - .../migrations/0010_add_cert_data.py | 18 - .../0011_add_cert_provision_token.py | 35 - ...2_certprovisiontoken_cert_data_and_more.py | 23 - .../0013_certprovisiontoken_ip_address.py | 18 - apps/bootstrap/migrations/__init__.py | 0 apps/bootstrap/models.py | 119 - apps/bootstrap/signals.py | 10 - apps/bootstrap/tasks.py | 272 - apps/bootstrap/tests/__init__.py | 0 apps/bootstrap/token_utils.py | 21 - apps/bootstrap/urls.py | 48 - apps/bootstrap/views.py | 1444 ------ apps/certificates/apps.py | 56 - apps/certificates/migrations/0001_initial.py | 78 - ...ertificateauthority_expires_at_and_more.py | 28 - ...rtificateauthority_private_key_and_more.py | 161 - .../migrations/0004_migrate_to_ecc_p256.py | 28 - .../0005_remove_cert_content_from_db.py | 37 - ...tificateauthority__private_key_and_more.py | 91 - .../0007_cert_dir_to_cert_root_sub.py | 136 - apps/certificates/migrations/__init__.py | 0 apps/certificates/models.py | 108 - apps/certificates/signals.py | 10 - apps/certificates/urls.py | 16 - apps/certificates/views.py | 492 -- apps/dashboard/__init__.py | 4 - apps/dashboard/admin.py | 1 - apps/dashboard/apps.py | 11 - apps/dashboard/context_processors.py | 43 - apps/dashboard/forms.py | 249 - apps/dashboard/forms_admin.py | 158 - apps/dashboard/forms_sitegroup.py | 199 - apps/dashboard/management/__init__.py | 0 .../dashboard/management/commands/__init__.py | 0 .../management/commands/locallock.py | 23 - .../management/commands/localunlock.py | 23 - .../commands/migrate_hostname_branding.py | 67 - apps/dashboard/middleware.py | 121 - apps/dashboard/migrations/0001_initial.py | 101 - ...systemconfig_email_suffix_list_and_more.py | 23 - ...g_icp_number_systemconfig_police_number.py | 35 - .../migrations/0004_remove_system_stats.py | 16 - .../0005_add_local_access_locked.py | 22 - .../migrations/0006_delete_useractivity.py | 16 - ...config_email_suffix_whitelist_blacklist.py | 53 - .../0008_add_qq_bot_default_config.py | 28 - .../migrations/0009_remove_qq_bot_fields.py | 25 - .../0010_migrate_qq_bot_to_plugin_config.py | 83 - ...remove_systemconfig_captcha_id_and_more.py | 62 - ...0012_systemconfig_captcha_type_and_more.py | 33 - .../0013_systemconfig_hostname_branding.py | 23 - ...14_sitegroup_sitegrouphostname_and_more.py | 148 - ...up_dashboard_s_slug_5cb7f5_idx_and_more.py | 21 - .../0016_systemconfig_smtp_from_name.py | 24 - ...move_systemconfig_smtp_use_tls_and_more.py | 46 - .../migrations/0018_sitegroupconfig.py | 226 - ...019_sitegroupconfig_icp_number_and_more.py | 57 - .../migrations/0020_systemconfig_site_icon.py | 24 - apps/dashboard/migrations/__init__.py | 0 apps/dashboard/models.py | 540 -- apps/dashboard/signals.py | 3 - apps/dashboard/templatetags/__init__.py | 0 .../dashboard/templatetags/markdown_extras.py | 48 - apps/dashboard/urls.py | 88 - apps/dashboard/urls_admin.py | 37 - apps/dashboard/views.py | 433 -- apps/dashboard/views_admin.py | 264 - apps/dashboard/views_sitegroup.py | 225 - apps/dashboard/views_sitegroup_users.py | 182 - apps/errors/__init__.py | 52 - apps/errors/urls.py | 18 - apps/errors/views.py | 56 - apps/hosts/__init__.py | 4 - apps/hosts/admin.py | 1 - apps/hosts/apps.py | 10 - apps/hosts/forms_admin.py | 330 -- apps/hosts/forms_provider.py | 255 - apps/hosts/forms_wizard.py | 372 -- apps/hosts/management/__init__.py | 0 apps/hosts/management/commands/__init__.py | 0 .../management/commands/gateway_listener.py | 249 - .../commands/generate_tunnel_token.py | 60 - apps/hosts/migrations/0001_initial.py | 58 - .../migrations/0002_alter_hostgroup_hosts.py | 18 - ...rename_password_host__password_and_more.py | 52 - ...4_alter_host_port_delete_hostpermission.py | 21 - ...05_host_connection_type_alter_host_port.py | 23 - .../migrations/0006_host_administrators.py | 25 - .../0007_add_hostgroup_created_by.py | 28 - ...008_add_providers_to_host_and_hostgroup.py | 37 - .../migrations/0009_host_tunnel_fields.py | 84 - .../migrations/0010_remove_host_host_type.py | 17 - ...auth_method_host_cert_key_path_and_more.py | 38 - .../migrations/0012_host_username_optional.py | 18 - .../0013_add_cert_provision_fields.py | 48 - ...14_host_site_group_hostgroup_site_group.py | 41 - ...t_site_group_alter_hostgroup_site_group.py | 39 - apps/hosts/migrations/__init__.py | 0 apps/hosts/models.py | 595 --- apps/hosts/tasks.py | 381 -- .../hosts/templates/hosts/hostgroup_list.html | 74 - apps/hosts/urls_admin.py | 98 - apps/hosts/urls_provider.py | 72 - apps/hosts/views_admin.py | 1152 ----- apps/hosts/views_provider.py | 639 --- apps/operations/__init__.py | 14 - apps/operations/admin.py | 1 - apps/operations/apps.py | 11 - apps/operations/forms.py | 226 - apps/operations/forms_admin.py | 323 -- apps/operations/forms_provider.py | 480 -- apps/operations/forms_wizard.py | 303 -- apps/operations/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/create_public_host_info.py | 65 - apps/operations/migrations/0001_initial.py | 164 - .../migrations/0002_publichostinfo.py | 34 - apps/operations/migrations/0003_manual_fix.py | 15 - ...ningrequest_requested_password_and_more.py | 37 - .../0005_cloudcomputeruser_owner.py | 21 - .../migrations/0006_add_product_created_by.py | 35 - .../migrations/0007_add_product_group.py | 117 - ...equest_requested_disk_capacity_and_more.py | 62 - .../migrations/0009_rdpdomainroute.py | 84 - .../0010_product_enable_host_protection.py | 23 - ..._operations__domain_5c01f5_idx_and_more.py | 38 - ...bility_productgroup_visibility_and_more.py | 111 - .../0013_productgroup_created_by.py | 21 - .../0014_encrypt_initial_password.py | 60 - ...15_alter_rdpdomainroute_domain_and_more.py | 23 - .../0016_add_product_limit_one_per_user.py | 16 - .../0017_accountopeningrequest_retry_count.py | 20 - ...duct_site_group_productgroup_site_group.py | 41 - .../0019_alter_product_site_group_and_more.py | 39 - apps/operations/migrations/__init__.py | 0 apps/operations/models.py | 1572 ------ apps/operations/services.py | 191 - apps/operations/tasks.py | 672 --- apps/operations/urls.py | 40 - apps/operations/urls_admin.py | 55 - apps/operations/urls_provider.py | 203 - apps/operations/views.py | 955 ---- apps/operations/views_admin.py | 1773 ------- apps/operations/views_provider.py | 1736 ------- apps/provider/__init__.py | 0 apps/provider/apps.py | 8 - apps/provider/context_mixin.py | 20 - apps/provider/decorators.py | 63 - apps/provider/migrations/__init__.py | 0 apps/provider/urls.py | 65 - apps/provider/views.py | 245 - apps/provider_backend/__init__.py | 0 apps/provider_backend/admin.py | 1 - apps/provider_backend/api.py | 272 - apps/provider_backend/api_urls.py | 20 - apps/provider_backend/apps.py | 7 - apps/provider_backend/decorators.py | 42 - apps/provider_backend/middleware.py | 22 - apps/provider_backend/migrations/__init__.py | 0 apps/provider_backend/models.py | 3 - apps/provider_backend/tests/__init__.py | 0 apps/provider_backend/urls.py | 69 - apps/provider_backend/views.py | 575 --- apps/tasks/migrations/0001_initial.py | 57 - apps/tasks/migrations/__init__.py | 0 apps/tasks/models.py | 100 - apps/tasks/urls.py | 11 - apps/tasks/views.py | 37 - apps/themes/__init__.py | 11 - apps/themes/admin.py | 1 - apps/themes/apps.py | 15 - apps/themes/context_processors.py | 40 - apps/themes/forms_admin.py | 93 - apps/themes/migrations/0001_initial.py | 65 - apps/themes/migrations/__init__.py | 0 apps/themes/models.py | 320 -- apps/themes/templatetags/__init__.py | 1 - apps/themes/templatetags/theme_tags.py | 198 - apps/themes/urls_admin.py | 53 - apps/themes/views_admin.py | 278 -- apps/tickets/__init__.py | 0 apps/tickets/admin.py | 1 - apps/tickets/apps.py | 13 - apps/tickets/audit_integration.py | 134 - apps/tickets/forms.py | 317 -- apps/tickets/forms_admin.py | 189 - apps/tickets/forms_provider.py | 176 - apps/tickets/migrations/0001_initial.py | 180 - .../0002_add_created_by_to_ticketcategory.py | 21 - .../migrations/0003_add_group_assignment.py | 29 - .../0004_ticketcategory_allow_banned_users.py | 22 - ...roduct_host_with_cloud_computer_request.py | 49 - apps/tickets/migrations/__init__.py | 0 apps/tickets/models.py | 651 --- apps/tickets/notifications.py | 231 - apps/tickets/signals.py | 78 - apps/tickets/urls.py | 27 - apps/tickets/urls_admin.py | 33 - apps/tickets/urls_provider.py | 87 - apps/tickets/views.py | 478 -- apps/tickets/views_admin.py | 578 --- apps/tickets/views_provider.py | 688 --- apps/tunnel/__init__.py | 0 apps/tunnel/admin.py | 1 - apps/tunnel/apps.py | 7 - apps/tunnel/migrations/__init__.py | 0 apps/tunnel/models.py | 3 - apps/tunnel/tests_dir/__init__.py | 0 apps/tunnel/urls.py | 10 - apps/tunnel/views.py | 260 - celerybeat-schedule | Bin 12288 -> 0 bytes config/__init__.py | 6 - config/celery.py | 73 - config/demo_middleware.py | 159 - config/demo_startup.py | 39 - config/local_lock_middleware.py | 58 - config/maintenance_middleware.py | 47 - config/management/commands/init_demo.py | 137 - config/security_middleware.py | 34 - config/settings.py | 589 --- config/tests.py | 45 - config/urls.py | 42 - config/views.py | 158 - config/wsgi.py | 11 - conftest.py | 46 - manage.py | 22 - plugins/README.md | 97 - plugins/STRUCTURE.md | 57 - plugins/__init__.py | 2 - plugins/admin.py | 1 - plugins/apps.py | 20 - plugins/available_plugins.py | 30 - plugins/beta_push/.gitignore | 9 - plugins/beta_push/__init__.py | 53 - plugins/beta_push/apps.py | 47 - plugins/beta_push/migrations/0001_initial.py | 39 - plugins/beta_push/migrations/__init__.py | 0 plugins/beta_push/models.py | 71 - plugins/beta_push/services.py | 554 -- plugins/beta_push/tasks.py | 55 - .../templates/beta_push/dashboard.html | 293 -- plugins/beta_push/urls.py | 11 - plugins/beta_push/views.py | 173 - plugins/core/__init__.py | 0 plugins/core/base.py | 231 - plugins/core/plugin_manager.py | 220 - plugins/django_integration.py | 205 - plugins/dynamic_urls.py | 35 - plugins/forms_admin.py | 0 plugins/forms_provider.py | 0 plugins/management/commands/plugin.py | 1482 ------ plugins/migrations/0001_initial.py | 54 - .../0002_qqverifyconfig_and_more.py | 32 - plugins/migrations/0003_auto_20260129_1808.py | 35 - plugins/migrations/0005_auto_20260130_1041.py | 14 - ...fig_alter_pluginrecord_options_and_more.py | 78 - .../0007_add_use_default_bot_and_group_ids.py | 57 - ...08_move_qqverificationconfig_to_own_app.py | 19 - plugins/migrations/__init__.py | 0 plugins/models.py | 89 - plugins/plugin_manager.py | 393 -- plugins/signals.py | 99 - plugins/templatetags/__init__.py | 0 plugins/templatetags/plugin_extensions.py | 91 - plugins/test_demo_plugin/__init__.py | 16 - plugins/tests.py | 58 - plugins/urls.py | 16 - plugins/urls_admin.py | 1 - plugins/urls_provider.py | 1 - plugins/views.py | 109 - plugins/views_admin.py | 0 plugins/views_provider.py | 0 pyproject.toml | 116 +- scripts/deploy.py | 1553 ------ scripts/tailwind-build.sh | 83 - static/admin/css/bootstrap-deploy-button.css | 871 ---- static/admin/css/bootstrap_admin.css | 394 -- static/admin/js/bootstrap-deploy-button.js | 656 --- static/admin/js/bootstrap_admin.js | 323 -- static/css/accounts.css | 702 --- static/css/base.css | 947 ---- static/css/dashboard.css | 497 -- static/css/operations.css | 859 ---- static/css/profile.css | 449 -- static/css/provider.css | 4442 ----------------- static/css/theme.css | 467 -- static/img/bgimg.jpg | Bin 409827 -> 0 bytes static/img/default.png | Bin 1630076 -> 0 bytes static/img/favicon.svg | 34 - static/img/head-logo.png | Bin 1113 -> 0 bytes static/js/accounts.js | 541 -- static/js/auth-animation.js | 157 - static/js/base.js | 275 - static/js/dashboard.js | 250 - static/js/email_code.js | 79 - static/js/operations.js | 538 -- static/js/tianai_adapter.js | 214 - static/scripts/init.ps1 | 222 - static/src/tailwind.css | 81 - static/vendor/alpine/alpine.min.js | 5 - .../bootstrap-5.1.3/css/bootstrap.min.css | 7 - .../js/bootstrap.bundle.min.js | 7 - .../bootstrap-icons/bootstrap-icons.min.css | 5 - .../bootstrap-icons/bootstrap-icons.woff | Bin 164360 -> 0 bytes .../bootstrap-icons/bootstrap-icons.woff2 | Bin 121340 -> 0 bytes .../bootstrap/css/bootstrap.5.3.0.min.css | 6 - static/vendor/bootstrap/css/bootstrap.min.css | 7 - .../bootstrap/js/bootstrap.bundle.min.js | 7 - static/vendor/chart.js-3.7.0/chart.min.js | 13 - static/vendor/chart.js/chart.min.css | 1 - static/vendor/chart.js/chart.min.js | 13 - static/vendor/fonts/material-icons.woff2 | Bin 128616 -> 0 bytes static/vendor/jquery-3.6.0/jquery.min.js | 2 - static/vendor/jquery/jquery.min.js | 2 - .../vendor/material-icons/material-icons.css | 20 - .../vendor/material-icons/material-icons.ttf | Bin 356840 -> 0 bytes .../material-symbols-rounded.css | 22 - .../material-symbols-rounded.woff2 | Bin 450340 -> 0 bytes templates/accounts/banned.html | 112 - templates/accounts/forgot_password.html | 235 - templates/accounts/login.html | 234 - templates/accounts/migrate.html | 196 - templates/accounts/profile.html | 513 -- templates/accounts/register.html | 253 - templates/admin/base.html | 127 - templates/admin/base_site.html | 14 - .../admin/hosts/host/manage_permissions.html | 106 - .../admin_base/audit/auditlog_detail.html | 88 - templates/admin_base/audit/auditlog_list.html | 158 - templates/admin_base/base.html | 447 -- templates/admin_base/dashboard.html | 177 - .../dashboard/systemconfig_edit.html | 315 -- .../dashboard/test_email_progress.html | 171 - .../dashboard/widget_confirm_delete.html | 48 - .../admin_base/dashboard/widget_form.html | 160 - .../admin_base/dashboard/widget_list.html | 90 - .../groups/group_confirm_delete.html | 54 - templates/admin_base/groups/group_form.html | 124 - templates/admin_base/groups/group_list.html | 125 - .../admin_base/hosts/host_confirm_delete.html | 82 - templates/admin_base/hosts/host_detail.html | 497 -- templates/admin_base/hosts/host_form.html | 738 --- templates/admin_base/hosts/host_list.html | 176 - templates/admin_base/hosts/host_wizard.html | 1100 ---- .../hosts/hostgroup_confirm_delete.html | 66 - .../admin_base/hosts/hostgroup_form.html | 137 - .../admin_base/hosts/hostgroup_list.html | 102 - .../admin_base/operations/grant_list.html | 131 - .../operations/product_confirm_delete.html | 73 - .../admin_base/operations/product_form.html | 493 -- .../admin_base/operations/product_list.html | 370 -- .../admin_base/operations/product_wizard.html | 741 --- .../productgroup_confirm_delete.html | 69 - .../operations/productgroup_form.html | 126 - .../operations/productgroup_list.html | 104 - .../admin_base/operations/request_detail.html | 334 -- .../admin_base/operations/request_list.html | 200 - .../admin_base/operations/route_list.html | 103 - .../admin_base/operations/task_list.html | 157 - .../admin_base/operations/token_detail.html | 229 - .../admin_base/operations/token_list.html | 149 - .../admin_base/operations/user_detail.html | 500 -- .../admin_base/operations/user_list.html | 139 - .../admin_base/provider/coming_soon.html | 29 - templates/admin_base/provider/dashboard.html | 108 - templates/admin_base/providers/host_list.html | 124 - .../providers/host_provider_assign.html | 226 - .../admin_base/providers/hostgroup_list.html | 93 - .../providers/hostgroup_provider_assign.html | 117 - .../reglinks/reglink_confirm_delete.html | 71 - .../admin_base/reglinks/reglink_form.html | 121 - .../admin_base/reglinks/reglink_list.html | 208 - .../themes/pagecontent_confirm_delete.html | 46 - .../admin_base/themes/pagecontent_form.html | 131 - .../admin_base/themes/pagecontent_list.html | 93 - .../admin_base/themes/themeconfig_edit.html | 167 - .../themes/widgetlayout_confirm_delete.html | 48 - .../admin_base/themes/widgetlayout_form.html | 115 - .../admin_base/themes/widgetlayout_list.html | 96 - .../admin_base/tickets/activity_list.html | 129 - .../tickets/category_confirm_delete.html | 76 - .../admin_base/tickets/category_form.html | 247 - .../admin_base/tickets/category_list.html | 136 - .../admin_base/tickets/ticket_detail.html | 267 - templates/admin_base/tickets/ticket_list.html | 213 - .../admin_base/users/user_confirm_delete.html | 72 - templates/admin_base/users/user_form.html | 116 - templates/admin_base/users/user_list.html | 216 - .../admin_base/users/user_reset_password.html | 82 - templates/base.html | 389 -- templates/components/alert.html | 67 - templates/components/button.html | 113 - templates/cotton/x_admin_alert.html | 85 - templates/cotton/x_admin_badge.html | 30 - templates/cotton/x_admin_button.html | 36 - templates/cotton/x_admin_card.html | 22 - templates/cotton/x_admin_empty.html | 12 - templates/cotton/x_admin_input.html | 46 - templates/cotton/x_admin_modal.html | 46 - templates/cotton/x_admin_pagination.html | 57 - templates/cotton/x_admin_table.html | 19 - templates/cotton/x_md_alert.html | 105 - templates/dashboard/base.html | 33 - templates/dashboard/index.html | 164 - templates/dashboard/sitegroup_config.html | 189 - templates/dashboard/sitegroup_detail.html | 189 - templates/dashboard/sitegroup_form.html | 81 - templates/dashboard/sitegroup_list.html | 89 - templates/dashboard/sitegroup_user_list.html | 203 - .../dashboard/sitegroup_user_remove.html | 43 - .../sitegroup_user_reset_password.html | 61 - templates/dashboard/system_config.html | 199 - templates/dashboard/widget_config.html | 105 - templates/docs/index.html | 365 -- templates/errors/400.html | 51 - templates/errors/403.html | 47 - templates/errors/404.html | 58 - templates/errors/500.html | 57 - templates/maintenance.html | 79 - .../operations/account_opening_confirm.html | 85 - .../account_opening_request_detail.html | 216 - .../account_opening_request_form.html | 240 - .../account_opening_request_list.html | 141 - .../operations/cloud_computer_user_list.html | 169 - templates/operations/invite_result.html | 87 - .../operations/my_cloud_computer_detail.html | 324 -- templates/operations/my_cloud_computers.html | 153 - templates/operations/systemtask_detail.html | 88 - templates/operations/systemtask_list.html | 115 - templates/tickets/dashboard.html | 157 - templates/tickets/email/assigned.html | 38 - templates/tickets/email/closed.html | 34 - templates/tickets/email/new_comment.html | 35 - templates/tickets/email/overdue.html | 36 - templates/tickets/email/status_update.html | 35 - templates/tickets/my_tickets.html | 119 - templates/tickets/pending_list.html | 99 - templates/tickets/ticket_detail.html | 329 -- templates/tickets/ticket_form.html | 114 - templates/tickets/ticket_list.html | 147 - tickets/__init__.py | 0 tickets/admin.py | 1 - tickets/apps.py | 6 - tickets/migrations/__init__.py | 0 tickets/models.py | 3 - tickets/tests/__init__.py | 0 tickets/views.py | 3 - utils/__init__.py | 3 - utils/ca_bundle.py | 454 -- utils/cert_service.py | 272 - utils/cert_storage.py | 139 - utils/crypto.py | 28 - utils/disk_quota.py | 336 -- utils/error_handlers.py | 158 - utils/gateway_client.py | 150 - utils/helpers.py | 424 -- utils/local_winserver_client.py | 611 --- utils/production_checker.py | 93 - utils/provider.py | 124 - utils/rate_limit.py | 206 - utils/redis_helper.py | 97 - utils/sensitive_log_filters.py | 71 - utils/site_group.py | 154 - utils/winrm_client.py | 712 --- uv.lock | 1580 ------ uv.toml | 25 - 622 files changed, 8633 insertions(+), 83211 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/208e77e631c4_initial_schema.py create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/deps.py create mode 100644 app/api/v1/__init__.py create mode 100644 app/api/v1/audit.py create mode 100644 app/api/v1/hosts.py create mode 100644 app/api/v1/operations.py create mode 100644 app/api/v1/tickets.py create mode 100644 app/auth/__init__.py create mode 100644 app/auth/dependencies.py create mode 100644 app/auth/routes.py create mode 100644 app/auth/schemas.py create mode 100644 app/cache/__init__.py create mode 100644 app/cache/app_shell.py create mode 100644 app/cache/fragments.py create mode 100644 app/cache/keys.py create mode 100644 app/core/__init__.py create mode 100644 app/core/config.py create mode 100644 app/core/db.py create mode 100644 app/core/exceptions.py create mode 100644 app/core/logging.py create mode 100644 app/core/redis.py create mode 100644 app/main.py create mode 100644 app/models/__init__.py create mode 100644 app/models/audit.py create mode 100644 app/models/base.py create mode 100644 app/models/bootstrap.py create mode 100644 app/models/certificate.py create mode 100644 app/models/host.py create mode 100644 app/models/operations.py create mode 100644 app/models/plugin.py create mode 100644 app/models/task.py create mode 100644 app/models/tenant.py create mode 100644 app/models/theme.py create mode 100644 app/models/ticket.py create mode 100644 app/models/user.py create mode 100644 app/plugins/__init__.py create mode 100644 app/plugins/base.py create mode 100644 app/plugins/example/__init__.py create mode 100644 app/plugins/example/plugin.py create mode 100644 app/plugins/loader.py create mode 100644 app/plugins/manager.py create mode 100644 app/plugins/registry.py create mode 100644 app/security/__init__.py create mode 100644 app/security/ban_version.py create mode 100644 app/security/crypto.py create mode 100644 app/security/field_cipher.py create mode 100644 app/security/jwt_auth.py create mode 100644 app/security/password.py create mode 100644 app/security/refresh_token.py create mode 100644 app/static/css/base.css create mode 100644 app/static/js/auth.js create mode 100644 app/static/vendor/htmx.min.js create mode 100644 app/tasks/__init__.py create mode 100644 app/tasks/hosts.py create mode 100644 app/tasks/huey_app.py create mode 100644 app/tasks/operations.py create mode 100644 app/templates/__init__.py create mode 100644 app/templates/fragments/my_cloud_computers.html create mode 100644 app/templates/fragments/nav.html create mode 100644 app/templates/fragments/product_groups.html create mode 100644 app/templates/fragments/stats.html create mode 100644 app/templates/layouts/app_shell.html create mode 100644 app/templates/pages/dashboard.html create mode 100644 app/templates/pages/login.html create mode 100644 app/templates/pages/register.html create mode 100644 app/tenant/__init__.py create mode 100644 app/tenant/dependencies.py create mode 100644 app/tenant/middleware.py create mode 100644 app/tenant/resolver.py create mode 100644 app/web/__init__.py create mode 100644 app/web/fragments.py create mode 100644 app/web/shell.py create mode 100644 app/winrm/__init__.py create mode 100644 app/winrm/client.py create mode 100644 app/winrm/commands.py create mode 100644 app/winrm/transport.py delete mode 100755 apps/__init__.py delete mode 100755 apps/accounts/__init__.py delete mode 100755 apps/accounts/admin.py delete mode 100755 apps/accounts/apps.py delete mode 100644 apps/accounts/captcha_service.py delete mode 100644 apps/accounts/email_service.py delete mode 100755 apps/accounts/forms.py delete mode 100644 apps/accounts/forms_admin.py delete mode 100644 apps/accounts/forms_superadmin.py delete mode 100755 apps/accounts/management/commands/create_demo_superuser.py delete mode 100755 apps/accounts/management/commands/setup_demo_users.py delete mode 100644 apps/accounts/management/commands/setup_provider_group.py delete mode 100755 apps/accounts/migrations/0001_initial.py delete mode 100644 apps/accounts/migrations/0002_groupprofile_model.py delete mode 100644 apps/accounts/migrations/0003_default_groups.py delete mode 100644 apps/accounts/migrations/0004_normal_user_group.py delete mode 100644 apps/accounts/migrations/0005_auto_staff.py delete mode 100644 apps/accounts/migrations/0006_registrationlink.py delete mode 100644 apps/accounts/migrations/0007_add_registrationlink_max_uses.py delete mode 100644 apps/accounts/migrations/0008_user_site_groups.py delete mode 100644 apps/accounts/migrations/0009_useremail.py delete mode 100644 apps/accounts/migrations/0010_userbanhistory_userban.py delete mode 100755 apps/accounts/migrations/__init__.py delete mode 100755 apps/accounts/models.py delete mode 100644 apps/accounts/provider_decorators.py delete mode 100755 apps/accounts/rate_limit.py delete mode 100755 apps/accounts/signals.py delete mode 100644 apps/accounts/tasks.py delete mode 100755 apps/accounts/urls.py delete mode 100644 apps/accounts/urls_admin.py delete mode 100644 apps/accounts/urls_admin_groups.py delete mode 100644 apps/accounts/urls_admin_providers.py delete mode 100644 apps/accounts/urls_admin_reglinks.py delete mode 100644 apps/accounts/urls_admin_users.py delete mode 100644 apps/accounts/urls_provider.py delete mode 100644 apps/accounts/user_service.py delete mode 100755 apps/accounts/views.py delete mode 100644 apps/accounts/views_admin.py delete mode 100644 apps/accounts/views_admin_groups.py delete mode 100644 apps/accounts/views_admin_reglinks.py delete mode 100644 apps/accounts/views_admin_users.py delete mode 100644 apps/accounts/views_provider.py delete mode 100644 apps/accounts/views_superadmin.py delete mode 100644 apps/audit/admin.py delete mode 100755 apps/audit/apps.py delete mode 100755 apps/audit/decorators.py delete mode 100644 apps/audit/forms_admin.py delete mode 100755 apps/audit/migrations/0001_initial.py delete mode 100644 apps/audit/migrations/0002_auditlog_tunnel_actions.py delete mode 100644 apps/audit/migrations/0003_alter_auditlog_action.py delete mode 100644 apps/audit/migrations/0004_auditlog_user_agent_alter_auditlog_action.py delete mode 100755 apps/audit/migrations/__init__.py delete mode 100755 apps/audit/models.py delete mode 100755 apps/audit/signals.py delete mode 100644 apps/audit/tests/__init__.py delete mode 100755 apps/audit/urls.py delete mode 100644 apps/audit/urls_admin.py delete mode 100755 apps/audit/views.py delete mode 100644 apps/audit/views_admin.py delete mode 100644 apps/bootstrap/README.md delete mode 100644 apps/bootstrap/admin.py delete mode 100755 apps/bootstrap/apps.py delete mode 100644 apps/bootstrap/management/commands/cleanup_expired_sessions.py delete mode 100644 apps/bootstrap/middleware.py delete mode 100755 apps/bootstrap/migrations/0001_initial.py delete mode 100644 apps/bootstrap/migrations/0002_bootstraptoken_is_paired_bootstraptoken_paired_at_and_more.py delete mode 100644 apps/bootstrap/migrations/0003_bootstraptoken_totp_secret.py delete mode 100644 apps/bootstrap/migrations/0004_remove_bootstraptoken_pairing_code_and_more.py delete mode 100644 apps/bootstrap/migrations/0005_delete_bootstraptoken.py delete mode 100644 apps/bootstrap/migrations/0006_alter_activesession_expires_at.py delete mode 100644 apps/bootstrap/migrations/0007_update_initialtoken_for_pairing.py delete mode 100644 apps/bootstrap/migrations/0008_add_pairing_attempts.py delete mode 100644 apps/bootstrap/migrations/0009_initialtoken_host_nullable.py delete mode 100644 apps/bootstrap/migrations/0010_add_cert_data.py delete mode 100644 apps/bootstrap/migrations/0011_add_cert_provision_token.py delete mode 100644 apps/bootstrap/migrations/0012_certprovisiontoken_cert_data_and_more.py delete mode 100644 apps/bootstrap/migrations/0013_certprovisiontoken_ip_address.py delete mode 100755 apps/bootstrap/migrations/__init__.py delete mode 100644 apps/bootstrap/models.py delete mode 100644 apps/bootstrap/signals.py delete mode 100644 apps/bootstrap/tasks.py delete mode 100644 apps/bootstrap/tests/__init__.py delete mode 100644 apps/bootstrap/token_utils.py delete mode 100644 apps/bootstrap/urls.py delete mode 100644 apps/bootstrap/views.py delete mode 100755 apps/certificates/apps.py delete mode 100755 apps/certificates/migrations/0001_initial.py delete mode 100755 apps/certificates/migrations/0002_alter_certificateauthority_expires_at_and_more.py delete mode 100644 apps/certificates/migrations/0003_remove_certificateauthority_private_key_and_more.py delete mode 100644 apps/certificates/migrations/0004_migrate_to_ecc_p256.py delete mode 100644 apps/certificates/migrations/0005_remove_cert_content_from_db.py delete mode 100644 apps/certificates/migrations/0006_remove_certificateauthority__private_key_and_more.py delete mode 100644 apps/certificates/migrations/0007_cert_dir_to_cert_root_sub.py delete mode 100755 apps/certificates/migrations/__init__.py delete mode 100755 apps/certificates/models.py delete mode 100755 apps/certificates/signals.py delete mode 100755 apps/certificates/urls.py delete mode 100755 apps/certificates/views.py delete mode 100755 apps/dashboard/__init__.py delete mode 100755 apps/dashboard/admin.py delete mode 100755 apps/dashboard/apps.py delete mode 100644 apps/dashboard/context_processors.py delete mode 100755 apps/dashboard/forms.py delete mode 100644 apps/dashboard/forms_admin.py delete mode 100644 apps/dashboard/forms_sitegroup.py delete mode 100644 apps/dashboard/management/__init__.py delete mode 100644 apps/dashboard/management/commands/__init__.py delete mode 100644 apps/dashboard/management/commands/locallock.py delete mode 100644 apps/dashboard/management/commands/localunlock.py delete mode 100644 apps/dashboard/management/commands/migrate_hostname_branding.py delete mode 100644 apps/dashboard/middleware.py delete mode 100755 apps/dashboard/migrations/0001_initial.py delete mode 100755 apps/dashboard/migrations/0002_systemconfig_email_suffix_list_and_more.py delete mode 100644 apps/dashboard/migrations/0003_systemconfig_icp_number_systemconfig_police_number.py delete mode 100644 apps/dashboard/migrations/0004_remove_system_stats.py delete mode 100644 apps/dashboard/migrations/0005_add_local_access_locked.py delete mode 100644 apps/dashboard/migrations/0006_delete_useractivity.py delete mode 100644 apps/dashboard/migrations/0007_systemconfig_email_suffix_whitelist_blacklist.py delete mode 100644 apps/dashboard/migrations/0008_add_qq_bot_default_config.py delete mode 100644 apps/dashboard/migrations/0009_remove_qq_bot_fields.py delete mode 100644 apps/dashboard/migrations/0010_migrate_qq_bot_to_plugin_config.py delete mode 100644 apps/dashboard/migrations/0011_remove_systemconfig_captcha_id_and_more.py delete mode 100644 apps/dashboard/migrations/0012_systemconfig_captcha_type_and_more.py delete mode 100644 apps/dashboard/migrations/0013_systemconfig_hostname_branding.py delete mode 100644 apps/dashboard/migrations/0014_sitegroup_sitegrouphostname_and_more.py delete mode 100644 apps/dashboard/migrations/0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more.py delete mode 100644 apps/dashboard/migrations/0016_systemconfig_smtp_from_name.py delete mode 100644 apps/dashboard/migrations/0017_remove_systemconfig_smtp_use_tls_and_more.py delete mode 100644 apps/dashboard/migrations/0018_sitegroupconfig.py delete mode 100644 apps/dashboard/migrations/0019_sitegroupconfig_icp_number_and_more.py delete mode 100644 apps/dashboard/migrations/0020_systemconfig_site_icon.py delete mode 100755 apps/dashboard/migrations/__init__.py delete mode 100755 apps/dashboard/models.py delete mode 100644 apps/dashboard/signals.py delete mode 100755 apps/dashboard/templatetags/__init__.py delete mode 100755 apps/dashboard/templatetags/markdown_extras.py delete mode 100755 apps/dashboard/urls.py delete mode 100644 apps/dashboard/urls_admin.py delete mode 100755 apps/dashboard/views.py delete mode 100644 apps/dashboard/views_admin.py delete mode 100644 apps/dashboard/views_sitegroup.py delete mode 100644 apps/dashboard/views_sitegroup_users.py delete mode 100755 apps/errors/__init__.py delete mode 100755 apps/errors/urls.py delete mode 100755 apps/errors/views.py delete mode 100755 apps/hosts/__init__.py delete mode 100644 apps/hosts/admin.py delete mode 100755 apps/hosts/apps.py delete mode 100644 apps/hosts/forms_admin.py delete mode 100644 apps/hosts/forms_provider.py delete mode 100644 apps/hosts/forms_wizard.py delete mode 100755 apps/hosts/management/__init__.py delete mode 100755 apps/hosts/management/commands/__init__.py delete mode 100644 apps/hosts/management/commands/gateway_listener.py delete mode 100644 apps/hosts/management/commands/generate_tunnel_token.py delete mode 100755 apps/hosts/migrations/0001_initial.py delete mode 100755 apps/hosts/migrations/0002_alter_hostgroup_hosts.py delete mode 100755 apps/hosts/migrations/0003_alter_host_password_rename_password_host__password_and_more.py delete mode 100755 apps/hosts/migrations/0004_alter_host_port_delete_hostpermission.py delete mode 100755 apps/hosts/migrations/0005_host_connection_type_alter_host_port.py delete mode 100644 apps/hosts/migrations/0006_host_administrators.py delete mode 100644 apps/hosts/migrations/0007_add_hostgroup_created_by.py delete mode 100644 apps/hosts/migrations/0008_add_providers_to_host_and_hostgroup.py delete mode 100644 apps/hosts/migrations/0009_host_tunnel_fields.py delete mode 100644 apps/hosts/migrations/0010_remove_host_host_type.py delete mode 100644 apps/hosts/migrations/0011_host_auth_method_host_cert_key_path_and_more.py delete mode 100644 apps/hosts/migrations/0012_host_username_optional.py delete mode 100644 apps/hosts/migrations/0013_add_cert_provision_fields.py delete mode 100644 apps/hosts/migrations/0014_host_site_group_hostgroup_site_group.py delete mode 100644 apps/hosts/migrations/0015_alter_host_site_group_alter_hostgroup_site_group.py delete mode 100755 apps/hosts/migrations/__init__.py delete mode 100755 apps/hosts/models.py delete mode 100755 apps/hosts/tasks.py delete mode 100755 apps/hosts/templates/hosts/hostgroup_list.html delete mode 100644 apps/hosts/urls_admin.py delete mode 100644 apps/hosts/urls_provider.py delete mode 100644 apps/hosts/views_admin.py delete mode 100644 apps/hosts/views_provider.py delete mode 100755 apps/operations/__init__.py delete mode 100755 apps/operations/admin.py delete mode 100755 apps/operations/apps.py delete mode 100755 apps/operations/forms.py delete mode 100644 apps/operations/forms_admin.py delete mode 100644 apps/operations/forms_provider.py delete mode 100644 apps/operations/forms_wizard.py delete mode 100755 apps/operations/management/__init__.py delete mode 100755 apps/operations/management/commands/__init__.py delete mode 100755 apps/operations/management/commands/create_public_host_info.py delete mode 100755 apps/operations/migrations/0001_initial.py delete mode 100755 apps/operations/migrations/0002_publichostinfo.py delete mode 100755 apps/operations/migrations/0003_manual_fix.py delete mode 100755 apps/operations/migrations/0004_remove_accountopeningrequest_requested_password_and_more.py delete mode 100644 apps/operations/migrations/0005_cloudcomputeruser_owner.py delete mode 100644 apps/operations/migrations/0006_add_product_created_by.py delete mode 100644 apps/operations/migrations/0007_add_product_group.py delete mode 100644 apps/operations/migrations/0008_accountopeningrequest_requested_disk_capacity_and_more.py delete mode 100644 apps/operations/migrations/0009_rdpdomainroute.py delete mode 100644 apps/operations/migrations/0010_product_enable_host_protection.py delete mode 100644 apps/operations/migrations/0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more.py delete mode 100644 apps/operations/migrations/0012_product_visibility_productgroup_visibility_and_more.py delete mode 100644 apps/operations/migrations/0013_productgroup_created_by.py delete mode 100644 apps/operations/migrations/0014_encrypt_initial_password.py delete mode 100644 apps/operations/migrations/0015_alter_rdpdomainroute_domain_and_more.py delete mode 100644 apps/operations/migrations/0016_add_product_limit_one_per_user.py delete mode 100644 apps/operations/migrations/0017_accountopeningrequest_retry_count.py delete mode 100644 apps/operations/migrations/0018_product_site_group_productgroup_site_group.py delete mode 100644 apps/operations/migrations/0019_alter_product_site_group_and_more.py delete mode 100755 apps/operations/migrations/__init__.py delete mode 100755 apps/operations/models.py delete mode 100644 apps/operations/services.py delete mode 100755 apps/operations/tasks.py delete mode 100755 apps/operations/urls.py delete mode 100644 apps/operations/urls_admin.py delete mode 100644 apps/operations/urls_provider.py delete mode 100755 apps/operations/views.py delete mode 100644 apps/operations/views_admin.py delete mode 100644 apps/operations/views_provider.py delete mode 100644 apps/provider/__init__.py delete mode 100644 apps/provider/apps.py delete mode 100644 apps/provider/context_mixin.py delete mode 100644 apps/provider/decorators.py delete mode 100644 apps/provider/migrations/__init__.py delete mode 100644 apps/provider/urls.py delete mode 100644 apps/provider/views.py delete mode 100644 apps/provider_backend/__init__.py delete mode 100644 apps/provider_backend/admin.py delete mode 100644 apps/provider_backend/api.py delete mode 100644 apps/provider_backend/api_urls.py delete mode 100644 apps/provider_backend/apps.py delete mode 100644 apps/provider_backend/decorators.py delete mode 100644 apps/provider_backend/middleware.py delete mode 100644 apps/provider_backend/migrations/__init__.py delete mode 100644 apps/provider_backend/models.py delete mode 100644 apps/provider_backend/tests/__init__.py delete mode 100644 apps/provider_backend/urls.py delete mode 100644 apps/provider_backend/views.py delete mode 100755 apps/tasks/migrations/0001_initial.py delete mode 100755 apps/tasks/migrations/__init__.py delete mode 100755 apps/tasks/models.py delete mode 100644 apps/tasks/urls.py delete mode 100644 apps/tasks/views.py delete mode 100755 apps/themes/__init__.py delete mode 100755 apps/themes/admin.py delete mode 100755 apps/themes/apps.py delete mode 100755 apps/themes/context_processors.py delete mode 100644 apps/themes/forms_admin.py delete mode 100644 apps/themes/migrations/0001_initial.py delete mode 100644 apps/themes/migrations/__init__.py delete mode 100755 apps/themes/models.py delete mode 100755 apps/themes/templatetags/__init__.py delete mode 100755 apps/themes/templatetags/theme_tags.py delete mode 100644 apps/themes/urls_admin.py delete mode 100644 apps/themes/views_admin.py delete mode 100644 apps/tickets/__init__.py delete mode 100644 apps/tickets/admin.py delete mode 100644 apps/tickets/apps.py delete mode 100644 apps/tickets/audit_integration.py delete mode 100644 apps/tickets/forms.py delete mode 100644 apps/tickets/forms_admin.py delete mode 100644 apps/tickets/forms_provider.py delete mode 100644 apps/tickets/migrations/0001_initial.py delete mode 100644 apps/tickets/migrations/0002_add_created_by_to_ticketcategory.py delete mode 100644 apps/tickets/migrations/0003_add_group_assignment.py delete mode 100644 apps/tickets/migrations/0004_ticketcategory_allow_banned_users.py delete mode 100644 apps/tickets/migrations/0005_replace_related_product_host_with_cloud_computer_request.py delete mode 100644 apps/tickets/migrations/__init__.py delete mode 100644 apps/tickets/models.py delete mode 100644 apps/tickets/notifications.py delete mode 100644 apps/tickets/signals.py delete mode 100644 apps/tickets/urls.py delete mode 100644 apps/tickets/urls_admin.py delete mode 100644 apps/tickets/urls_provider.py delete mode 100644 apps/tickets/views.py delete mode 100644 apps/tickets/views_admin.py delete mode 100644 apps/tickets/views_provider.py delete mode 100644 apps/tunnel/__init__.py delete mode 100644 apps/tunnel/admin.py delete mode 100644 apps/tunnel/apps.py delete mode 100644 apps/tunnel/migrations/__init__.py delete mode 100644 apps/tunnel/models.py delete mode 100644 apps/tunnel/tests_dir/__init__.py delete mode 100644 apps/tunnel/urls.py delete mode 100644 apps/tunnel/views.py delete mode 100644 celerybeat-schedule delete mode 100755 config/__init__.py delete mode 100755 config/celery.py delete mode 100755 config/demo_middleware.py delete mode 100755 config/demo_startup.py delete mode 100644 config/local_lock_middleware.py delete mode 100755 config/maintenance_middleware.py delete mode 100755 config/management/commands/init_demo.py delete mode 100644 config/security_middleware.py delete mode 100644 config/settings.py delete mode 100644 config/tests.py delete mode 100755 config/urls.py delete mode 100755 config/views.py delete mode 100755 config/wsgi.py delete mode 100644 conftest.py delete mode 100755 manage.py delete mode 100755 plugins/README.md delete mode 100755 plugins/STRUCTURE.md delete mode 100755 plugins/__init__.py delete mode 100755 plugins/admin.py delete mode 100755 plugins/apps.py delete mode 100755 plugins/available_plugins.py delete mode 100644 plugins/beta_push/.gitignore delete mode 100644 plugins/beta_push/__init__.py delete mode 100644 plugins/beta_push/apps.py delete mode 100644 plugins/beta_push/migrations/0001_initial.py delete mode 100644 plugins/beta_push/migrations/__init__.py delete mode 100644 plugins/beta_push/models.py delete mode 100644 plugins/beta_push/services.py delete mode 100644 plugins/beta_push/tasks.py delete mode 100644 plugins/beta_push/templates/beta_push/dashboard.html delete mode 100644 plugins/beta_push/urls.py delete mode 100644 plugins/beta_push/views.py delete mode 100755 plugins/core/__init__.py delete mode 100755 plugins/core/base.py delete mode 100755 plugins/core/plugin_manager.py delete mode 100755 plugins/django_integration.py delete mode 100644 plugins/dynamic_urls.py delete mode 100644 plugins/forms_admin.py delete mode 100644 plugins/forms_provider.py delete mode 100755 plugins/management/commands/plugin.py delete mode 100755 plugins/migrations/0001_initial.py delete mode 100755 plugins/migrations/0002_qqverifyconfig_and_more.py delete mode 100755 plugins/migrations/0003_auto_20260129_1808.py delete mode 100755 plugins/migrations/0005_auto_20260130_1041.py delete mode 100644 plugins/migrations/0006_delete_qqverifyconfig_alter_pluginrecord_options_and_more.py delete mode 100644 plugins/migrations/0007_add_use_default_bot_and_group_ids.py delete mode 100644 plugins/migrations/0008_move_qqverificationconfig_to_own_app.py delete mode 100755 plugins/migrations/__init__.py delete mode 100755 plugins/models.py delete mode 100755 plugins/plugin_manager.py delete mode 100755 plugins/signals.py delete mode 100644 plugins/templatetags/__init__.py delete mode 100644 plugins/templatetags/plugin_extensions.py delete mode 100644 plugins/test_demo_plugin/__init__.py delete mode 100644 plugins/tests.py delete mode 100755 plugins/urls.py delete mode 100644 plugins/urls_admin.py delete mode 100644 plugins/urls_provider.py delete mode 100755 plugins/views.py delete mode 100644 plugins/views_admin.py delete mode 100644 plugins/views_provider.py delete mode 100755 scripts/deploy.py delete mode 100755 scripts/tailwind-build.sh delete mode 100755 static/admin/css/bootstrap-deploy-button.css delete mode 100644 static/admin/css/bootstrap_admin.css delete mode 100644 static/admin/js/bootstrap-deploy-button.js delete mode 100644 static/admin/js/bootstrap_admin.js delete mode 100755 static/css/accounts.css delete mode 100755 static/css/base.css delete mode 100755 static/css/dashboard.css delete mode 100755 static/css/operations.css delete mode 100755 static/css/profile.css delete mode 100644 static/css/provider.css delete mode 100644 static/css/theme.css delete mode 100644 static/img/bgimg.jpg delete mode 100755 static/img/default.png delete mode 100644 static/img/favicon.svg delete mode 100644 static/img/head-logo.png delete mode 100755 static/js/accounts.js delete mode 100755 static/js/auth-animation.js delete mode 100755 static/js/base.js delete mode 100755 static/js/dashboard.js delete mode 100755 static/js/email_code.js delete mode 100755 static/js/operations.js delete mode 100644 static/js/tianai_adapter.js delete mode 100644 static/scripts/init.ps1 delete mode 100644 static/src/tailwind.css delete mode 100644 static/vendor/alpine/alpine.min.js delete mode 100644 static/vendor/bootstrap-5.1.3/css/bootstrap.min.css delete mode 100644 static/vendor/bootstrap-5.1.3/js/bootstrap.bundle.min.js delete mode 100644 static/vendor/bootstrap-icons/bootstrap-icons.min.css delete mode 100644 static/vendor/bootstrap-icons/bootstrap-icons.woff delete mode 100644 static/vendor/bootstrap-icons/bootstrap-icons.woff2 delete mode 100644 static/vendor/bootstrap/css/bootstrap.5.3.0.min.css delete mode 100644 static/vendor/bootstrap/css/bootstrap.min.css delete mode 100644 static/vendor/bootstrap/js/bootstrap.bundle.min.js delete mode 100644 static/vendor/chart.js-3.7.0/chart.min.js delete mode 100644 static/vendor/chart.js/chart.min.css delete mode 100644 static/vendor/chart.js/chart.min.js delete mode 100644 static/vendor/fonts/material-icons.woff2 delete mode 100644 static/vendor/jquery-3.6.0/jquery.min.js delete mode 100644 static/vendor/jquery/jquery.min.js delete mode 100644 static/vendor/material-icons/material-icons.css delete mode 100644 static/vendor/material-icons/material-icons.ttf delete mode 100644 static/vendor/material-symbols-rounded/material-symbols-rounded.css delete mode 100644 static/vendor/material-symbols-rounded/material-symbols-rounded.woff2 delete mode 100644 templates/accounts/banned.html delete mode 100755 templates/accounts/forgot_password.html delete mode 100644 templates/accounts/login.html delete mode 100644 templates/accounts/migrate.html delete mode 100755 templates/accounts/profile.html delete mode 100644 templates/accounts/register.html delete mode 100755 templates/admin/base.html delete mode 100755 templates/admin/base_site.html delete mode 100644 templates/admin/hosts/host/manage_permissions.html delete mode 100644 templates/admin_base/audit/auditlog_detail.html delete mode 100644 templates/admin_base/audit/auditlog_list.html delete mode 100644 templates/admin_base/base.html delete mode 100644 templates/admin_base/dashboard.html delete mode 100644 templates/admin_base/dashboard/systemconfig_edit.html delete mode 100644 templates/admin_base/dashboard/test_email_progress.html delete mode 100644 templates/admin_base/dashboard/widget_confirm_delete.html delete mode 100644 templates/admin_base/dashboard/widget_form.html delete mode 100644 templates/admin_base/dashboard/widget_list.html delete mode 100644 templates/admin_base/groups/group_confirm_delete.html delete mode 100644 templates/admin_base/groups/group_form.html delete mode 100644 templates/admin_base/groups/group_list.html delete mode 100644 templates/admin_base/hosts/host_confirm_delete.html delete mode 100644 templates/admin_base/hosts/host_detail.html delete mode 100644 templates/admin_base/hosts/host_form.html delete mode 100644 templates/admin_base/hosts/host_list.html delete mode 100644 templates/admin_base/hosts/host_wizard.html delete mode 100644 templates/admin_base/hosts/hostgroup_confirm_delete.html delete mode 100644 templates/admin_base/hosts/hostgroup_form.html delete mode 100644 templates/admin_base/hosts/hostgroup_list.html delete mode 100644 templates/admin_base/operations/grant_list.html delete mode 100644 templates/admin_base/operations/product_confirm_delete.html delete mode 100644 templates/admin_base/operations/product_form.html delete mode 100644 templates/admin_base/operations/product_list.html delete mode 100644 templates/admin_base/operations/product_wizard.html delete mode 100644 templates/admin_base/operations/productgroup_confirm_delete.html delete mode 100644 templates/admin_base/operations/productgroup_form.html delete mode 100644 templates/admin_base/operations/productgroup_list.html delete mode 100644 templates/admin_base/operations/request_detail.html delete mode 100644 templates/admin_base/operations/request_list.html delete mode 100644 templates/admin_base/operations/route_list.html delete mode 100644 templates/admin_base/operations/task_list.html delete mode 100644 templates/admin_base/operations/token_detail.html delete mode 100644 templates/admin_base/operations/token_list.html delete mode 100644 templates/admin_base/operations/user_detail.html delete mode 100644 templates/admin_base/operations/user_list.html delete mode 100644 templates/admin_base/provider/coming_soon.html delete mode 100644 templates/admin_base/provider/dashboard.html delete mode 100644 templates/admin_base/providers/host_list.html delete mode 100644 templates/admin_base/providers/host_provider_assign.html delete mode 100644 templates/admin_base/providers/hostgroup_list.html delete mode 100644 templates/admin_base/providers/hostgroup_provider_assign.html delete mode 100644 templates/admin_base/reglinks/reglink_confirm_delete.html delete mode 100644 templates/admin_base/reglinks/reglink_form.html delete mode 100644 templates/admin_base/reglinks/reglink_list.html delete mode 100644 templates/admin_base/themes/pagecontent_confirm_delete.html delete mode 100644 templates/admin_base/themes/pagecontent_form.html delete mode 100644 templates/admin_base/themes/pagecontent_list.html delete mode 100644 templates/admin_base/themes/themeconfig_edit.html delete mode 100644 templates/admin_base/themes/widgetlayout_confirm_delete.html delete mode 100644 templates/admin_base/themes/widgetlayout_form.html delete mode 100644 templates/admin_base/themes/widgetlayout_list.html delete mode 100644 templates/admin_base/tickets/activity_list.html delete mode 100644 templates/admin_base/tickets/category_confirm_delete.html delete mode 100644 templates/admin_base/tickets/category_form.html delete mode 100644 templates/admin_base/tickets/category_list.html delete mode 100644 templates/admin_base/tickets/ticket_detail.html delete mode 100644 templates/admin_base/tickets/ticket_list.html delete mode 100644 templates/admin_base/users/user_confirm_delete.html delete mode 100644 templates/admin_base/users/user_form.html delete mode 100644 templates/admin_base/users/user_list.html delete mode 100644 templates/admin_base/users/user_reset_password.html delete mode 100755 templates/base.html delete mode 100644 templates/components/alert.html delete mode 100644 templates/components/button.html delete mode 100644 templates/cotton/x_admin_alert.html delete mode 100644 templates/cotton/x_admin_badge.html delete mode 100644 templates/cotton/x_admin_button.html delete mode 100644 templates/cotton/x_admin_card.html delete mode 100644 templates/cotton/x_admin_empty.html delete mode 100644 templates/cotton/x_admin_input.html delete mode 100644 templates/cotton/x_admin_modal.html delete mode 100644 templates/cotton/x_admin_pagination.html delete mode 100644 templates/cotton/x_admin_table.html delete mode 100644 templates/cotton/x_md_alert.html delete mode 100755 templates/dashboard/base.html delete mode 100755 templates/dashboard/index.html delete mode 100644 templates/dashboard/sitegroup_config.html delete mode 100644 templates/dashboard/sitegroup_detail.html delete mode 100644 templates/dashboard/sitegroup_form.html delete mode 100644 templates/dashboard/sitegroup_list.html delete mode 100644 templates/dashboard/sitegroup_user_list.html delete mode 100644 templates/dashboard/sitegroup_user_remove.html delete mode 100644 templates/dashboard/sitegroup_user_reset_password.html delete mode 100755 templates/dashboard/system_config.html delete mode 100755 templates/dashboard/widget_config.html delete mode 100644 templates/docs/index.html delete mode 100755 templates/errors/400.html delete mode 100755 templates/errors/403.html delete mode 100755 templates/errors/404.html delete mode 100755 templates/errors/500.html delete mode 100755 templates/maintenance.html delete mode 100644 templates/operations/account_opening_confirm.html delete mode 100755 templates/operations/account_opening_request_detail.html delete mode 100644 templates/operations/account_opening_request_form.html delete mode 100755 templates/operations/account_opening_request_list.html delete mode 100755 templates/operations/cloud_computer_user_list.html delete mode 100644 templates/operations/invite_result.html delete mode 100755 templates/operations/my_cloud_computer_detail.html delete mode 100755 templates/operations/my_cloud_computers.html delete mode 100755 templates/operations/systemtask_detail.html delete mode 100755 templates/operations/systemtask_list.html delete mode 100644 templates/tickets/dashboard.html delete mode 100644 templates/tickets/email/assigned.html delete mode 100644 templates/tickets/email/closed.html delete mode 100644 templates/tickets/email/new_comment.html delete mode 100644 templates/tickets/email/overdue.html delete mode 100644 templates/tickets/email/status_update.html delete mode 100644 templates/tickets/my_tickets.html delete mode 100644 templates/tickets/pending_list.html delete mode 100644 templates/tickets/ticket_detail.html delete mode 100644 templates/tickets/ticket_form.html delete mode 100644 templates/tickets/ticket_list.html delete mode 100644 tickets/__init__.py delete mode 100644 tickets/admin.py delete mode 100644 tickets/apps.py delete mode 100644 tickets/migrations/__init__.py delete mode 100644 tickets/models.py delete mode 100644 tickets/tests/__init__.py delete mode 100644 tickets/views.py delete mode 100755 utils/__init__.py delete mode 100644 utils/ca_bundle.py delete mode 100644 utils/cert_service.py delete mode 100644 utils/cert_storage.py delete mode 100644 utils/crypto.py delete mode 100644 utils/disk_quota.py delete mode 100755 utils/error_handlers.py delete mode 100644 utils/gateway_client.py delete mode 100755 utils/helpers.py delete mode 100755 utils/local_winserver_client.py delete mode 100755 utils/production_checker.py delete mode 100644 utils/provider.py delete mode 100755 utils/rate_limit.py delete mode 100644 utils/redis_helper.py delete mode 100755 utils/sensitive_log_filters.py delete mode 100644 utils/site_group.py delete mode 100755 utils/winrm_client.py delete mode 100644 uv.lock delete mode 100644 uv.toml diff --git a/.env.example b/.env.example index e8e314f..1b4564c 100644 --- a/.env.example +++ b/.env.example @@ -1,90 +1,77 @@ -# ZASCA 环境配置文件模板 -# 复制此文件为 .env 并填写实际配置值 - -# ========== 核心配置(影响功能或必须在初始化时定义) ========== - -# 调试模式(生产环境必须设置为 False) -DEBUG=True - -# Django 密钥(生产环境必须修改) -DJANGO_SECRET_KEY=your-secret-key-here-change-this-in-production - -# 允许访问的主机(生产环境必须配置) -ALLOWED_HOSTS=localhost,127.0.0.1 - -# CSRF 可信来源(生产环境必须配置) -CSRF_TRUSTED_ORIGINS=https://localhost,https://127.0.0.1 - -# ========== 数据库配置 ========== -# 数据库引擎: sqlite, mysql 或 postgresql -DB_ENGINE=sqlite -# MySQL 配置(DB_ENGINE=mysql 时生效) -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_NAME=zasca -DB_USER=root -DB_PASSWORD=your_database_password_here -# PostgreSQL 配置(DB_ENGINE=postgresql 时生效) -# 安装依赖: uv sync --extra postgresql -#DB_HOST=127.0.0.1 -#DB_PORT=5432 -#DB_NAME=zasca -#DB_USER=postgres -#DB_PASSWORD=your_database_password_here - -# ========== Redis 配置(可选增强) ========== -# Redis 是锦上添花的组件,不配置时程序使用本地替代方案: -# 缓存 -> LocMemCache(本地内存) -# 会话 -> 数据库存储 -# Celery -> SQLite broker -# 配置 REDIS_URL 且 Redis 服务可达时,自动切换到 Redis: -# 缓存 -> Redis(高性能,支持分布式) -# 会话 -> Redis 缓存(更快,支持多进程共享) -# Celery -> Redis broker(更稳定,支持结果过期清理) -#REDIS_URL=redis://localhost:6379/0 - -# ========== Celery 配置 ========== -# 未配置 Redis 时默认使用 SQLite broker,无需手动设置 -# 配置了 Redis 后默认自动使用 Redis broker(db1/db2),也可手动覆盖: -#CELERY_BROKER_URL=redis://localhost:6379/1 -#CELERY_RESULT_BACKEND=redis://localhost:6379/2 - -# ========== 演示模式 ========== -# 设置为 1 启用演示模式 -ZASCA_DEMO=0 - -# ========== 安全配置 ========== -# 生产环境必须设置为 True -SECURE_SSL_REDIRECT=False -SESSION_COOKIE_SECURE=False -CSRF_COOKIE_SECURE=False - -# 可信反向代理 IP(逗号分隔) -# 使用 nginx 反向代理时必须配置,否则所有用户共享同一 IP 导致限流误触发 -TRUSTED_PROXY_IPS=127.0.0.1,::1 - -# ========== 日志配置 ========== -LOG_LEVEL=DEBUG -LOG_FILE=/var/log/2c2a/application.log - -# ========== WinRM 配置 ========== +# 2c2a 异步架构 - 环境变量配置示例 +# 复制为 .env 并修改:cp .env.example .env + +# ── 运行环境 ── +ENV=production # production / staging / development +DEBUG=false +2C2A_DEMO=0 # 1=演示模式(自动生成密钥,仅本地) + +# ── Granian / FastAPI ── +HOST=0.0.0.0 +PORT=8000 +WORKERS=4 # 生产建议 CPU 核心数 + +# ── 密钥(生产必须显式配置)── +SECRET_KEY= # 生成:python -c "import secrets;print(secrets.token_urlsafe(48))" + +# Ed25519 密钥对(生成见下方说明) +ED25519_PRIVATE_KEY_PEM= +ED25519_PUBLIC_KEY_PEM= + +# AES-GCM 主密钥(32 字节 base64) +# 生成:python -c "import base64,os;print(base64.b64encode(os.urandom(32)).decode())" +CRYPTO_MASTER_KEY_B64= + +# keyed-BLAKE2b 缓存签名密钥 +CACHE_SIGNING_KEY= # 生成:python -c "import secrets;print(secrets.token_urlsafe(32))" + +# ── 数据库 ── +DB_ENGINE=sqlite # sqlite / postgresql / mysql +DB_NAME=2c2a +# PostgreSQL/MySQL 专用 +DB_HOST=localhost +DB_PORT=5432 +DB_USER=2c2a +DB_PASSWORD= +DB_POOL_SIZE=10 +DB_MAX_OVERFLOW=20 + +# ── Redis ── +REDIS_ENABLED=true +REDIS_URL=redis://localhost:6379/0 + +# ── 认证 ── +ACCESS_TOKEN_TTL_SECONDS=300 # Access Token 5 分钟 +REFRESH_TOKEN_TTL_DAYS=7 # Refresh Token 7 天 + +# ── Argon2id 参数 ── +ARGON2_TIME_COST=3 +ARGON2_MEMORY_COST=65536 # 64 MiB +ARGON2_PARALLELISM=2 + +# ── 缓存 ── +APP_SHELL_CACHE_TTL=300 # App Shell 边缘缓存 5 分钟 +TENANT_CACHE_TTL=300 + +# ── WinRM ── WINRM_TIMEOUT=30 -WINRM_RETRY_COUNT=3 - -# ========== Gateway 配置 ========== -GATEWAY_ENABLED=False -GATEWAY_CONTROL_SOCKET=/run/2c2a/control.sock - -# ========== Beta数据库配置(Beta推送插件) ========== -# 配置后可将生产数据推送到Beta版本数据库,仅支持PostgreSQL架构 -#BETA_DB_NAME=zasca_beta -#BETA_DB_USER=postgres -#BETA_DB_PASSWORD=your_beta_database_password_here -#BETA_DB_HOST=127.0.0.1 -#BETA_DB_PORT=5432 -# Beta环境的SECRET_KEY(用于重加密:生产密钥解密 → Beta密钥加密) -# 若不配置则直接复制密文,Beta端需使用与生产相同的SECRET_KEY才能解密 -#BETA_SECRET_KEY= - -# ========== Bootstrap 认证配置 ========== -BOOTSTRAP_SHARED_SALT= +WINRM_MAX_RETRIES=3 + +# ── 速率限制 ── +LOGIN_RATE_LIMIT=5 +API_RATE_LIMIT=100 + +# ── 可信代理 ── +TRUSTED_PROXY_IPS= +USE_X_FORWARDED_FOR=true + +# ────────────────────────────────────────────── +# Ed25519 密钥对生成方法: +# python -c " +# from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +# from cryptography.hazmat.primitives import serialization +# k = Ed25519PrivateKey.generate() +# print('ED25519_PRIVATE_KEY_PEM=' + k.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()).decode()) +# print('ED25519_PUBLIC_KEY_PEM=' + k.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode()) +# " +# ────────────────────────────────────────────── diff --git a/README.md b/README.md index a4c9bac..ae655e1 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ ![2c2a Logo](./docs/images/logo.svg) -

2c2a - -Cloudy Computer Account Activation Integration Platform

+

2c2a - Cloudy Computer Account Activation Integration Platform

- 基于 Django 的企业级 Windows 主机远程管理平台
- 零代理架构 · WinRM 直连 · Gateway 隧道保护 · 可选部署 + 异步架构重写版 · 全异步非阻塞 · 边缘缓存 · 站点隔离 · 插件系统
+ Granian + FastAPI + SQLAlchemy 2.0 Async + HTMX OOB + RedisHuey + JinjaX + aiohttp

- Python 3.13+ - Django 5.x - Go 1.22+ + Python 3.12+ + FastAPI + SQLAlchemy 2.0 + Granian License: AGPL-3.0

@@ -21,219 +21,191 @@ Cloudy Computer Account Activation Integration Platform --- -## 核心特性 +## 架构概览 -![核心特性](./docs/images/features.svg) +2c2a v2.0 是从 Django 同步架构全面重写的异步版本,贯彻**异步化**与**模块化**思想,前端绝不阻塞。 -- **零代理架构**:无需在目标主机安装客户端软件,通过 WinRM 协议直接管控 -- **Gateway 隧道保护**:可选部署 Gateway,为零公网 IP 主机提供安全 RDP 访问 -- **Django Admin 优先**:最大化利用 Django 内置管理功能,降低学习成本 -- **Material Design 3**:现代化的前端用户体验,支持多主题切换 -- **RBAC 权限控制**:细粒度的角色和权限管理,满足企业合规要求 -- **安全审计**:完整的操作日志和安全监控,支持行为分析 -- **松耦合设计**:Gateway 为可选组件,2c2a 可独立运行 +### 技术栈 -## 系统架构 +| 层 | 技术 | 说明 | +|---|---|---| +| ASGI 服务器 | **Granian** | Rust 内核高性能 ASGI 服务器 | +| Web 框架 | **FastAPI** | 原生异步,依赖注入 | +| ORM | **SQLAlchemy 2.0 Async** | `Mapped`/`mapped_column` 新语法,全异步会话 | +| 模板 | **JinjaX + Jinja2** | 组件化模板,配合 HTMX OOB | +| 前端交互 | **HTMX OOB** | 服务端渲染片段,Out-of-Band swap | +| 任务队列 | **RedisHuey** | 替代 Celery,轻量异步任务 | +| 远程管理 | **aiohttp** | 替代同步 pywinrm,全异步 WinRM | +| 缓存 | **Redis** | 边缘缓存 + 租户缓存 + 任务 broker | -![系统架构](./docs/images/architecture-v2.svg) +### 核心机制 -2c2a 采用四层架构设计: +**1. App Shell 边缘全量缓存 + HTMX 动态片段精准分离** +- 页面骨架路由仅依据请求域名解析租户配置,绝不依赖用户状态 +- 按域名区分 + keyed-BLAKE2b 签名生成缓存键,CDN 边缘节点全量高速缓存与防污染 +- 用户导航、统计等动态内容由 HTMX 在页面加载后发起独立请求,服务端基于 Ed25519 验签实时返回不可缓存片段 -| 层级 | 组件 | 说明 | -|------|------|------| -| **管理层** | Django Admin | RBAC、审计、工单、插件、主题、主机保护配置 | -| **核心层** | 2c2a Django | WinRM 客户端、Celery 任务、GatewayClient、证书管理 | -| **网关层** | Gateway (Go) | RDP 代理 (SNI 路由)、WSS 隧道服务、控制面 (可选) | -| **边缘层** | 2c2a-tunnel (Go) | Windows 服务、WSS 客户端、多路复用、远程执行 | +**2. 身份认证** +- Access Token:Ed25519 签名 JWT,5 分钟有效期,存放前端内存(防 XSS) +- Refresh Token:AES-GCM 加密,7 天有效期,HttpOnly Cookie(防 XSS 读取) +- 密码:前端 BLAKE2b 预哈希(防 DoS 截断)+ 后端 Argon2id 加盐慢哈希(抗 GPU/ASIC 爆破) +- ban_version 机制:JWT Payload 携带版本号,封禁时递增数据库版本,无需 Redis 黑名单实现无状态秒级令牌撤销 -> **Gateway 为可选组件**:不部署 Gateway 时,2c2a 通过 WinRM 直连管理主机,功能完全可用。部署 Gateway 后可启用主机保护模式,实现零公网 IP 的安全 RDP 访问。 +**3. 字段级加密** +- HKDF-SHA256 按字段名派生子密钥 + AES-256-GCM 加密,实现字段级密钥隔离与防篡改 -## 生态项目 +**4. 原生插件系统** +- 插件基类、服务注册表、路由提供者、UI 扩展、事件钩子 +- 启动时自动发现并加载,路由动态挂载 -| 项目 | 语言 | 说明 | -|------|------|------| -| [2c2a](.) | Python/Django | 核心管理平台,Web 后台 + API | -| [Gateway](../gateway) | Go | 隧道网关,RDP 代理 + WSS 服务 + 控制面 | -| [Tunnel](../tunnel) | Go | 边缘代理,Windows 服务 + WSS 客户端 | +**5. 原生站点隔离** +- 按请求域名解析租户(SiteGroup),所有业务数据按租户过滤 +- 站点组配置覆盖全局配置 -## 快速开始 +### 已移除功能 +- 仪表盘自定义功能(DashboardWidget / WidgetLayout) +- 隧道功能(tunnel app、Host.tunnel_* 字段、Gateway 客户端) -![快速开始流程](./docs/images/workflow.svg) +--- -### 环境要求 +## 目录结构 -- Python 3.13+(由 `.python-version` 文件指定) -- PostgreSQL 12+(可选,也可使用 SQLite) -- Go 1.22+(仅 Gateway/Tunnel 开发需要) +``` +app/ +├── core/ # 配置、数据库、Redis、日志、异常 +├── security/ # BLAKE2b/Argon2id/HKDF/AES-GCM/Ed25519 JWT/Refresh/ban_version +├── cache/ # keyed-BLAKE2b 缓存键、App Shell 边缘缓存、HTMX 片段 +├── tenant/ # 域名租户解析、中间件、依赖注入 +├── models/ # SQLAlchemy 2.0 异步模型(44 模型 / 52 表) +├── auth/ # 认证依赖、路由、schemas +├── winrm/ # aiohttp 异步 WinRM 客户端 +├── tasks/ # RedisHuey 异步任务 +├── plugins/ # 插件系统框架 + 示例插件 +├── api/v1/ # JSON API(hosts/operations/tickets/audit) +├── web/ # App Shell 页面骨架 + HTMX 动态片段 +├── templates/ # Jinja2 布局/页面/片段 + JinjaX 组件 +├── static/ # CSS/JS/HTMX +└── main.py # FastAPI 应用工厂 + Granian 入口 +alembic/ # 数据库迁移 +``` -### 环境配置 +--- + +## 快速开始 -1. **复制环境配置文件** +### 1. 安装依赖 ```bash -cp .env.example .env +pip install -e . +# 或 +pip install granian fastapi "sqlalchemy[asyncio]" aiosqlite redis huey aiohttp \ + argon2-cffi pyjwt cryptography pydantic-settings jinjax jinja2 structlog ``` -2. **编辑 .env 文件** +### 2. 配置环境变量 ```bash -nano .env +cp .env.example .env +# 开发模式:设置 DEBUG=true 2C2A_DEMO=1,密钥自动生成 +# 生产模式:必须配置 SECRET_KEY / ED25519_* / CRYPTO_MASTER_KEY_B64 / CACHE_SIGNING_KEY ``` -3. **关键配置项说明** - +生成密钥: ```bash -DEBUG=True -SECRET_KEY=your-secret-key-here - -# 数据库配置 (PostgreSQL) -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=2c2a_dev -DB_USER=2c2a_user -DB_PASSWORD=your_password - -# Gateway (可选) -GATEWAY_ENABLED=False -GATEWAY_CONTROL_SOCKET=/run/2c2a/control.sock - -# 演示模式 (快速体验) -2C2A_DEMO=1 +python -c "import secrets;print('SECRET_KEY='+secrets.token_urlsafe(48))" +python -c "import base64,os;print('CRYPTO_MASTER_KEY_B64='+base64.b64encode(os.urandom(32)).decode())" +python -c "import secrets;print('CACHE_SIGNING_KEY='+secrets.token_urlsafe(32))" +python -c " +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives import serialization +k=Ed25519PrivateKey.generate() +print('ED25519_PRIVATE_KEY_PEM='+k.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()).decode()) +print('ED25519_PUBLIC_KEY_PEM='+k.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo).decode()) +" ``` -### 开发环境搭建 +### 3. 初始化数据库 ```bash -git clone https://github.com/2c2a/2c2a.git -cd 2c2a - -uv sync -uv run python manage.py migrate -uv run python manage.py createsuperuser -uv run python manage.py runserver +alembic upgrade head ``` -访问 `http://127.0.0.1:8000/admin/` 进入管理后台。 - -### 启用 Gateway(可选) +### 4. 启动服务 ```bash -# 1. 构建并启动 Gateway -cd ../gateway -go build -o 2c2a-gateway ./cmd/gateway/ -./2c2a-gateway -config configs/gateway.yaml - -# 2. 在 2c2a .env 中启用 -GATEWAY_ENABLED=True - -# 3. 启动事件监听 -uv run python manage.py gateway_listener +# 开发 +granian --interface asgi --reload app.main:app --host 0.0.0.0 --port 8000 -# 4. 为主机生成 Tunnel Token -uv run python manage.py generate_tunnel_token +# 生产(多 worker) +granian --interface asgi --workers 4 app.main:app --host 0.0.0.0 --port 8000 ``` -### 部署 Tunnel 到 Windows 主机 +### 5. 启动任务消费者(可选,WinRM 后台操作) ```bash -# 下载 2c2a-tunnel.exe (从 GitHub Release) -# 或通过 CI/CD 自动打包 - -2c2a-tunnel.exe install \ - -token \ - -server wss://gateway.example.com:9000 +huey_consumer app.tasks.huey_app.huey ``` -> **注意**:本项目使用 [UV](https://github.com/astral-sh/uv) 作为 Python 包管理器。所有 Python 命令都必须通过 `uv run` 执行。 +--- -## 项目结构 +## 缓存策略详解 ``` -2c2a/ -├── apps/ # 应用模块 -│ ├── accounts/ # 用户认证 -│ ├── hosts/ # 主机管理 + 隧道连接 -│ ├── operations/ # 运维操作 + RDP域名路由 -│ ├── audit/ # 审计日志 + 隧道事件 -│ ├── dashboard/ # 仪表盘 -│ ├── bootstrap/ # 安全启动 -│ ├── certificates/ # 证书管理 -│ ├── plugins/ # 插件系统 -│ └── themes/ # 主题管理 -├── config/ # 项目配置 -├── utils/ # 工具模块 -│ ├── gateway_client.py # Gateway客户端(松耦合) -│ ├── winrm_client.py # WinRM客户端 -│ └── ... -├── docs/ # 技术文档 -├── frontend/ # 前端静态文件和模板 -└── pyproject.toml # 项目依赖 (UV, 无Redis) +请求 → TenantMiddleware(按域名解析租户,Redis 缓存) + → App Shell 路由(仅租户配置,keyed-BLAKE2b 缓存键) + ├─ 缓存命中 → 返回 HTML(Cache-Control: public, CDN 可缓存) + └─ 缓存未命中 → 渲染骨架 → 写缓存 → 返回 + → 浏览器加载 HTML 后,HTMX 发起片段请求 + → /fragments/* 路由(Ed25519 验签 + 租户依赖) + → 返回片段(Cache-Control: no-store,不可缓存) ``` -## 文档目录 - -详细的项目文档请查看 [`docs/`](./docs) 目录: - -- [开发规范指南](./docs/00_开发规范指南.md) - 强制执行的开发标准 -- [项目架构与设计](./docs/01_项目架构与设计.md) - 系统架构和技术选型 -- [API接口文档](./docs/02_API接口文档.md) - RESTful API 详细说明 -- [Database Schema](./docs/03_Database_Schema.md) - 数据库设计和表结构 -- [部署运维手册](./docs/04_部署运维手册.md) - 生产环境部署指南 -- [更新日志](./docs/05_更新日志.md) - 版本发布历史 -- [安全配置指南](./docs/06_安全配置指南.md) - 安全策略和防护措施 +## 认证流程 -## 安全特性 - -- 基于角色的访问控制 (RBAC) -- 数据传输加密 (TLS/SSL) -- 敏感信息加密存储 -- 完整的操作审计日志(含隧道/RDP事件) -- 多因素认证支持 -- 防暴力破解机制 -- 安全启动和会话管理 -- 主机保护模式(Gateway 隧道隔离) -- Ed25519 密钥交换 - -## 贡献指南 +``` +登录:前端 BLAKE2b 预哈希密码 → POST /auth/login + → 后端 Argon2id 验证 → 签发 Ed25519 JWT(内存)+ AES-GCM Refresh(Cookie) -我们欢迎任何形式的贡献!请先阅读我们的[开发规范指南](./docs/00_开发规范指南.md)。 +请求:HTMX 自动注入 Authorization: Bearer + → Ed25519 验签 → ban_version 校验(无状态撤销) -### 分支规范(分阶段发布模型) +刷新:Access Token 即将过期 → POST /auth/refresh(携带 Cookie) + → AES-GCM 解密 Refresh → 校验 ban_version → 签发新 JWT + 轮换 Refresh -本项目采用 **5 级分阶段分支模型**,所有生态仓库(包括 [webServer](https://github.com/2c2a/webServer))统一遵循: +封禁:递增 User.ban_version → 所有旧 JWT 立即失效(无需 Redis 黑名单) +``` -| 分支 | 阶段 | 用途 | 部署环境 | -|------|------|------|----------| -| `master` | 生产版本 | 线上稳定运行,仅接受 hotfix 合并 | 生产服务器 | -| `beta` | 公测版本 | 服务器端集成测试,QA 验证通过后方可合入 master | 预发布/测试服务器 | -| `alpha` | 内测版本 | 本地开发机测试,功能验证与联调 | 本地开发环境 | -| `hotfix` | 热修补 | 紧急修复线上问题,从 master 切出,修复后合并回 master 并同步 beta/alpha | 临时生产修复 | -| `feat` | 功能开发 | 新功能迭代分支,开发完成后合并至 alpha 进入内测 | 本地开发环境 | +--- -**合并流向**:`feat` → `alpha` → `beta` → `master`,`hotfix` 可直接回灌各分支。 +## 插件开发 -### 开发流程 +```python +# app/plugins/myplugin/plugin.py +from fastapi import APIRouter +from app.plugins import PluginInterface, RouteProvider -1. Fork 项目 -2. 从 `feat` 切出功能子分支 (`git checkout -b feat/xxx origin/feat`) -3. 开发完成后合并至 `alpha` 进行本地测试 -4. 通过测试后提 PR 合并至 `beta` 进行服务器公测 -5. QA 通过后由维护者合并至 `master` 发布 +class Plugin(PluginInterface, RouteProvider): + def __init__(self): + super().__init__("myplugin", "My Plugin", "1.0.0") -## 许可证 + async def initialize(self) -> bool: + return True -本项目采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + async def shutdown(self) -> bool: + return True -## 联系我们 + def get_routers(self) -> list[APIRouter]: + router = APIRouter() + @router.get("/hello") + async def hello(): + return {"msg": "hello"} + return [router] +``` -- 组织主页: https://github.com/2c2a -- 2c2a 仓库: https://github.com/2c2a/2c2a -- 问题反馈: [GitHub Issues](https://github.com/2c2a/2c2a/issues) +插件目录结构:`app/plugins/myplugin/__init__.py` + `plugin.py`,启动时自动发现加载。 --- -
- -*2c2a - 让 Windows 主机管理更简单、更安全* +## License -
+AGPL-3.0 diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..7830eee --- /dev/null +++ b/alembic.ini @@ -0,0 +1,43 @@ +# Alembic 配置 +[alembic] +script_location = alembic +prepend_sys_path = . +sqlalchemy.url = + +# 模板 +[post_write_hooks] + +# 日志 +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..2faad92 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,63 @@ +"""Alembic 迁移环境(异步)。 + +使用 SQLAlchemy 2.0 异步引擎执行迁移。 +""" +from __future__ import annotations + +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from app.core.config import settings +from app.models import Base # 导入所有模型以注册到 metadata + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# 注入数据库 URL +config.set_main_option("sqlalchemy.url", settings.database_url) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + """离线模式:生成 SQL 脚本。""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online() -> None: + """在线模式:异步引擎执行迁移。""" + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + await connectable.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..6f2cb5c --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from __future__ import annotations + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/208e77e631c4_initial_schema.py b/alembic/versions/208e77e631c4_initial_schema.py new file mode 100644 index 0000000..4f0e7c5 --- /dev/null +++ b/alembic/versions/208e77e631c4_initial_schema.py @@ -0,0 +1,841 @@ +"""initial schema + +Revision ID: 208e77e631c4 +Revises: +Create Date: 2026-06-18 12:17:06.674307 +""" +from __future__ import annotations + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = '208e77e631c4' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('certificate_authority', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('cert_root', sa.String(length=2), nullable=False), + sa.Column('cert_sub', sa.String(length=2), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('page_content', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('position', sa.String(length=50), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('is_enabled', sa.Boolean(), nullable=False), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('position') + ) + op.create_table('plugin_record', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('plugin_id', sa.String(length=100), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('version', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('plugin_id') + ) + op.create_table('site_group', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('slug', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('site_name', sa.String(length=100), nullable=False), + sa.Column('site_icon', sa.String(length=500), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug') + ) + op.create_table('system_config', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('smtp_host', sa.String(length=255), nullable=True), + sa.Column('smtp_port', sa.Integer(), nullable=True), + sa.Column('smtp_encryption', sa.String(length=8), nullable=False), + sa.Column('smtp_username', sa.String(length=255), nullable=True), + sa.Column('smtp_password_cipher', sa.String(length=255), nullable=True), + sa.Column('smtp_from_email', sa.String(length=254), nullable=True), + sa.Column('smtp_from_name', sa.String(length=255), nullable=True), + sa.Column('captcha_provider', sa.String(length=32), nullable=False), + sa.Column('captcha_type', sa.String(length=32), nullable=False), + sa.Column('login_captcha_type', sa.String(length=32), nullable=True), + sa.Column('register_captcha_type', sa.String(length=32), nullable=True), + sa.Column('email_captcha_type', sa.String(length=32), nullable=True), + sa.Column('site_name', sa.String(length=100), nullable=False), + sa.Column('site_icon', sa.String(length=500), nullable=False), + sa.Column('enable_registration', sa.Boolean(), nullable=False), + sa.Column('icp_number', sa.String(length=100), nullable=True), + sa.Column('police_number', sa.String(length=100), nullable=True), + sa.Column('email_suffix_whitelist', sa.Text(), nullable=True), + sa.Column('email_suffix_blacklist', sa.Text(), nullable=True), + sa.Column('local_access_locked', sa.Boolean(), nullable=False), + sa.Column('hostname_branding', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('theme_config', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('active_theme', sa.String(length=50), nullable=False), + sa.Column('branding', sa.JSON(), nullable=True), + sa.Column('custom_colors', sa.JSON(), nullable=True), + sa.Column('css_overrides', sa.Text(), nullable=True), + sa.Column('enable_mobile_optimization', sa.Boolean(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_group', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=150), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=False), + sa.Column('auto_staff', sa.Boolean(), nullable=False), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('username', sa.String(length=150), nullable=False), + sa.Column('email', sa.String(length=254), nullable=True), + sa.Column('phone', sa.String(length=20), nullable=True), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('avatar', sa.String(length=500), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_staff', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=False), + sa.Column('last_login', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_login_ip', sa.String(length=45), nullable=True), + sa.Column('ban_version', sa.Integer(), nullable=False), + sa.Column('date_joined', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('async_task', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('task_id', sa.String(length=255), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('progress', sa.Integer(), nullable=False), + sa.Column('result', sa.JSON(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('target_object_id', sa.Integer(), nullable=True), + sa.Column('target_content_type', sa.String(length=100), nullable=True), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('task_id') + ) + op.create_table('client_certificate', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('upn_value', sa.String(length=255), nullable=True), + sa.Column('ca_id', sa.Integer(), nullable=False), + sa.Column('thumbprint', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('assigned_to_user_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['assigned_to_user_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['ca_id'], ['certificate_authority.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('thumbprint') + ) + op.create_table('host_group', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('site_group_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('hosts', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('os_type', sa.String(length=20), nullable=False), + sa.Column('hostname', sa.String(length=255), nullable=False), + sa.Column('connection_type', sa.String(length=20), nullable=False), + sa.Column('auth_method', sa.String(length=20), nullable=False), + sa.Column('port', sa.Integer(), nullable=False), + sa.Column('rdp_port', sa.Integer(), nullable=False), + sa.Column('use_ssl', sa.Boolean(), nullable=False), + sa.Column('username', sa.String(length=100), nullable=True), + sa.Column('password_cipher', sa.String(length=255), nullable=True), + sa.Column('cert_pem_path', sa.String(length=512), nullable=True), + sa.Column('cert_key_path', sa.String(length=512), nullable=True), + sa.Column('os_version', sa.String(length=50), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('site_group_id', sa.Integer(), nullable=True), + sa.Column('cert_root', sa.String(length=2), nullable=True), + sa.Column('cert_sub', sa.String(length=2), nullable=True), + sa.Column('pfx_password_cipher', sa.String(length=255), nullable=True), + sa.Column('ntlm_fallback_user', sa.String(length=100), nullable=True), + sa.Column('ntlm_fallback_password_cipher', sa.String(length=255), nullable=True), + sa.Column('cert_activated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('cert_provision_status', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('login_log', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=False), + sa.Column('user_agent', sa.Text(), nullable=False), + sa.Column('login_type', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('failure_reason', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('plugin_configuration', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('plugin_id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(length=200), nullable=False), + sa.Column('value_cipher', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['plugin_id'], ['plugin_record.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('plugin_id', 'key', name='uq_plugin_configuration_plugin_key') + ) + op.create_table('product_group', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('display_order', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('visibility', sa.String(length=20), nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('registration_link', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=64), nullable=False), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('used_by_id', sa.Integer(), nullable=True), + sa.Column('max_uses', sa.Integer(), nullable=False), + sa.Column('used_count', sa.Integer(), nullable=False), + sa.Column('used', sa.Boolean(), nullable=False), + sa.Column('used_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('note', sa.String(length=200), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['group_id'], ['user_group.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['used_by_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token') + ) + op.create_table('security_event', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('event_type', sa.String(length=50), nullable=False), + sa.Column('severity', sa.String(length=10), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('resolved', sa.Boolean(), nullable=False), + sa.Column('resolved_by_id', sa.Integer(), nullable=True), + sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('resolution_notes', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['resolved_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('sensitive_operation', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('operation_type', sa.String(length=50), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('target', sa.String(length=255), nullable=False), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('justification', sa.Text(), nullable=True), + sa.Column('approved_by_id', sa.Integer(), nullable=True), + sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('result', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['approved_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('server_certificate', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('hostname', sa.String(length=255), nullable=False), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('ca_id', sa.Integer(), nullable=False), + sa.Column('thumbprint', sa.String(length=255), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('is_revoked', sa.Boolean(), nullable=False), + sa.Column('revocation_reason', sa.String(length=255), nullable=True), + sa.Column('revocation_date', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['ca_id'], ['certificate_authority.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('hostname'), + sa.UniqueConstraint('thumbprint') + ) + op.create_table('session_activity', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('session_key', sa.String(length=64), nullable=False), + sa.Column('ip_address', sa.String(length=45), nullable=False), + sa.Column('user_agent', sa.Text(), nullable=False), + sa.Column('login_time', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('logout_time', sa.DateTime(timezone=True), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('site_group_config', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=False), + sa.Column('smtp_host', sa.String(length=255), nullable=True), + sa.Column('smtp_port', sa.Integer(), nullable=True), + sa.Column('smtp_encryption', sa.String(length=8), nullable=True), + sa.Column('smtp_username', sa.String(length=255), nullable=True), + sa.Column('smtp_password_cipher', sa.String(length=255), nullable=True), + sa.Column('smtp_from_email', sa.String(length=254), nullable=True), + sa.Column('smtp_from_name', sa.String(length=255), nullable=True), + sa.Column('captcha_provider', sa.String(length=32), nullable=True), + sa.Column('captcha_type', sa.String(length=32), nullable=True), + sa.Column('login_captcha_type', sa.String(length=32), nullable=True), + sa.Column('register_captcha_type', sa.String(length=32), nullable=True), + sa.Column('email_captcha_type', sa.String(length=32), nullable=True), + sa.Column('enable_registration', sa.Boolean(), nullable=True), + sa.Column('email_suffix_whitelist', sa.Text(), nullable=True), + sa.Column('email_suffix_blacklist', sa.Text(), nullable=True), + sa.Column('site_name', sa.String(length=100), nullable=True), + sa.Column('site_icon', sa.String(length=500), nullable=True), + sa.Column('icp_number', sa.String(length=100), nullable=True), + sa.Column('police_number', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('site_group_id') + ) + op.create_table('site_group_hostname', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('hostname', sa.String(length=255), nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('hostname') + ) + op.create_table('system_task', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('task_type', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('progress', sa.Integer(), nullable=False), + sa.Column('result', sa.JSON(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('ticket_category', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('icon', sa.String(length=50), nullable=True), + sa.Column('default_priority', sa.String(length=20), nullable=False), + sa.Column('auto_assign_to_id', sa.Integer(), nullable=True), + sa.Column('auto_assign_to_group_id', sa.Integer(), nullable=True), + sa.Column('sla_hours', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('allow_banned_users', sa.Boolean(), nullable=False), + sa.Column('display_order', sa.Integer(), nullable=False), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['auto_assign_to_group_id'], ['user_group.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['auto_assign_to_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_ban', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('banned_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['banned_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id') + ) + op.create_table('user_ban_history', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('reason', sa.Text(), nullable=False), + sa.Column('banned_by_id', sa.Integer(), nullable=True), + sa.Column('unbanned_by_id', sa.Integer(), nullable=True), + sa.Column('banned_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('unbanned_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['banned_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['unbanned_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_email', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=254), nullable=False), + sa.Column('is_primary', sa.Boolean(), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_group_members', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('group_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['user_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'group_id') + ) + op.create_table('user_profile', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('nickname', sa.String(length=100), nullable=True), + sa.Column('gender', sa.String(length=10), nullable=True), + sa.Column('birthday', sa.Date(), nullable=True), + sa.Column('location', sa.String(length=100), nullable=True), + sa.Column('bio', sa.Text(), nullable=True), + sa.Column('email_notification', sa.Boolean(), nullable=False), + sa.Column('system_notification', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id') + ) + op.create_table('user_site_group_admins', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'site_group_id') + ) + op.create_table('user_site_groups', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'site_group_id') + ) + op.create_table('active_session', + sa.Column('session_token', sa.String(length=255), nullable=False), + sa.Column('host_id', sa.Integer(), nullable=False), + sa.Column('bound_ip', sa.String(length=45), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('session_token') + ) + op.create_table('audit_log', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('host_id', sa.Integer(), nullable=True), + sa.Column('action', sa.String(length=50), nullable=False), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('user_agent', sa.Text(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('success', sa.Boolean(), nullable=False), + sa.Column('details', sa.JSON(), nullable=True), + sa.Column('result', sa.Text(), nullable=True), + sa.Column('content_type', sa.String(length=100), nullable=True), + sa.Column('object_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('cert_provision_token', + sa.Column('token', sa.String(length=64), nullable=False), + sa.Column('host_id', sa.Integer(), nullable=True), + sa.Column('server_host', sa.String(length=255), nullable=True), + sa.Column('hostname', sa.String(length=255), nullable=True), + sa.Column('ip_address', sa.String(length=255), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('cert_data', sa.JSON(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('consumed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('token') + ) + op.create_table('host_administrators', + sa.Column('host_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('host_id', 'user_id') + ) + op.create_table('host_providers', + sa.Column('host_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('host_id', 'user_id') + ) + op.create_table('hostgroup_hosts', + sa.Column('hostgroup_id', sa.Integer(), nullable=False), + sa.Column('host_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['hostgroup_id'], ['host_group.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('hostgroup_id', 'host_id') + ) + op.create_table('hostgroup_providers', + sa.Column('hostgroup_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['hostgroup_id'], ['host_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('hostgroup_id', 'user_id') + ) + op.create_table('initial_token', + sa.Column('token', sa.String(length=255), nullable=False), + sa.Column('host_id', sa.Integer(), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('pairing_code', sa.String(length=6), nullable=True), + sa.Column('pairing_code_expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('pairing_attempts', sa.Integer(), nullable=False), + sa.Column('cert_data', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('token') + ) + op.create_table('product', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('display_name', sa.String(length=200), nullable=True), + sa.Column('display_description', sa.Text(), nullable=True), + sa.Column('product_group_id', sa.Integer(), nullable=True), + sa.Column('host_id', sa.Integer(), nullable=False), + sa.Column('site_group_id', sa.Integer(), nullable=True), + sa.Column('rdp_port', sa.Integer(), nullable=False), + sa.Column('display_hostname', sa.String(length=255), nullable=True), + sa.Column('is_available', sa.Boolean(), nullable=False), + sa.Column('auto_approval', sa.Boolean(), nullable=False), + sa.Column('visibility', sa.String(length=20), nullable=False), + sa.Column('limit_one_per_user', sa.Boolean(), nullable=False), + sa.Column('enable_disk_quota', sa.Boolean(), nullable=False), + sa.Column('enable_host_protection', sa.Boolean(), nullable=False), + sa.Column('default_disk_quota', sa.JSON(), nullable=True), + sa.Column('allow_extra_quota_disks', sa.JSON(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_group_id'], ['product_group.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['site_group_id'], ['site_group.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('productgroup_auto_providers', + sa.Column('productgroup_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['productgroup_id'], ['product_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('productgroup_id', 'user_id') + ) + op.create_table('public_host_info', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('internal_host_id', sa.Integer(), nullable=False), + sa.Column('display_name', sa.String(length=200), nullable=False), + sa.Column('display_description', sa.Text(), nullable=True), + sa.Column('display_hostname', sa.String(length=255), nullable=False), + sa.Column('display_rdp_port', sa.Integer(), nullable=False), + sa.Column('is_available', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['internal_host_id'], ['hosts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('internal_host_id') + ) + op.create_table('task_progress', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('progress', sa.Integer(), nullable=False), + sa.Column('message', sa.Text(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['task_id'], ['async_task.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('account_opening_request', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('applicant_id', sa.Integer(), nullable=False), + sa.Column('contact_email', sa.String(length=254), nullable=False), + sa.Column('contact_phone', sa.String(length=20), nullable=True), + sa.Column('username', sa.String(length=150), nullable=False), + sa.Column('user_fullname', sa.String(length=100), nullable=True), + sa.Column('user_email', sa.String(length=254), nullable=True), + sa.Column('user_description', sa.Text(), nullable=True), + sa.Column('target_product_id', sa.Integer(), nullable=False), + sa.Column('requested_disk_capacity', sa.JSON(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('approved_by_id', sa.Integer(), nullable=True), + sa.Column('approval_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('approval_notes', sa.Text(), nullable=True), + sa.Column('cloud_user_id', sa.String(length=255), nullable=True), + sa.Column('result_message', sa.Text(), nullable=True), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['applicant_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['approved_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['target_product_id'], ['product.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('product_invitation_token', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('token', sa.String(length=64), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=True), + sa.Column('product_group_id', sa.Integer(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=False), + sa.Column('max_uses', sa.Integer(), nullable=False), + sa.Column('used_count', sa.Integer(), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_group_id'], ['product_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token') + ) + op.create_table('rdp_domain_route', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('domain', sa.String(length=255), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=False), + sa.Column('assigned_to_id', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_activity_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['assigned_to_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('domain') + ) + op.create_table('cloud_computer_user', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('username', sa.String(length=150), nullable=False), + sa.Column('fullname', sa.String(length=100), nullable=True), + sa.Column('email', sa.String(length=254), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('product_id', sa.Integer(), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.Column('groups', sa.Text(), nullable=True), + sa.Column('disk_quota', sa.JSON(), nullable=True), + sa.Column('created_from_request_id', sa.Integer(), nullable=True), + sa.Column('owner_id', sa.Integer(), nullable=True), + sa.Column('initial_password_cipher', sa.String(length=512), nullable=True), + sa.Column('password_viewed', sa.Boolean(), nullable=False), + sa.Column('password_viewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['created_from_request_id'], ['account_opening_request.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('product_id', 'username', name='uq_cloud_user_product_username') + ) + op.create_table('product_access_grant', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=True), + sa.Column('product_group_id', sa.Integer(), nullable=True), + sa.Column('granted_by_token_id', sa.Integer(), nullable=True), + sa.Column('granted_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('is_revoked', sa.Boolean(), nullable=False), + sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('revoked_by_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['granted_by_token_id'], ['product_invitation_token.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['product_group_id'], ['product_group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['revoked_by_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'product_group_id', name='uq_user_productgroup_grant'), + sa.UniqueConstraint('user_id', 'product_id', name='uq_user_product_grant') + ) + op.create_table('ticket', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('ticket_no', sa.String(length=20), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('category_id', sa.Integer(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('priority', sa.String(length=20), nullable=False), + sa.Column('source', sa.String(length=20), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.Column('assignee_id', sa.Integer(), nullable=True), + sa.Column('assigned_group_id', sa.Integer(), nullable=True), + sa.Column('related_cloud_computer_id', sa.Integer(), nullable=True), + sa.Column('related_request_id', sa.Integer(), nullable=True), + sa.Column('due_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('satisfaction', sa.Integer(), nullable=True), + sa.Column('satisfaction_comment', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['assigned_group_id'], ['user_group.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['assignee_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['category_id'], ['ticket_category.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['related_cloud_computer_id'], ['cloud_computer_user.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['related_request_id'], ['account_opening_request.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('ticket_no') + ) + op.create_table('ticket_activity', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('ticket_id', sa.Integer(), nullable=False), + sa.Column('actor_id', sa.Integer(), nullable=True), + sa.Column('action', sa.String(length=20), nullable=False), + sa.Column('old_value', sa.Text(), nullable=True), + sa.Column('new_value', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['actor_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['ticket_id'], ['ticket.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('ticket_attachment', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('ticket_id', sa.Integer(), nullable=False), + sa.Column('file_path', sa.String(length=500), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=False), + sa.Column('uploaded_by_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['ticket_id'], ['ticket.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uploaded_by_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('ticket_comment', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('ticket_id', sa.Integer(), nullable=False), + sa.Column('author_id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('is_internal', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['author_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['ticket_id'], ['ticket.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('ticket_comment') + op.drop_table('ticket_attachment') + op.drop_table('ticket_activity') + op.drop_table('ticket') + op.drop_table('product_access_grant') + op.drop_table('cloud_computer_user') + op.drop_table('rdp_domain_route') + op.drop_table('product_invitation_token') + op.drop_table('account_opening_request') + op.drop_table('task_progress') + op.drop_table('public_host_info') + op.drop_table('productgroup_auto_providers') + op.drop_table('product') + op.drop_table('initial_token') + op.drop_table('hostgroup_providers') + op.drop_table('hostgroup_hosts') + op.drop_table('host_providers') + op.drop_table('host_administrators') + op.drop_table('cert_provision_token') + op.drop_table('audit_log') + op.drop_table('active_session') + op.drop_table('user_site_groups') + op.drop_table('user_site_group_admins') + op.drop_table('user_profile') + op.drop_table('user_group_members') + op.drop_table('user_email') + op.drop_table('user_ban_history') + op.drop_table('user_ban') + op.drop_table('ticket_category') + op.drop_table('system_task') + op.drop_table('site_group_hostname') + op.drop_table('site_group_config') + op.drop_table('session_activity') + op.drop_table('server_certificate') + op.drop_table('sensitive_operation') + op.drop_table('security_event') + op.drop_table('registration_link') + op.drop_table('product_group') + op.drop_table('plugin_configuration') + op.drop_table('login_log') + op.drop_table('hosts') + op.drop_table('host_group') + op.drop_table('client_certificate') + op.drop_table('async_task') + op.drop_table('users') + op.drop_table('user_group') + op.drop_table('theme_config') + op.drop_table('system_config') + op.drop_table('site_group') + op.drop_table('plugin_record') + op.drop_table('page_content') + op.drop_table('certificate_authority') + # ### end Alembic commands ### diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..21a453f --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,2 @@ +"""2c2a 异步架构根包。""" +__version__ = "2.0.0" diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..024dd94 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1 @@ +"""API 路由(JSON 接口,供前端/外部调用)。""" diff --git a/app/api/deps.py b/app/api/deps.py new file mode 100644 index 0000000..b6b5c11 --- /dev/null +++ b/app/api/deps.py @@ -0,0 +1,20 @@ +"""共享 API 依赖。""" +from __future__ import annotations + +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import CurrentUser, get_current_user +from app.core.db import get_db +from app.tenant.dependencies import get_tenant +from app.tenant.resolver import TenantContext + + +class DBDep: + def __init__(self, db: AsyncSession = Depends(get_db)): + self.db = db + + +class APIAuth: + def __init__(self, user: CurrentUser = Depends(get_current_user)): + self.user = user diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..d08e389 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1 @@ +"""API v1 路由包。""" diff --git a/app/api/v1/audit.py b/app/api/v1/audit.py new file mode 100644 index 0000000..b9c7969 --- /dev/null +++ b/app/api/v1/audit.py @@ -0,0 +1,35 @@ +"""审计 API。""" +from __future__ import annotations + +from fastapi import APIRouter, Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import require_staff +from app.core.db import get_db +from app.models.audit import AuditLog + +router = APIRouter(prefix="/audit", tags=["audit"]) + + +@router.get("/logs") +async def list_audit_logs( + user=Depends(require_staff), + db: AsyncSession = Depends(get_db), + limit: int = 50, +): + """列出审计日志(仅管理员)。""" + result = await db.execute( + select(AuditLog).order_by(AuditLog.timestamp.desc()).limit(limit) + ) + logs = result.scalars().all() + return [ + { + "id": l.id, + "action": l.action, + "ip_address": l.ip_address, + "success": l.success, + "timestamp": l.timestamp.isoformat() if l.timestamp else None, + } + for l in logs + ] diff --git a/app/api/v1/hosts.py b/app/api/v1/hosts.py new file mode 100644 index 0000000..07d4823 --- /dev/null +++ b/app/api/v1/hosts.py @@ -0,0 +1,122 @@ +"""主机管理 API。""" +from __future__ import annotations + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import APIAuth +from app.auth.dependencies import require_staff +from app.core.db import get_db +from app.models.host import Host +from app.tenant.dependencies import get_tenant +from app.tenant.resolver import TenantContext + +router = APIRouter(prefix="/hosts", tags=["hosts"]) + + +class HostOut(BaseModel): + id: int + name: str + hostname: str + connection_type: str + auth_method: str + port: int + status: str + os_version: str | None = None + + +class HostCreate(BaseModel): + name: str + hostname: str + connection_type: str = "winrm" + auth_method: str = "ntlm" + port: int = 5985 + rdp_port: int = 3389 + use_ssl: bool = False + username: str | None = None + password: str | None = None # 明文,后端加密存储 + description: str | None = None + + +@router.get("", response_model=list[HostOut]) +async def list_hosts( + user=Depends(require_staff), + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """列出主机(站点隔离)。""" + filters = [] + if tenant.site_group_id and not user.is_superuser: + filters.append( + (Host.site_group_id == tenant.site_group_id) + | (Host.site_group_id.is_(None)) + ) + result = await db.execute(select(Host).where(*filters).order_by(Host.id)) + hosts = result.scalars().all() + return [ + HostOut( + id=h.id, name=h.name, hostname=h.hostname, + connection_type=h.connection_type, auth_method=h.auth_method, + port=h.port, status=h.status, os_version=h.os_version, + ) + for h in hosts + ] + + +@router.post("", response_model=HostOut, status_code=201) +async def create_host( + body: HostCreate, + user=Depends(require_staff), + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """创建主机(密码字段级加密存储)。""" + from app.security.field_cipher import encrypt_field + + host = Host( + name=body.name, + hostname=body.hostname, + connection_type=body.connection_type, + auth_method=body.auth_method, + port=body.port, + rdp_port=body.rdp_port, + use_ssl=body.use_ssl, + username=body.username, + password_cipher=encrypt_field(body.password, "host.password") if body.password else None, + description=body.description, + created_by_id=user.id, + site_group_id=tenant.site_group_id, + status="active", + ) + db.add(host) + await db.commit() + await db.refresh(host) + return HostOut( + id=host.id, name=host.name, hostname=host.hostname, + connection_type=host.connection_type, auth_method=host.auth_method, + port=host.port, status=host.status, os_version=host.os_version, + ) + + +@router.post("/{host_id}/test") +async def test_host_connection( + host_id: int, + user=Depends(require_staff), + db: AsyncSession = Depends(get_db), +): + """测试主机连接(异步 WinRM,不阻塞)。""" + from app.winrm import AsyncWinRMClient + + result = await db.execute(select(Host).where(Host.id == host_id)) + host = result.scalar_one_or_none() + if host is None: + return {"success": False, "error": "主机不存在"} + + client = await AsyncWinRMClient.from_host_config(host) + try: + res = await client.execute_command("whoami") + return {"success": res.success, "output": res.std_out.strip(), "error": res.std_err} + finally: + await client.close() diff --git a/app/api/v1/operations.py b/app/api/v1/operations.py new file mode 100644 index 0000000..1cd79bd --- /dev/null +++ b/app/api/v1/operations.py @@ -0,0 +1,139 @@ +"""运维操作 API:开户申请、云电脑用户。""" +from __future__ import annotations + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import CurrentUser, get_current_user +from app.core.db import get_db +from app.models.operations import AccountOpeningRequest, CloudComputerUser +from app.tasks.operations import process_account_opening + +router = APIRouter(prefix="/operations", tags=["operations"]) + + +class AccountOpeningCreate(BaseModel): + target_product_id: int + username: str + user_fullname: str | None = None + user_email: str | None = None + user_description: str | None = None + contact_email: str + contact_phone: str | None = None + requested_disk_capacity: dict | None = None + + +class AccountOpeningOut(BaseModel): + id: int + status: str + username: str + target_product_id: int + + +@router.get("/account-openings", response_model=list[AccountOpeningOut]) +async def list_my_requests( + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """列出我的开户申请。""" + result = await db.execute( + select(AccountOpeningRequest) + .where(AccountOpeningRequest.applicant_id == user.id) + .order_by(AccountOpeningRequest.created_at.desc()) + ) + reqs = result.scalars().all() + return [ + AccountOpeningOut(id=r.id, status=r.status, username=r.username, target_product_id=r.target_product_id) + for r in reqs + ] + + +@router.post("/account-openings", response_model=AccountOpeningOut, status_code=201) +async def create_account_opening( + body: AccountOpeningCreate, + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """提交开户申请(异步处理,不阻塞前端)。 + + 提交后立即返回 pending 状态,后台通过 RedisHuey 任务执行 WinRM 操作。 + """ + req = AccountOpeningRequest( + applicant_id=user.id, + contact_email=body.contact_email, + contact_phone=body.contact_phone, + username=body.username, + user_fullname=body.user_fullname, + user_email=body.user_email, + user_description=body.user_description, + target_product_id=body.target_product_id, + requested_disk_capacity=body.requested_disk_capacity, + status="pending", + ) + db.add(req) + await db.commit() + await db.refresh(req) + + # 异步任务处理(不阻塞响应) + process_account_opening.schedule(args=(req.id,)) + + return AccountOpeningOut( + id=req.id, status=req.status, username=req.username, target_product_id=req.target_product_id + ) + + +class CloudComputerOut(BaseModel): + id: int + username: str + fullname: str | None + status: str + product_id: int + + +@router.get("/cloud-users", response_model=list[CloudComputerOut]) +async def list_my_cloud_users( + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """列出我的云电脑用户。""" + result = await db.execute( + select(CloudComputerUser) + .where(CloudComputerUser.owner_id == user.id) + .order_by(CloudComputerUser.created_at.desc()) + ) + users = result.scalars().all() + return [ + CloudComputerOut(id=u.id, username=u.username, fullname=u.fullname, status=u.status, product_id=u.product_id) + for u in users + ] + + +@router.get("/cloud-users/{ccu_id}/password") +async def get_initial_password( + ccu_id: int, + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """获取初始密码(阅后即焚:查看后清除)。""" + from datetime import datetime, timezone + + from app.security.field_cipher import decrypt_field + + result = await db.execute(select(CloudComputerUser).where(CloudComputerUser.id == ccu_id)) + ccu = result.scalar_one_or_none() + if ccu is None or ccu.owner_id != user.id: + return {"error": "不存在"} + + if ccu.password_viewed or not ccu.initial_password_cipher: + return {"error": "密码已查看或不存在"} + + password = decrypt_field(ccu.initial_password_cipher, "cloud_computer_user.initial_password") + # 阅后即焚 + ccu.password_viewed = True + ccu.password_viewed_at = datetime.now(timezone.utc) + ccu.initial_password_cipher = None + await db.commit() + + return {"password": password} diff --git a/app/api/v1/tickets.py b/app/api/v1/tickets.py new file mode 100644 index 0000000..bea48cf --- /dev/null +++ b/app/api/v1/tickets.py @@ -0,0 +1,73 @@ +"""工单 API。""" +from __future__ import annotations + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import CurrentUser, get_current_user +from app.core.db import get_db +from app.models.ticket import Ticket + +router = APIRouter(prefix="/tickets", tags=["tickets"]) + + +class TicketCreate(BaseModel): + title: str + description: str + category_id: int | None = None + priority: str = "normal" + + +class TicketOut(BaseModel): + id: int + ticket_no: str + title: str + status: str + priority: str + + +@router.get("", response_model=list[TicketOut]) +async def list_tickets( + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """列出我的工单。""" + result = await db.execute( + select(Ticket) + .where(Ticket.creator_id == user.id) + .order_by(Ticket.created_at.desc()) + ) + tickets = result.scalars().all() + return [ + TicketOut(id=t.id, ticket_no=t.ticket_no, title=t.title, status=t.status, priority=t.priority) + for t in tickets + ] + + +@router.post("", response_model=TicketOut, status_code=201) +async def create_ticket( + body: TicketCreate, + user: CurrentUser = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """创建工单。""" + import uuid + + ticket = Ticket( + ticket_no=f"TK-{uuid.uuid4().hex[:8].upper()}", + title=body.title, + description=body.description, + category_id=body.category_id, + priority=body.priority, + status="open", + creator_id=user.id, + ) + db.add(ticket) + await db.commit() + await db.refresh(ticket) + return TicketOut( + id=ticket.id, ticket_no=ticket.ticket_no, title=ticket.title, + status=ticket.status, priority=ticket.priority, + ) diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..00516d6 --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1 @@ +"""认证模块:依赖注入、路由、schemas。""" diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py new file mode 100644 index 0000000..7de7d46 --- /dev/null +++ b/app/auth/dependencies.py @@ -0,0 +1,114 @@ +"""认证依赖注入:从请求中解析当前用户。 + +流程: +- Access Token 从 Authorization: Bearer 头读取(前端内存存储,通过 JS 发送) +- Ed25519 验签 + 过期校验 +- ban_version 校验(无状态秒级撤销) +- Refresh Token 从 HttpOnly Cookie 读取(仅 /auth/refresh 端点使用) +""" +from __future__ import annotations + +from dataclasses import dataclass + +import jwt +from fastapi import Depends, Request +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.db import get_db +from app.core.exceptions import AuthError, ForbiddenError +from app.models.user import User +from app.security.ban_version import is_token_revoked +from app.security.jwt_auth import decode_access_token +from app.tenant.dependencies import get_tenant +from app.tenant.resolver import TenantContext + + +@dataclass +class CurrentUser: + """当前认证用户上下文。""" + + id: int + username: str + is_superuser: bool + is_staff: bool + ban_version: int + site_group_id: int | None + db_user: User | None = None + + +async def get_current_user_optional( + request: Request, + db: AsyncSession = Depends(get_db), +) -> CurrentUser | None: + """可选认证:有合法 token 返回用户,无 token 返回 None(用于公开页面)。""" + auth = request.headers.get("authorization", "") + if not auth.lower().startswith("bearer "): + return None + token = auth.split(" ", 1)[1].strip() + try: + payload = decode_access_token(token) + except jwt.PyJWTError: + return None + + user_id = int(payload["sub"]) + jwt_bv = int(payload.get("bv", 0)) + + # 查库校验 ban_version(无状态撤销) + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user is None or not user.is_active: + return None + if is_token_revoked(jwt_bv, user.ban_version): + return None # 已封禁/强制下线 + + return CurrentUser( + id=user.id, + username=user.username, + is_superuser=user.is_superuser, + is_staff=user.is_staff, + ban_version=user.ban_version, + site_group_id=payload.get("sg"), + db_user=user, + ) + + +async def get_current_user( + user: CurrentUser | None = Depends(get_current_user_optional), +) -> CurrentUser: + """强制认证:无合法用户抛 401。""" + if user is None: + raise AuthError("请先登录") + return user + + +async def require_staff( + user: CurrentUser = Depends(get_current_user), +) -> CurrentUser: + """要求 staff 及以上权限。""" + if not (user.is_staff or user.is_superuser): + raise ForbiddenError("需要管理员权限") + return user + + +async def require_superuser( + user: CurrentUser = Depends(get_current_user), +) -> CurrentUser: + """要求超级管理员权限。""" + if not user.is_superuser: + raise ForbiddenError("需要超级管理员权限") + return user + + +async def require_tenant_admin( + user: CurrentUser = Depends(get_current_user), + tenant: TenantContext = Depends(get_tenant), +) -> CurrentUser: + """要求当前租户的管理员权限(超管或站点组管理员)。""" + if user.is_superuser: + return user + if tenant.site_group_id and user.db_user: + # 检查是否为该站点组管理员 + if tenant.site_group_id in [sg.id for sg in user.db_user.admin_site_groups]: + return user + raise ForbiddenError("需要站点组管理员权限") diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..e224cfc --- /dev/null +++ b/app/auth/routes.py @@ -0,0 +1,233 @@ +"""认证路由:登录、注册、刷新、登出。 + +- 登录:验证 BLAKE2b 预哈希 + Argon2id,签发 Ed25519 JWT(内存)+ AES-GCM Refresh(Cookie) +- 刷新:用 Refresh Token 换取新 Access Token(滑动窗口) +- 登出:清除 Refresh Cookie(前端清除内存 Access Token) +- 注册:BLAKE2b 预哈希 + Argon2id 存储 +""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request, Response +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import CurrentUser, get_current_user +from app.auth.schemas import ( + ChangePasswordRequest, + LoginRequest, + LoginResponse, + RefreshResponse, + RegisterRequest, + UserInfo, +) +from app.core.config import settings +from app.core.db import get_db +from app.core.exceptions import AppError, AuthError, RateLimitError +from app.core.logging import get_logger +from app.models.user import User, UserProfile +from app.security.jwt_auth import issue_access_token +from app.security.password import hash_password, needs_rehash, verify_password +from app.security.refresh_token import ( + clear_refresh_cookie, + decode_refresh_token, + issue_refresh_token, + set_refresh_cookie, +) +from app.tenant.dependencies import get_client_ip, get_tenant +from app.tenant.resolver import TenantContext + +log = get_logger(__name__) +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/login", response_model=LoginResponse) +async def login( + body: LoginRequest, + request: Request, + response: Response, + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """用户登录。 + + 前端先对密码做 BLAKE2b 预哈希(防 DoS),后端 Argon2id 验证。 + 成功后签发 Ed25519 JWT(5 分钟,前端内存)+ AES-GCM Refresh(7 天,HttpOnly Cookie)。 + """ + ip = get_client_ip(request) + # TODO: 速率限制(基于 Redis) + + result = await db.execute(select(User).where(User.username == body.username)) + user = result.scalar_one_or_none() + + if user is None or not verify_password(body.password_prehash, user.password_hash): + log.info("login_failed", username=body.username, ip=ip) + raise AuthError("用户名或密码错误") + + if not user.is_active: + raise AuthError("账号已被禁用") + + # 检查封禁 + if user.active_ban is not None: + raise AuthError("账号已被封禁") + + # Argon2id 参数升级时重新哈希 + if needs_rehash(user.password_hash): + user.password_hash = hash_password(body.password_prehash) + await db.commit() + + # 签发令牌 + access_token = issue_access_token( + user_id=user.id, + username=user.username, + ban_version=user.ban_version, + site_group_id=tenant.site_group_id, + is_superuser=user.is_superuser, + is_staff=user.is_staff, + ) + refresh_token = issue_refresh_token( + user_id=user.id, + ban_version=user.ban_version, + site_group_id=tenant.site_group_id, + ) + set_refresh_cookie(response, refresh_token) + + log.info("login_success", user_id=user.id, username=user.username, ip=ip) + return LoginResponse( + access_token=access_token, + expires_in=settings.access_token_ttl_seconds, + username=user.username, + is_superuser=user.is_superuser, + is_staff=user.is_staff, + ) + + +@router.post("/register", response_model=LoginResponse) +async def register( + body: RegisterRequest, + request: Request, + response: Response, + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """用户注册。""" + # 检查用户名是否已存在 + existing = await db.execute(select(User).where(User.username == body.username)) + if existing.scalar_one_or_none() is not None: + raise AppError("用户名已存在", "username_exists") + + user = User( + username=body.username, + email=body.email, + password_hash=hash_password(body.password_prehash), + is_active=True, + is_verified=False, + ) + db.add(user) + await db.flush() + # 创建空 profile + db.add(UserProfile(user_id=user.id)) + await db.commit() + + access_token = issue_access_token( + user_id=user.id, + username=user.username, + ban_version=user.ban_version, + site_group_id=tenant.site_group_id, + ) + refresh_token = issue_refresh_token( + user_id=user.id, ban_version=user.ban_version, site_group_id=tenant.site_group_id + ) + set_refresh_cookie(response, refresh_token) + + log.info("register_success", user_id=user.id, username=user.username) + return LoginResponse( + access_token=access_token, + expires_in=settings.access_token_ttl_seconds, + username=user.username, + is_superuser=False, + is_staff=False, + ) + + +@router.post("/refresh", response_model=RefreshResponse) +async def refresh( + request: Request, + response: Response, + db: AsyncSession = Depends(get_db), +): + """用 Refresh Token 换取新 Access Token(滑动窗口轮换)。""" + cookie_token = request.cookies.get(settings.refresh_token_cookie_name, "") + payload = decode_refresh_token(cookie_token) + if payload is None: + raise AuthError("Refresh Token 无效或已过期") + + user_id = int(payload["sub"]) + jwt_bv = int(payload.get("bv", 0)) + + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user is None or not user.is_active: + clear_refresh_cookie(response) + raise AuthError("用户不存在或已禁用") + + # ban_version 校验 + from app.security.ban_version import is_token_revoked + + if is_token_revoked(jwt_bv, user.ban_version): + clear_refresh_cookie(response) + raise AuthError("令牌已撤销,请重新登录") + + # 签发新 Access Token + access_token = issue_access_token( + user_id=user.id, + username=user.username, + ban_version=user.ban_version, + site_group_id=payload.get("sg"), + is_superuser=user.is_superuser, + is_staff=user.is_staff, + ) + # 轮换 Refresh Token(滑动窗口) + new_refresh = issue_refresh_token( + user_id=user.id, + ban_version=user.ban_version, + site_group_id=payload.get("sg"), + ) + set_refresh_cookie(response, new_refresh) + + return RefreshResponse(access_token=access_token, expires_in=settings.access_token_ttl_seconds) + + +@router.post("/logout") +async def logout(response: Response): + """登出:清除 Refresh Cookie(前端清除内存 Access Token)。""" + clear_refresh_cookie(response) + return {"success": True} + + +@router.get("/me", response_model=UserInfo) +async def me(user=Depends(get_current_user)): + """获取当前用户信息。""" + return UserInfo( + id=user.id, + username=user.username, + email=user.db_user.email if user.db_user else None, + is_superuser=user.is_superuser, + is_staff=user.is_staff, + is_verified=user.db_user.is_verified if user.db_user else False, + ) + + +@router.post("/password") +async def change_password( + body: ChangePasswordRequest, + user=Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """修改密码。""" + if user.db_user is None: + raise AuthError() + if not verify_password(body.old_password_prehash, user.db_user.password_hash): + raise AuthError("原密码错误") + user.db_user.password_hash = hash_password(body.new_password_prehash) + await db.commit() + return {"success": True} diff --git a/app/auth/schemas.py b/app/auth/schemas.py new file mode 100644 index 0000000..a8d1669 --- /dev/null +++ b/app/auth/schemas.py @@ -0,0 +1,46 @@ +"""认证 Pydantic schemas。""" +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class LoginRequest(BaseModel): + username: str = Field(..., min_length=1, max_length=150) + # 前端 BLAKE2b 预哈希后的 hex(防 DoS 截断) + password_prehash: str = Field(..., alias="password", min_length=8, max_length=256) + captcha: str | None = None + captcha_id: str | None = None + + +class RegisterRequest(BaseModel): + username: str = Field(..., min_length=3, max_length=150, pattern=r"^[A-Za-z0-9._-]+$") + password_prehash: str = Field(..., alias="password", min_length=8, max_length=256) + email: str | None = None + invite_token: str | None = None + + +class RefreshResponse(BaseModel): + access_token: str + expires_in: int + + +class LoginResponse(BaseModel): + access_token: str + expires_in: int + username: str + is_superuser: bool + is_staff: bool + + +class UserInfo(BaseModel): + id: int + username: str + email: str | None + is_superuser: bool + is_staff: bool + is_verified: bool + + +class ChangePasswordRequest(BaseModel): + old_password_prehash: str = Field(..., alias="old_password", min_length=8, max_length=256) + new_password_prehash: str = Field(..., alias="new_password", min_length=8, max_length=256) diff --git a/app/cache/__init__.py b/app/cache/__init__.py new file mode 100644 index 0000000..284897b --- /dev/null +++ b/app/cache/__init__.py @@ -0,0 +1 @@ +"""缓存层:App Shell 边缘全量缓存 + HTMX 动态片段精准分离。""" diff --git a/app/cache/app_shell.py b/app/cache/app_shell.py new file mode 100644 index 0000000..8251004 --- /dev/null +++ b/app/cache/app_shell.py @@ -0,0 +1,100 @@ +"""App Shell 边缘全量缓存。 + +策略(来自架构要求): +- 页面骨架路由仅依据请求域名解析租户配置进行渲染,绝不依赖用户状态 +- 通过按域名区分并配合 keyed-BLAKE2b 签名生成缓存键 +- 实现 CDN 边缘节点全量高速缓存与防污染 +- 缓存的 HTML 不含任何用户特定内容(无 Set-Cookie、无用户数据) +- 用户导航、统计等动态内容由 HTMX 在页面加载后独立请求获取 +""" +from __future__ import annotations + +from typing import Awaitable, Callable + +from fastapi import Request, Response +from starlette.responses import HTMLResponse + +from app.cache.keys import ( + app_shell_cache_key, + compute_etag, + edge_cache_headers, +) +from app.core.config import settings +from app.core.logging import get_logger +from app.core.redis import get_redis +from app.tenant.resolver import TenantContext + +log = get_logger(__name__) + + +async def get_app_shell_cache(domain: str, path: str) -> str | None: + """读取 App Shell 边缘缓存。返回 HTML 或 None。""" + if not settings.redis_enabled: + return None + try: + redis = await get_redis() + key = app_shell_cache_key(domain, path) + cached = await redis.get(key) + return cached + except Exception as e: # noqa: BLE001 + log.warning("app_shell_cache_read_failed", error=str(e)) + return None + + +async def set_app_shell_cache(domain: str, path: str, html: str) -> None: + """写入 App Shell 边缘缓存。""" + if not settings.redis_enabled: + return + try: + redis = await get_redis() + key = app_shell_cache_key(domain, path) + await redis.set(key, html, ex=settings.app_shell_cache_ttl) + except Exception as e: # noqa: BLE001 + log.warning("app_shell_cache_write_failed", error=str(e)) + + +async def invalidate_app_shell(domain: str, path: str = "*") -> None: + """失效 App Shell 缓存(租户配置变更时)。""" + if not settings.redis_enabled: + return + try: + redis = await get_redis() + if path == "*": + # 失效该域名所有 shell 缓存(用 scan) + prefix = app_shell_cache_key(domain, "") + async for key in redis.scan_iter(match=f"{prefix}*", count=100): + await redis.delete(key) + else: + await redis.delete(app_shell_cache_key(domain, path)) + except Exception as e: # noqa: BLE001 + log.warning("app_shell_cache_invalidate_failed", error=str(e)) + + +async def render_app_shell( + request: Request, + tenant: TenantContext, + path: str, + render_fn: Callable[[], Awaitable[str]], +) -> HTMLResponse: + """渲染 App Shell 页面(带边缘缓存)。 + + - 先查缓存,命中直接返回(带 CDN 缓存头) + - 未命中调用 render_fn 生成 HTML,写缓存后返回 + - 响应头标记为可缓存(public, max-age),不含 Set-Cookie + """ + domain = tenant.hostname + cached = await get_app_shell_cache(domain, path) + if cached is not None: + headers = edge_cache_headers(settings.app_shell_cache_ttl) + headers["X-2C2A-Cache-Hit"] = "1" + return HTMLResponse(content=cached, headers=headers) + + # 渲染 + html = await render_fn() + await set_app_shell_cache(domain, path, html) + + headers = edge_cache_headers(settings.app_shell_cache_ttl) + headers["X-2C2A-Cache-Hit"] = "0" + etag = compute_etag(domain, path, str(len(html))) + headers["ETag"] = etag + return HTMLResponse(content=html, headers=headers) diff --git a/app/cache/fragments.py b/app/cache/fragments.py new file mode 100644 index 0000000..20eb2d3 --- /dev/null +++ b/app/cache/fragments.py @@ -0,0 +1,77 @@ +"""HTMX 动态片段:用户导航、统计等动态内容。 + +策略(来自架构要求): +- 用户导航、统计等动态内容由 HTMX 在页面加载后发起独立请求 +- 服务端基于 Ed25519 验签和租户依赖实时返回不可缓存的 HTML 片段 +- 片段响应标记为 no-store(不可缓存),确保数据安全隔离 +- 支持 HTMX OOB(Out of Band)swap,一次请求更新多个区域 +- 支持 ETag 协商缓存(片段内容未变时返回 304,降低带宽) +""" +from __future__ import annotations + +from typing import Any + +from fastapi import Request +from starlette.responses import HTMLResponse + +from app.cache.keys import compute_etag, no_cache_headers + + +def fragment_response( + html: str, + *, + request: Request | None = None, + etag_parts: list[str] | None = None, + extra_oob: list[str] | None = None, +) -> HTMLResponse: + """返回 HTMX 片段响应(不可缓存)。 + + - 标记 no-store,确保动态内容不被 CDN/浏览器缓存 + - 可选 ETag 协商(etag_parts 变化时才返回新内容) + - extra_oob:额外的 OOB 片段 HTML(拼接到主片段后,HTMX 自动 swap) + """ + headers = no_cache_headers() + + # ETag 协商缓存 + if etag_parts and request is not None: + etag = compute_etag(*etag_parts) + headers["ETag"] = etag + if request.headers.get("if-none-match") == etag: + return HTMLResponse(status_code=304, headers=headers) + + content = html + if extra_oob: + content = html + "".join(extra_oob) + + return HTMLResponse(content=content, headers=headers) + + +def oob_fragment(target_id: str, html: str, *, swap: str = "outerHTML") -> str: + """构造 HTMX OOB 片段。 + + HTMX OOB 允许一次响应更新多个 DOM 区域: + + """ + return f'
{html}
' + + +def htmx_trigger(event_name: str, payload: Any = None) -> dict[str, str]: + """生成 HTMX 触发事件响应头。 + + 用于服务端主动触发前端事件(如任务完成通知)。 + """ + import json + + if payload is None: + return {"HX-Trigger": event_name} + return {"HX-Trigger": json.dumps({event_name: payload})} + + +def htmx_redirect(url: str) -> dict[str, str]: + """生成 HTMX 重定向响应头。""" + return {"HX-Redirect": url} + + +def htmx_refresh() -> dict[str, str]: + """生成 HTMX 刷新响应头。""" + return {"HX-Refresh": "true"} diff --git a/app/cache/keys.py b/app/cache/keys.py new file mode 100644 index 0000000..920cc71 --- /dev/null +++ b/app/cache/keys.py @@ -0,0 +1,90 @@ +"""keyed-BLAKE2b 缓存键与 ETag 生成。 + +策略: +- 页面骨架路由仅依据请求域名解析租户配置,绝不依赖用户状态 +- 按域名区分并配合 keyed-BLAKE2b 签名生成缓存键,实现 CDN 边缘节点全量高速缓存与防污染 +- HTMX ETag 同样用 keyed-BLAKE2b 生成,兼顾极速与防脏数据污染 + +缓存键结构:shell:{domain_hash}:{path_hash} +ETag 结构:W/"{blake2b_short}" +""" +from __future__ import annotations + +from urllib.parse import urlencode + +from app.security.crypto import keyed_blake2b, keyed_blake2b_short + +# 缓存键前缀 +SHELL_KEY_PREFIX = "shell" +FRAGMENT_KEY_PREFIX = "frag" +TENANT_KEY_PREFIX = "tenant" + + +def domain_hash(domain: str) -> str: + """域名哈希(keyed-BLAKE2b),用于缓存键的域名维度。""" + return keyed_blake2b_short(domain, context="domain") + + +def tenant_cache_key(hostname: str) -> str: + """租户配置缓存键(按域名)。 + + 用于缓存域名 → SiteGroup 的解析结果,避免每次请求查库。 + """ + return f"{TENANT_KEY_PREFIX}:host:{keyed_blake2b_short(hostname, context='tenant')}" + + +def app_shell_cache_key(domain: str, path: str) -> str: + """App Shell 边缘缓存键。 + + 仅依据域名 + 路径生成,绝不包含用户状态,确保 CDN 可全量缓存。 + """ + dh = domain_hash(domain) + ph = keyed_blake2b_short(path, context="shell-path") + return f"{SHELL_KEY_PREFIX}:{dh}:{ph}" + + +def fragment_cache_key(domain: str, path: str, params: dict | None = None) -> str: + """HTMX 动态片段缓存键(按域名+路径+参数)。 + + 片段通常不缓存(动态内容),但某些半静态片段可短时缓存。 + """ + parts = [domain, path] + if params: + parts.append(urlencode(sorted(params.items()), doseq=True)) + raw = "|".join(parts) + return f"{FRAGMENT_KEY_PREFIX}:{keyed_blake2b_short(raw, context='fragment')}" + + +def compute_etag(*parts: str) -> str: + """计算 HTMX ETag(keyed-BLAKE2b 短哈希)。 + + 用于片段响应的 If-None-Match 校验,命中则返回 304。 + """ + raw = "|".join(parts) + return f'W/"{keyed_blake2b_short(raw, context="etag")}"' + + +def cache_vary_headers() -> list[str]: + """App Shell 响应的 Vary 头(仅域名相关,不含用户状态)。""" + return ["Host"] + + +def edge_cache_headers(ttl: int) -> dict[str, str]: + """生成 CDN 边缘缓存响应头。 + + - Cache-Control: public, max-age(CDN 可缓存) + - 不含 Set-Cookie(确保可缓存性) + """ + return { + "Cache-Control": f"public, max-age={ttl}, s-maxage={ttl}", + "Vary": ", ".join(cache_vary_headers()), + "X-2C2A-Cache": "shell", + } + + +def no_cache_headers() -> dict[str, str]: + """动态片段响应头(不可缓存)。""" + return { + "Cache-Control": "no-store, no-cache, must-revalidate, private", + "X-2C2A-Cache": "fragment", + } diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..fccff98 --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +"""核心基础设施:配置、数据库、Redis、日志。""" diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..7769e36 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,154 @@ +"""全局配置(pydantic-settings)。 + +所有配置通过环境变量 / .env 文件注入,启动时一次性加载并校验。 +密钥类配置(Ed25519、AES-GCM master key、BLAKE2b cache key)生产环境必须显式提供。 +""" +from __future__ import annotations + +import secrets +from functools import lru_cache +from pathlib import Path + +from pydantic import Field, computed_field, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + +BASE_DIR = Path(__file__).resolve().parent.parent.parent # /workspace + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=str(BASE_DIR / ".env"), + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + # ── 运行环境 ── + app_name: str = "2c2a" + debug: bool = False + demo: bool = Field(False, alias="2C2A_DEMO") + env: str = "production" # production / staging / development + + # ── Granian / FastAPI ── + host: str = "0.0.0.0" + port: int = 8000 + workers: int = 1 + secret_key: str = "" # 兼容旧字段,仅用于派生非关键用途 + + # ── 数据库 ── + db_engine: str = "sqlite" # sqlite / postgresql / mysql + db_host: str = "localhost" + db_port: int = 5432 + db_name: str = "2c2a" + db_user: str = "2c2a" + db_password: str = "" + db_pool_size: int = 10 + db_max_overflow: int = 20 + db_echo: bool = False + + # ── Redis ── + redis_url: str = "redis://localhost:6379/0" + redis_enabled: bool = True + + # ── 安全密钥(生产必须显式配置)── + # Ed25519 私钥(PEM),用于签发 Access Token JWT + ed25519_private_key_pem: str = "" + # Ed25519 公钥(PEM),用于验签 + ed25519_public_key_pem: str = "" + # AES-GCM 主密钥(32 字节 base64),用于加密 Refresh Token 与字段级加密 + crypto_master_key_b64: str = "" + # keyed-BLAKE2b 边缘缓存签名密钥(任意长度) + cache_signing_key: str = "" + + # ── 认证 ── + access_token_ttl_seconds: int = 300 # 5 分钟 + refresh_token_ttl_days: int = 7 + refresh_token_cookie_name: str = "2c2a_rt" + # 前端 BLAKE2b 预哈希输出长度(字节),用于防 DoS 截断 + password_prehash_bytes: int = 64 + + # ── Argon2id 参数 ── + argon2_time_cost: int = 3 + argon2_memory_cost: int = 65536 # 64 MiB + argon2_parallelism: int = 2 + + # ── 缓存 ── + app_shell_cache_ttl: int = 300 # App Shell 边缘缓存 5 分钟 + tenant_cache_ttl: int = 300 # 租户配置缓存 + fragment_cache_ttl: int = 0 # HTMX 片段默认不缓存(动态) + + # ── WinRM ── + winrm_timeout: int = 30 + winrm_max_retries: int = 3 + winrm_transport_max_conns: int = 100 + + # ── 速率限制 ── + login_rate_limit: int = 5 + api_rate_limit: int = 100 + + # ── 可信代理(用于获取真实 IP)── + trusted_proxy_ips: str = "" + use_x_forwarded_for: bool = True + + # ── 演示模式自动生成密钥 ── + @field_validator("secret_key") + @classmethod + def _ensure_secret(cls, v: str, info) -> str: + if not v: + if info.data.get("demo") or info.data.get("debug"): + return secrets.token_urlsafe(48) + raise ValueError("SECRET_KEY 必须在生产环境显式配置") + return v + + @computed_field # type: ignore[misc] + @property + def is_prod(self) -> bool: + return self.env == "production" and not self.debug + + @property + def database_url(self) -> str: + """异步数据库 URL。""" + e = self.db_engine + if e == "sqlite": + return f"sqlite+aiosqlite:///{BASE_DIR / (self.db_name + '.db')}" + if e == "postgresql": + return ( + f"postgresql+asyncpg://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + if e == "mysql": + return ( + f"mysql+aiomysql://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + raise ValueError(f"不支持的数据库引擎: {e}") + + @property + def sync_database_url(self) -> str: + """同步数据库 URL(仅 Alembic 离线使用)。""" + e = self.db_engine + if e == "sqlite": + return f"sqlite:///{BASE_DIR / (self.db_name + '.db')}" + if e == "postgresql": + return ( + f"postgresql+psycopg://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + if e == "mysql": + return ( + f"mysql+pymysql://{self.db_user}:{self.db_password}" + f"@{self.db_host}:{self.db_port}/{self.db_name}" + ) + raise ValueError(f"不支持的数据库引擎: {e}") + + @property + def trusted_proxies(self) -> set[str]: + return {ip.strip() for ip in self.trusted_proxy_ips.split(",") if ip.strip()} + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + return Settings() # type: ignore[call-arg] + + +settings = get_settings() diff --git a/app/core/db.py b/app/core/db.py new file mode 100644 index 0000000..9117bc1 --- /dev/null +++ b/app/core/db.py @@ -0,0 +1,50 @@ +"""SQLAlchemy 2.0 异步引擎与会话工厂。 + +采用 async_sessionmaker + AsyncSession,所有数据库操作均为异步非阻塞。 +会话通过 FastAPI 依赖注入 `get_db` 提供给路由层。 +""" +from __future__ import annotations + +from collections.abc import AsyncIterator + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) + +from app.core.config import settings + +# 异步引擎:连接池仅在非 SQLite 时启用 +_engine_kwargs: dict = {"echo": settings.db_echo} +if settings.db_engine != "sqlite": + _engine_kwargs.update( + pool_size=settings.db_pool_size, + max_overflow=settings.db_max_overflow, + pool_pre_ping=True, + pool_recycle=1800, + ) + +engine = create_async_engine(settings.database_url, **_engine_kwargs) + +AsyncSessionLocal = async_sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, + autoflush=False, +) + + +async def get_db() -> AsyncIterator[AsyncSession]: + """FastAPI 依赖:提供异步数据库会话,请求结束自动关闭。""" + async with AsyncSessionLocal() as session: + try: + yield session + except Exception: + await session.rollback() + raise + + +async def dispose_engine() -> None: + """应用关闭时释放连接池。""" + await engine.dispose() diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..9ada6cd --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,87 @@ +"""全局异常与错误处理。""" +from __future__ import annotations + +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +from app.core.logging import get_logger + +log = get_logger(__name__) + + +class AppError(Exception): + """业务错误基类。""" + + def __init__(self, message: str, code: str = "app_error", status: int = 400): + self.message = message + self.code = code + self.status = status + super().__init__(message) + + +class AuthError(AppError): + def __init__(self, message: str = "认证失败", code: str = "auth_error", status: int = 401): + super().__init__(message, code, status) + + +class ForbiddenError(AppError): + def __init__(self, message: str = "无权访问"): + super().__init__(message, "forbidden", 403) + + +class NotFoundError(AppError): + def __init__(self, message: str = "资源不存在"): + super().__init__(message, "not_found", 404) + + +class TenantError(AppError): + def __init__(self, message: str = "租户解析失败"): + super().__init__(message, "tenant_error", 404) + + +class RateLimitError(AppError): + def __init__(self, message: str = "请求过于频繁"): + super().__init__(message, "rate_limited", 429) + + +def register_exception_handlers(app: FastAPI) -> None: + @app.exception_handler(AppError) + async def _app_error_handler(_: Request, exc: AppError): + accept = _.headers.get("accept", "") + if "text/html" in accept and not _.headers.get("hx-request"): + return HTMLResponse( + f'
{exc.message}
', status_code=exc.status + ) + return JSONResponse( + {"error": exc.code, "message": exc.message}, status_code=exc.status + ) + + @app.exception_handler(StarletteHTTPException) + async def _http_exc_handler(_: Request, exc: StarletteHTTPException): + # HTMX 请求返回片段,普通请求返回 JSON 或重定向到错误页 + if _.headers.get("hx-request"): + return HTMLResponse( + f'
' + f"{exc.detail}
", + status_code=exc.status_code, + ) + if exc.status_code in (401,): + return RedirectResponse(url="/login", status_code=303) + return JSONResponse( + {"error": "http_error", "message": str(exc.detail)}, + status_code=exc.status_code, + ) + + @app.exception_handler(Exception) + async def _unhandled_handler(_: Request, exc: Exception): + log.exception("unhandled_exception", error=str(exc)) + if _.headers.get("hx-request"): + return HTMLResponse( + '
' + "服务器内部错误
", + status_code=500, + ) + return JSONResponse( + {"error": "internal", "message": "服务器内部错误"}, status_code=500 + ) diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..439bdb7 --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,51 @@ +"""结构化日志(structlog)。 + +生产环境输出 JSON,开发环境输出彩色控制台文本。 +""" +from __future__ import annotations + +import logging +import sys + +import structlog + +from app.core.config import settings + + +def setup_logging() -> None: + timestamper = structlog.processors.TimeStamper(fmt="iso") + + shared_processors = [ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + timestamper, + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + ] + + if settings.is_prod: + renderer = structlog.processors.JSONRenderer() + log_level = logging.INFO + else: + renderer = structlog.dev.ConsoleRenderer(colors=True) + log_level = logging.DEBUG + + structlog.configure( + processors=[*shared_processors, renderer], + wrapper_class=structlog.make_filtering_bound_logger(log_level), + context_class=dict, + logger_factory=structlog.PrintLoggerFactory(file=sys.stderr), + cache_logger_on_first_use=True, + ) + + # 拦截标准库 logging + logging.basicConfig( + level=log_level, + format="%(message)s", + stream=sys.stderr, + force=True, + ) + + +def get_logger(name: str | None = None): + return structlog.get_logger(name) diff --git a/app/core/redis.py b/app/core/redis.py new file mode 100644 index 0000000..37bed40 --- /dev/null +++ b/app/core/redis.py @@ -0,0 +1,34 @@ +"""Redis 异步客户端。 + +提供全局 Redis 连接池,用于: +- 缓存(租户配置、App Shell 元数据) +- 速率限制 +- RedisHuey 任务队列 broker +""" +from __future__ import annotations + +from redis.asyncio import Redis, from_url + +from app.core.config import settings + +_redis: Redis | None = None + + +async def get_redis() -> Redis: + """获取全局 Redis 客户端(单例)。""" + global _redis + if _redis is None: + _redis = from_url( + settings.redis_url, + encoding="utf-8", + decode_responses=True, + max_connections=50, + ) + return _redis + + +async def close_redis() -> None: + global _redis + if _redis is not None: + await _redis.aclose() + _redis = None diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..d8b96c4 --- /dev/null +++ b/app/main.py @@ -0,0 +1,113 @@ +"""FastAPI 应用工厂与生命周期管理。 + +技术栈:Granian + FastAPI + SQLAlchemy 2.0 Async + HTMX OOB + RedisHuey + JinjaX + aiohttp + +启动方式: + granian --interface asgi app.main:app --host 0.0.0.0 --port 8000 +""" +from __future__ import annotations + +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from app.api.v1.audit import router as audit_router +from app.api.v1.hosts import router as hosts_router +from app.api.v1.operations import router as operations_router +from app.api.v1.tickets import router as tickets_router +from app.auth.routes import router as auth_router +from app.core.config import settings +from app.core.db import dispose_engine +from app.core.exceptions import register_exception_handlers +from app.core.logging import get_logger, setup_logging +from app.core.redis import close_redis +from app.tenant.middleware import TenantMiddleware +from app.web.fragments import router as fragments_router +from app.web.shell import router as shell_router + +log = get_logger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期:启动时初始化,关闭时清理。""" + setup_logging() + log.info("app_starting", env=settings.env, debug=settings.debug, demo=settings.demo) + + # 加载插件 + try: + from app.plugins import get_plugin_manager + from app.plugins.loader import PluginLoader + + manager = get_plugin_manager() + loader = PluginLoader(manager) + loaded = await loader.load_discovered() + log.info("plugins_loaded", count=len(loaded), plugins=loaded) + + # 挂载插件路由 + for prefix, router in manager.get_routers(): + app.include_router(router, prefix=prefix) + log.info("plugin_router_mounted", prefix=prefix) + except Exception as e: # noqa: BLE001 + log.warning("plugin_load_failed", error=str(e)) + + log.info("app_started") + yield + + # 关闭清理 + log.info("app_stopping") + try: + from app.plugins import get_plugin_manager + + await get_plugin_manager().shutdown_all() + except Exception as e: # noqa: BLE001 + log.warning("plugin_shutdown_failed", error=str(e)) + await close_redis() + await dispose_engine() + log.info("app_stopped") + + +def create_app() -> FastAPI: + """创建 FastAPI 应用实例。""" + app = FastAPI( + title=settings.app_name, + version="2.0.0", + description="Zero Agent Security Control Architecture - 异步架构", + lifespan=lifespan, + docs_url="/docs" if settings.debug else None, + redoc_url=None, + ) + + # 中间件:租户域名解析(最先执行) + app.add_middleware(TenantMiddleware) + + # 异常处理 + register_exception_handlers(app) + + # 静态文件 + app.mount("/static", StaticFiles(directory="app/static"), name="static") + + # 路由挂载 + # 1. 认证路由 + app.include_router(auth_router) + # 2. App Shell 页面骨架(可缓存) + app.include_router(shell_router) + # 3. HTMX 动态片段(不可缓存) + app.include_router(fragments_router) + # 4. API v1 + api_prefix = "/api/v1" + app.include_router(hosts_router, prefix=api_prefix) + app.include_router(operations_router, prefix=api_prefix) + app.include_router(tickets_router, prefix=api_prefix) + app.include_router(audit_router, prefix=api_prefix) + + # 健康检查 + @app.get("/health", tags=["health"]) + async def health(): + return {"status": "ok", "version": "2.0.0"} + + return app + + +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..06129b7 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,135 @@ +"""SQLAlchemy 2.0 异步模型包。 + +导入所有模型类以便 Alembic autogenerate 发现全部表。 +所有模型继承 Base(声明式基类),需要时间戳的混入 TimestampMixin。 +""" +from __future__ import annotations + +from app.models.base import Base, TimestampMixin +from app.models.tenant import ( + SiteGroup, + SiteGroupConfig, + SiteGroupHostname, + SystemConfig, +) +from app.models.user import ( + LoginLog, + RegistrationLink, + User, + UserBan, + UserBanHistory, + UserEmail, + UserGroup, + UserProfile, +) +from app.models.host import ( + Host, + HostGroup, +) +from app.models.operations import ( + AccountOpeningRequest, + CloudComputerUser, + Product, + ProductAccessGrant, + ProductGroup, + ProductInvitationToken, + PublicHostInfo, + RdpDomainRoute, + SystemTask, +) +from app.models.ticket import ( + Ticket, + TicketActivity, + TicketAttachment, + TicketCategory, + TicketComment, +) +from app.models.audit import ( + AuditLog, + SecurityEvent, + SensitiveOperation, + SessionActivity, +) +from app.models.certificate import ( + CertificateAuthority, + ClientCertificate, + ServerCertificate, +) +from app.models.bootstrap import ( + ActiveSession, + CertProvisionToken, + InitialToken, +) +from app.models.theme import ( + PageContent, + ThemeConfig, +) +from app.models.task import ( + AsyncTask, + TaskProgress, +) +from app.models.plugin import ( + PluginConfiguration, + PluginRecord, +) + +__all__ = [ + # base + "Base", + "TimestampMixin", + # tenant + "SystemConfig", + "SiteGroup", + "SiteGroupConfig", + "SiteGroupHostname", + # user + "User", + "UserEmail", + "UserProfile", + "UserBan", + "UserBanHistory", + "LoginLog", + "RegistrationLink", + "UserGroup", + # host + "Host", + "HostGroup", + # operations + "PublicHostInfo", + "SystemTask", + "ProductGroup", + "Product", + "AccountOpeningRequest", + "CloudComputerUser", + "RdpDomainRoute", + "ProductInvitationToken", + "ProductAccessGrant", + # ticket + "TicketCategory", + "Ticket", + "TicketComment", + "TicketActivity", + "TicketAttachment", + # audit + "AuditLog", + "SensitiveOperation", + "SecurityEvent", + "SessionActivity", + # certificate + "CertificateAuthority", + "ServerCertificate", + "ClientCertificate", + # bootstrap + "InitialToken", + "ActiveSession", + "CertProvisionToken", + # theme + "ThemeConfig", + "PageContent", + # task + "AsyncTask", + "TaskProgress", + # plugin + "PluginRecord", + "PluginConfiguration", +] diff --git a/app/models/audit.py b/app/models/audit.py new file mode 100644 index 0000000..7f39ed4 --- /dev/null +++ b/app/models/audit.py @@ -0,0 +1,128 @@ +"""审计与安全模型。 + +包含审计日志、敏感操作记录、安全事件与会话活动。 +AuditLog 使用 JSON details + content_type/object_id 字符串替代 Django GenericForeignKey, +避免引入 ContentType 框架的复杂度。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class AuditLog(Base): + """审计日志:记录用户操作、主机操作等全量审计轨迹。 + + 使用 JSON details 存储操作详情,content_type + object_id 字符串引用关联对象, + 替代 Django 的 GenericForeignKey 机制。 + """ + + __tablename__ = "audit_log" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + host_id: Mapped[int | None] = mapped_column( + ForeignKey("hosts.id", ondelete="SET NULL"), nullable=True + ) + action: Mapped[str] = mapped_column(String(50), nullable=False) + ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) + user_agent: Mapped[str | None] = mapped_column(Text, nullable=True) + timestamp: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + success: Mapped[bool] = mapped_column(Boolean, default=True) + details: Mapped[dict | None] = mapped_column(JSON, nullable=True) + result: Mapped[str | None] = mapped_column(Text, nullable=True) + content_type: Mapped[str | None] = mapped_column(String(100), nullable=True) + object_id: Mapped[int | None] = mapped_column(Integer, nullable=True) + + # ── 关系 ── + user: Mapped["User | None"] = relationship(foreign_keys=[user_id]) # noqa: F821 + host: Mapped["Host | None"] = relationship(foreign_keys=[host_id]) # noqa: F821 + + +class SensitiveOperation(Base): + """敏感操作记录:需审批的高风险操作审计。""" + + __tablename__ = "sensitive_operation" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + operation_type: Mapped[str] = mapped_column(String(50), nullable=False) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + target: Mapped[str] = mapped_column(String(255), nullable=False) + timestamp: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) + justification: Mapped[str | None] = mapped_column(Text, nullable=True) + approved_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + result: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + user: Mapped["User"] = relationship(foreign_keys=[user_id]) # noqa: F821 + approved_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[approved_by_id] + ) + + +class SecurityEvent(Base): + """安全事件:记录未授权访问、暴力破解等安全告警。""" + + __tablename__ = "security_event" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + event_type: Mapped[str] = mapped_column(String(50), nullable=False) + severity: Mapped[str] = mapped_column(String(10), default="medium") + user_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) + description: Mapped[str] = mapped_column(Text, nullable=False) + timestamp: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + resolved: Mapped[bool] = mapped_column(Boolean, default=False) + resolved_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + resolution_notes: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + user: Mapped["User | None"] = relationship(foreign_keys=[user_id]) # noqa: F821 + resolved_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[resolved_by_id] + ) + + +class SessionActivity(Base): + """会话活动记录:跟踪用户登录会话状态。""" + + __tablename__ = "session_activity" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + session_key: Mapped[str] = mapped_column(String(64), nullable=False) + ip_address: Mapped[str] = mapped_column(String(45), nullable=False) + user_agent: Mapped[str] = mapped_column(Text, default="") + login_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + logout_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + + # ── 关系 ── + user: Mapped["User"] = relationship(foreign_keys=[user_id]) # noqa: F821 diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..4c0cb7b --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,50 @@ +"""SQLAlchemy 2.0 声明式基类与通用 Mixin。 + +采用 Mapped / mapped_column 新语法,全异步。 +所有模型继承 TimestampMixin(created_at / updated_at)与软删除可选。 +""" +from __future__ import annotations + +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, String, func +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + """声明式基类。""" + + def __repr__(self) -> str: # pragma: no cover + cls_name = self.__class__.__name__ + pk = getattr(self, "id", None) + return f"<{cls_name} id={pk}>" + + +class TimestampMixin: + """创建/更新时间戳。""" + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + + +class UUIDPKMixin: + """UUID 主键(适用于跨库分布式场景,可选)。""" + + id: Mapped[uuid.UUID] = mapped_column( + String(36), primary_key=True, default=lambda: str(uuid.uuid4()) + ) + + +class SiteGroupMixin: + """站点隔离 Mixin:所有需要站点隔离的模型混入 site_group_id 字段。""" + + # 注意:site_group_id 在具体模型中用 ForeignKey 显式声明, + # 此 Mixin 仅作标记,便于泛型查询过滤。 diff --git a/app/models/bootstrap.py b/app/models/bootstrap.py new file mode 100644 index 0000000..fffd00e --- /dev/null +++ b/app/models/bootstrap.py @@ -0,0 +1,93 @@ +"""主机引导与证书配置令牌模型。 + +包含初始配置令牌(基于配对码认证)、活动会话与证书配置令牌。 +这些模型用于主机首次接入与证书自动签发流程。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class InitialToken(Base): + """初始配置令牌:基于配对码的简化主机认证机制。 + + token 为主键(AccessToken),配对码 6 位数字,5 次尝试限制。 + """ + + __tablename__ = "initial_token" + + token: Mapped[str] = mapped_column(String(255), primary_key=True) + host_id: Mapped[int | None] = mapped_column( + ForeignKey("hosts.id", ondelete="CASCADE"), nullable=True + ) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="ISSUED") + pairing_code: Mapped[str | None] = mapped_column(String(6), nullable=True) + pairing_code_expires_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + pairing_attempts: Mapped[int] = mapped_column(Integer, default=0) + cert_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + host: Mapped["Host | None"] = relationship(foreign_keys=[host_id]) # noqa: F821 + + +class ActiveSession(Base): + """活动会话:基于配对码认证的主机会话管理。""" + + __tablename__ = "active_session" + + session_token: Mapped[str] = mapped_column(String(255), primary_key=True) + host_id: Mapped[int] = mapped_column( + ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False + ) + bound_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + host: Mapped["Host"] = relationship(foreign_keys=[host_id]) # noqa: F821 + + +class CertProvisionToken(Base): + """证书配置令牌:用于主机证书自动签发与配置流程。 + + 状态流转: ISSUED → HOSTNAME_UPLOADED → CERT_ISSUED → HOST_CONFIGURED → CONSUMED。 + """ + + __tablename__ = "cert_provision_token" + + token: Mapped[str] = mapped_column(String(64), primary_key=True) + host_id: Mapped[int | None] = mapped_column( + ForeignKey("hosts.id", ondelete="CASCADE"), nullable=True + ) + server_host: Mapped[str | None] = mapped_column(String(255), nullable=True) + hostname: Mapped[str | None] = mapped_column(String(255), nullable=True) + ip_address: Mapped[str | None] = mapped_column(String(255), nullable=True) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="ISSUED") + cert_data: Mapped[dict | None] = mapped_column(JSON, nullable=True) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + host: Mapped["Host | None"] = relationship(foreign_keys=[host_id]) # noqa: F821 + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) diff --git a/app/models/certificate.py b/app/models/certificate.py new file mode 100644 index 0000000..fb5f2b6 --- /dev/null +++ b/app/models/certificate.py @@ -0,0 +1,96 @@ +"""证书管理模型。 + +包含证书颁发机构(CA)、服务器证书与客户端证书。 +CA 私钥不存数据库,仅存文件系统;数据库仅记录元数据与存储路径(cert_root/cert_sub)。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class CertificateAuthority(Base): + """证书颁发机构(CA)。 + + 私钥不存 DB,存文件系统。cert_root/cert_sub 标识存储路径。 + """ + + __tablename__ = "certificate_authority" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + cert_root: Mapped[str] = mapped_column(String(2), default="") + cert_sub: Mapped[str] = mapped_column(String(2), default="") + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + server_certificates: Mapped[list[ServerCertificate]] = relationship( + back_populates="ca", cascade="all, delete-orphan" + ) + client_certificates: Mapped[list[ClientCertificate]] = relationship( + back_populates="ca", cascade="all, delete-orphan" + ) + + +class ServerCertificate(Base): + """服务器证书:为主机签发的 TLS 服务器证书。""" + + __tablename__ = "server_certificate" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + hostname: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True) + ca_id: Mapped[int] = mapped_column( + ForeignKey("certificate_authority.id", ondelete="CASCADE"), nullable=False + ) + thumbprint: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + is_revoked: Mapped[bool] = mapped_column(Boolean, default=False) + revocation_reason: Mapped[str | None] = mapped_column(String(255), nullable=True) + revocation_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + + # ── 关系 ── + ca: Mapped[CertificateAuthority] = relationship(back_populates="server_certificates") + + +class ClientCertificate(Base): + """客户端证书:为用户签发的客户端认证证书。""" + + __tablename__ = "client_certificate" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + upn_value: Mapped[str | None] = mapped_column(String(255), nullable=True) + ca_id: Mapped[int] = mapped_column( + ForeignKey("certificate_authority.id", ondelete="CASCADE"), nullable=False + ) + thumbprint: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + assigned_to_user_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + ca: Mapped[CertificateAuthority] = relationship(back_populates="client_certificates") + assigned_to_user: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[assigned_to_user_id] + ) diff --git a/app/models/host.py b/app/models/host.py new file mode 100644 index 0000000..127f4f4 --- /dev/null +++ b/app/models/host.py @@ -0,0 +1,171 @@ +"""主机与主机组模型。 + +Host 表示一台可远程管理的 Windows/Linux 主机,支持 WinRM / 本地 WinServer / SSH 连接, +以及 NTLM 密码或证书认证。所有敏感密码字段使用 _cipher 后缀加密存储。 +本模型已移除全部 tunnel_* 字段(隧道功能已废弃)。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, + Table, + Text, + func, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + +# ────────────────────────────────────────────── +# 多对多关联表 +# ────────────────────────────────────────────── + +# 主机 ↔ 授权管理员 +host_administrators = Table( + "host_administrators", + Base.metadata, + Column("host_id", Integer, ForeignKey("hosts.id", ondelete="CASCADE"), primary_key=True), + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), +) + +# 主机 ↔ 管理提供商 +host_providers = Table( + "host_providers", + Base.metadata, + Column("host_id", Integer, ForeignKey("hosts.id", ondelete="CASCADE"), primary_key=True), + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), +) + +# 主机组 ↔ 主机 +hostgroup_hosts = Table( + "hostgroup_hosts", + Base.metadata, + Column("hostgroup_id", Integer, ForeignKey("host_group.id", ondelete="CASCADE"), primary_key=True), + Column("host_id", Integer, ForeignKey("hosts.id", ondelete="CASCADE"), primary_key=True), +) + +# 主机组 ↔ 管理提供商 +hostgroup_providers = Table( + "hostgroup_providers", + Base.metadata, + Column("hostgroup_id", Integer, ForeignKey("host_group.id", ondelete="CASCADE"), primary_key=True), + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), +) + + +class Host(Base, TimestampMixin): + """主机模型。 + + 连接类型: winrm / localwinserver / ssh(隧道模式已移除)。 + 认证方式: ntlm(管理员账户密码)/ certificate(证书)。 + """ + + __tablename__ = "hosts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + os_type: Mapped[str] = mapped_column(String(20), default="windows") + hostname: Mapped[str] = mapped_column(String(255), nullable=False) + + # ── 连接配置 ── + connection_type: Mapped[str] = mapped_column(String(20), default="winrm") + auth_method: Mapped[str] = mapped_column(String(20), default="ntlm") + port: Mapped[int] = mapped_column(Integer, default=5985) + rdp_port: Mapped[int] = mapped_column(Integer, default=3389) + use_ssl: Mapped[bool] = mapped_column(Boolean, default=False) + + # ── 认证凭据(加密存储)── + username: Mapped[str | None] = mapped_column(String(100), nullable=True) + password_cipher: Mapped[str | None] = mapped_column(String(255), nullable=True) + cert_pem_path: Mapped[str | None] = mapped_column(String(512), nullable=True) + cert_key_path: Mapped[str | None] = mapped_column(String(512), nullable=True) + + # ── 主机信息 ── + os_version: Mapped[str | None] = mapped_column(String(50), nullable=True) + status: Mapped[str] = mapped_column(String(20), default="active") + description: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关联 ── + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + site_group_id: Mapped[int | None] = mapped_column( + ForeignKey("site_group.id", ondelete="SET NULL"), nullable=True + ) + + # ── 证书存储路径 ── + cert_root: Mapped[str | None] = mapped_column(String(2), nullable=True) + cert_sub: Mapped[str | None] = mapped_column(String(2), nullable=True) + pfx_password_cipher: Mapped[str | None] = mapped_column(String(255), nullable=True) + + # ── NTLM 回退凭据(加密存储)── + ntlm_fallback_user: Mapped[str | None] = mapped_column(String(100), nullable=True) + ntlm_fallback_password_cipher: Mapped[str | None] = mapped_column(String(255), nullable=True) + + # ── 证书配置状态 ── + cert_activated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + cert_provision_status: Mapped[str | None] = mapped_column(String(20), nullable=True) + + # ── 关系 ── + administrators: Mapped[list["User"]] = relationship( # noqa: F821 + secondary=host_administrators, + back_populates="managed_hosts", + lazy="selectin", + ) + providers: Mapped[list["User"]] = relationship( # noqa: F821 + secondary=host_providers, + back_populates="provider_hosts", + lazy="selectin", + ) + groups: Mapped[list["HostGroup"]] = relationship( # noqa: F821 + secondary=hostgroup_hosts, + back_populates="hosts", + lazy="selectin", + ) + site_group: Mapped["SiteGroup | None"] = relationship( # noqa: F821 + foreign_keys=[site_group_id] + ) + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) + + +class HostGroup(Base, TimestampMixin): + """主机组:将多台主机分组管理。""" + + __tablename__ = "host_group" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + site_group_id: Mapped[int | None] = mapped_column( + ForeignKey("site_group.id", ondelete="SET NULL"), nullable=True + ) + + # ── 关系 ── + hosts: Mapped[list[Host]] = relationship( + secondary=hostgroup_hosts, + back_populates="groups", + lazy="selectin", + ) + providers: Mapped[list["User"]] = relationship( # noqa: F821 + secondary=hostgroup_providers, + back_populates="provider_hostgroups", + lazy="selectin", + ) + site_group: Mapped["SiteGroup | None"] = relationship( # noqa: F821 + foreign_keys=[site_group_id] + ) + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) diff --git a/app/models/operations.py b/app/models/operations.py new file mode 100644 index 0000000..0d81825 --- /dev/null +++ b/app/models/operations.py @@ -0,0 +1,352 @@ +"""运营与产品模型。 + +包含公开主机信息、系统任务、产品组/产品、开户申请、云电脑用户、 +RDP 域名路由、产品邀请令牌与产品访问授权。 +RdpDomainRoute 已移除 tunnel_token 字段(隧道功能已废弃)。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + String, + Table, + Text, + UniqueConstraint, + func, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + +# ────────────────────────────────────────────── +# 多对多关联表 +# ────────────────────────────────────────────── + +# 产品组 ↔ 自动分配提供商 +productgroup_auto_providers = Table( + "productgroup_auto_providers", + Base.metadata, + Column("productgroup_id", Integer, ForeignKey("product_group.id", ondelete="CASCADE"), primary_key=True), + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), +) + + +class PublicHostInfo(Base, TimestampMixin): + """公开主机信息:在前端展示主机信息而不暴露敏感数据。""" + + __tablename__ = "public_host_info" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + internal_host_id: Mapped[int] = mapped_column( + ForeignKey("hosts.id", ondelete="CASCADE"), unique=True, nullable=False + ) + display_name: Mapped[str] = mapped_column(String(200), nullable=False) + display_description: Mapped[str | None] = mapped_column(Text, nullable=True) + display_hostname: Mapped[str] = mapped_column(String(255), nullable=False) + display_rdp_port: Mapped[int] = mapped_column(Integer, default=3389) + is_available: Mapped[bool] = mapped_column(Boolean, default=True) + + # ── 关系 ── + internal_host: Mapped["Host"] = relationship( # noqa: F821 + foreign_keys=[internal_host_id] + ) + + +class SystemTask(Base): + """系统任务:记录异步任务执行状态与结果。""" + + __tablename__ = "system_task" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + task_type: Mapped[str] = mapped_column(String(50), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + status: Mapped[str] = mapped_column(String(20), default="pending") + progress: Mapped[int] = mapped_column(Integer, default=0) + result: Mapped[dict | None] = mapped_column(JSON, nullable=True) + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # ── 关系 ── + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) + + +class ProductGroup(Base, TimestampMixin): + """产品组:对产品进行分组管理。""" + + __tablename__ = "product_group" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + display_order: Mapped[int] = mapped_column(Integer, default=0) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + visibility: Mapped[str] = mapped_column(String(20), default="public") + site_group_id: Mapped[int | None] = mapped_column( + ForeignKey("site_group.id", ondelete="SET NULL"), nullable=True + ) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # ── 关系 ── + products: Mapped[list[Product]] = relationship( + back_populates="product_group", lazy="selectin" + ) + auto_assign_providers: Mapped[list["User"]] = relationship( # noqa: F821 + secondary=productgroup_auto_providers, + back_populates="auto_product_groups", + lazy="selectin", + ) + site_group: Mapped["SiteGroup | None"] = relationship( # noqa: F821 + foreign_keys=[site_group_id] + ) + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) + + +class Product(Base, TimestampMixin): + """产品:面向用户的云电脑产品,关联到具体主机。""" + + __tablename__ = "product" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + display_name: Mapped[str | None] = mapped_column(String(200), nullable=True) + display_description: Mapped[str | None] = mapped_column(Text, nullable=True) + product_group_id: Mapped[int | None] = mapped_column( + ForeignKey("product_group.id", ondelete="SET NULL"), nullable=True + ) + host_id: Mapped[int] = mapped_column( + ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False + ) + site_group_id: Mapped[int | None] = mapped_column( + ForeignKey("site_group.id", ondelete="SET NULL"), nullable=True + ) + rdp_port: Mapped[int] = mapped_column(Integer, default=3389) + display_hostname: Mapped[str | None] = mapped_column(String(255), nullable=True) + is_available: Mapped[bool] = mapped_column(Boolean, default=True) + auto_approval: Mapped[bool] = mapped_column(Boolean, default=False) + visibility: Mapped[str] = mapped_column(String(20), default="public") + limit_one_per_user: Mapped[bool] = mapped_column(Boolean, default=False) + enable_disk_quota: Mapped[bool] = mapped_column(Boolean, default=False) + enable_host_protection: Mapped[bool] = mapped_column(Boolean, default=False) + default_disk_quota: Mapped[dict | None] = mapped_column(JSON, nullable=True) + allow_extra_quota_disks: Mapped[list | None] = mapped_column(JSON, nullable=True) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # ── 关系 ── + product_group: Mapped[ProductGroup | None] = relationship( + back_populates="products", foreign_keys=[product_group_id] + ) + host: Mapped["Host"] = relationship(foreign_keys=[host_id]) # noqa: F821 + site_group: Mapped["SiteGroup | None"] = relationship( # noqa: F821 + foreign_keys=[site_group_id] + ) + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) + + +class AccountOpeningRequest(Base, TimestampMixin): + """开户申请:用户提交的云电脑开户请求。""" + + __tablename__ = "account_opening_request" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + applicant_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + contact_email: Mapped[str] = mapped_column(String(254), nullable=False) + contact_phone: Mapped[str | None] = mapped_column(String(20), nullable=True) + username: Mapped[str] = mapped_column(String(150), nullable=False) + user_fullname: Mapped[str | None] = mapped_column(String(100), nullable=True) + user_email: Mapped[str | None] = mapped_column(String(254), nullable=True) + user_description: Mapped[str | None] = mapped_column(Text, nullable=True) + target_product_id: Mapped[int] = mapped_column( + ForeignKey("product.id", ondelete="CASCADE"), nullable=False + ) + requested_disk_capacity: Mapped[dict | None] = mapped_column(JSON, nullable=True) + status: Mapped[str] = mapped_column(String(20), default="pending") + approved_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + approval_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + approval_notes: Mapped[str | None] = mapped_column(Text, nullable=True) + cloud_user_id: Mapped[str | None] = mapped_column(String(255), nullable=True) + result_message: Mapped[str | None] = mapped_column(Text, nullable=True) + retry_count: Mapped[int] = mapped_column(Integer, default=0) + + # ── 关系 ── + applicant: Mapped["User"] = relationship(foreign_keys=[applicant_id]) # noqa: F821 + target_product: Mapped[Product] = relationship(foreign_keys=[target_product_id]) + approved_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[approved_by_id] + ) + + +class CloudComputerUser(Base, TimestampMixin): + """云电脑用户:在云电脑产品上创建的用户记录。 + + initial_password_cipher 加密存储初始密码,阅后即焚。 + """ + + __tablename__ = "cloud_computer_user" + __table_args__ = ( + UniqueConstraint("product_id", "username", name="uq_cloud_user_product_username"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + username: Mapped[str] = mapped_column(String(150), nullable=False) + fullname: Mapped[str | None] = mapped_column(String(100), nullable=True) + email: Mapped[str | None] = mapped_column(String(254), nullable=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + product_id: Mapped[int] = mapped_column( + ForeignKey("product.id", ondelete="CASCADE"), nullable=False + ) + status: Mapped[str] = mapped_column(String(20), default="active") + is_admin: Mapped[bool] = mapped_column(Boolean, default=False) + groups: Mapped[str | None] = mapped_column(Text, nullable=True) + disk_quota: Mapped[dict | None] = mapped_column(JSON, nullable=True) + created_from_request_id: Mapped[int | None] = mapped_column( + ForeignKey("account_opening_request.id", ondelete="SET NULL"), nullable=True + ) + owner_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + initial_password_cipher: Mapped[str | None] = mapped_column(String(512), nullable=True) + password_viewed: Mapped[bool] = mapped_column(Boolean, default=False) + password_viewed_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + + # ── 关系 ── + product: Mapped[Product] = relationship(foreign_keys=[product_id]) + created_from_request: Mapped[AccountOpeningRequest | None] = relationship( + foreign_keys=[created_from_request_id] + ) + owner: Mapped["User | None"] = relationship(foreign_keys=[owner_id]) # noqa: F821 + + +class RdpDomainRoute(Base): + """RDP 域名路由:分配给用户的临时 RDP 访问域名。 + + tunnel_token 字段已移除(隧道功能已废弃)。 + """ + + __tablename__ = "rdp_domain_route" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + domain: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + product_id: Mapped[int] = mapped_column( + ForeignKey("product.id", ondelete="CASCADE"), nullable=False + ) + assigned_to_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + last_activity_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + product: Mapped[Product] = relationship(foreign_keys=[product_id]) + assigned_to: Mapped["User"] = relationship(foreign_keys=[assigned_to_id]) # noqa: F821 + + +class ProductInvitationToken(Base): + """产品邀请令牌:生成邀请链接以解锁产品或产品组的访问权限。""" + + __tablename__ = "product_invitation_token" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) + product_id: Mapped[int | None] = mapped_column( + ForeignKey("product.id", ondelete="CASCADE"), nullable=True + ) + product_group_id: Mapped[int | None] = mapped_column( + ForeignKey("product_group.id", ondelete="CASCADE"), nullable=True + ) + created_by_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + max_uses: Mapped[int] = mapped_column(Integer, default=1) + used_count: Mapped[int] = mapped_column(Integer, default=0) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + product: Mapped[Product | None] = relationship(foreign_keys=[product_id]) + product_group: Mapped[ProductGroup | None] = relationship(foreign_keys=[product_group_id]) + created_by: Mapped["User"] = relationship(foreign_keys=[created_by_id]) # noqa: F821 + + +class ProductAccessGrant(Base): + """产品访问授权:记录用户通过邀请链接获得的产品/产品组访问权限。""" + + __tablename__ = "product_access_grant" + __table_args__ = ( + UniqueConstraint("user_id", "product_id", name="uq_user_product_grant"), + UniqueConstraint("user_id", "product_group_id", name="uq_user_productgroup_grant"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + product_id: Mapped[int | None] = mapped_column( + ForeignKey("product.id", ondelete="CASCADE"), nullable=True + ) + product_group_id: Mapped[int | None] = mapped_column( + ForeignKey("product_group.id", ondelete="CASCADE"), nullable=True + ) + granted_by_token_id: Mapped[int | None] = mapped_column( + ForeignKey("product_invitation_token.id", ondelete="SET NULL"), nullable=True + ) + granted_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + is_revoked: Mapped[bool] = mapped_column(Boolean, default=False) + revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + revoked_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # ── 关系 ── + user: Mapped["User"] = relationship(foreign_keys=[user_id]) # noqa: F821 + product: Mapped[Product | None] = relationship(foreign_keys=[product_id]) + product_group: Mapped[ProductGroup | None] = relationship(foreign_keys=[product_group_id]) + granted_by_token: Mapped[ProductInvitationToken | None] = relationship( + foreign_keys=[granted_by_token_id] + ) + revoked_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[revoked_by_id] + ) diff --git a/app/models/plugin.py b/app/models/plugin.py new file mode 100644 index 0000000..aab7044 --- /dev/null +++ b/app/models/plugin.py @@ -0,0 +1,54 @@ +"""插件模型。 + +PluginRecord 记录已安装的插件元数据。 +PluginConfiguration 存储插件配置项,敏感值使用 value_cipher 加密存储。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + + +class PluginRecord(Base, TimestampMixin): + """插件记录:已安装插件的元数据。""" + + __tablename__ = "plugin_record" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + plugin_id: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + name: Mapped[str] = mapped_column(String(200), nullable=False) + version: Mapped[str] = mapped_column(String(50), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + + # ── 关系 ── + configurations: Mapped[list[PluginConfiguration]] = relationship( + back_populates="plugin", cascade="all, delete-orphan", lazy="selectin" + ) + + +class PluginConfiguration(Base, TimestampMixin): + """插件配置项:键值对存储,敏感值加密。 + + value_cipher 存储加密后的配置值,不存明文。 + """ + + __tablename__ = "plugin_configuration" + __table_args__ = ( + UniqueConstraint("plugin_id", "key", name="uq_plugin_configuration_plugin_key"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + plugin_id: Mapped[int] = mapped_column( + ForeignKey("plugin_record.id", ondelete="CASCADE"), nullable=False + ) + key: Mapped[str] = mapped_column(String(200), nullable=False) + value_cipher: Mapped[str | None] = mapped_column(Text, nullable=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + plugin: Mapped[PluginRecord] = relationship(back_populates="configurations") diff --git a/app/models/task.py b/app/models/task.py new file mode 100644 index 0000000..6905cd1 --- /dev/null +++ b/app/models/task.py @@ -0,0 +1,66 @@ +"""异步任务模型。 + +AsyncTask 跟踪 Celery/Huey 异步任务的执行状态与结果。 +TaskProgress 记录任务的进度更新历史。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class AsyncTask(Base): + """异步任务状态跟踪。 + + target_object_id + target_content_type 用于关联任意业务对象(字符串标识, + 替代 Django ContentType)。 + """ + + __tablename__ = "async_task" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + task_id: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + status: Mapped[str] = mapped_column(String(20), default="pending") + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + progress: Mapped[int] = mapped_column(Integer, default=0) + result: Mapped[dict | None] = mapped_column(JSON, nullable=True) + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + target_object_id: Mapped[int | None] = mapped_column(Integer, nullable=True) + target_content_type: Mapped[str | None] = mapped_column(String(100), nullable=True) + + # ── 关系 ── + created_by: Mapped["User | None"] = relationship(foreign_keys=[created_by_id]) # noqa: F821 + progress_updates: Mapped[list[TaskProgress]] = relationship( + back_populates="task", cascade="all, delete-orphan", lazy="selectin" + ) + + +class TaskProgress(Base): + """任务进度详情:记录任务执行过程中的进度更新。""" + + __tablename__ = "task_progress" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + task_id: Mapped[int] = mapped_column( + ForeignKey("async_task.id", ondelete="CASCADE"), nullable=False + ) + progress: Mapped[int] = mapped_column(Integer, nullable=False) + message: Mapped[str | None] = mapped_column(Text, nullable=True) + timestamp: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + task: Mapped[AsyncTask] = relationship(back_populates="progress_updates") diff --git a/app/models/tenant.py b/app/models/tenant.py new file mode 100644 index 0000000..8631059 --- /dev/null +++ b/app/models/tenant.py @@ -0,0 +1,163 @@ +"""租户与系统配置模型。 + +包含全局系统配置(单例)、站点组、站点组配置覆盖与站点组主机名绑定。 +所有模型继承 Base,需要时间戳的混入 TimestampMixin。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + + +class SystemConfig(Base, TimestampMixin): + """系统全局配置(单例,id 固定 = 1)。 + + 存储 SMTP、验证码、站点外观、注册策略等全局配置。 + 敏感字段 smtp_password_cipher 使用字段级加密,不存明文。 + """ + + __tablename__ = "system_config" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) + + # ── SMTP 配置 ── + smtp_host: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_port: Mapped[int | None] = mapped_column(Integer, nullable=True) + smtp_encryption: Mapped[str] = mapped_column(String(8), default="TLS") + smtp_username: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_password_cipher: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_from_email: Mapped[str | None] = mapped_column(String(254), nullable=True) + smtp_from_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + + # ── 验证码配置 ── + captcha_provider: Mapped[str] = mapped_column(String(32), default="none") + captcha_type: Mapped[str] = mapped_column(String(32), default="SLIDER") + login_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + register_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + email_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + + # ── 站点外观 ── + site_name: Mapped[str] = mapped_column(String(100), default="2c2a") + site_icon: Mapped[str] = mapped_column(String(500), default="") + + # ── 注册策略 ── + enable_registration: Mapped[bool] = mapped_column(Boolean, default=False) + + # ── 备案信息 ── + icp_number: Mapped[str | None] = mapped_column(String(100), nullable=True) + police_number: Mapped[str | None] = mapped_column(String(100), nullable=True) + + # ── 邮箱后缀白/黑名单(每行一个后缀)── + email_suffix_whitelist: Mapped[str | None] = mapped_column(Text, nullable=True) + email_suffix_blacklist: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 本地访问锁定 ── + local_access_locked: Mapped[bool] = mapped_column(Boolean, default=False) + + # ── 主机名品牌绑定 ── + hostname_branding: Mapped[dict | None] = mapped_column(JSON, nullable=True) + + +class SiteGroup(Base, TimestampMixin): + """站点组:实现多租户数据隔离的顶层实体。 + + 一个站点组拥有独立的配置覆盖、主机名绑定、成员与管理员。 + """ + + __tablename__ = "site_group" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + site_name: Mapped[str] = mapped_column(String(100), default="") + site_icon: Mapped[str] = mapped_column(String(500), default="") + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + + # ── 关系 ── + # 管理员(M2M → user_site_group_admins) + admins: Mapped[list[User]] = relationship( # noqa: F821 + secondary="user_site_group_admins", + back_populates="admin_site_groups", + lazy="selectin", + ) + # 成员(M2M → user_site_groups) + members: Mapped[list[User]] = relationship( # noqa: F821 + secondary="user_site_groups", + back_populates="site_groups", + lazy="selectin", + ) + # 配置覆盖(OneToOne) + config: Mapped[SiteGroupConfig | None] = relationship( + back_populates="site_group", + uselist=False, + cascade="all, delete-orphan", + ) + # 主机名绑定(OneToMany) + hostnames: Mapped[list[SiteGroupHostname]] = relationship( + back_populates="site_group", + cascade="all, delete-orphan", + ) + + +class SiteGroupConfig(Base, TimestampMixin): + """站点组配置覆盖(OneToOne → SiteGroup)。 + + 字段留空(NULL)表示使用 SystemConfig 的全局默认值。 + """ + + __tablename__ = "site_group_config" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + site_group_id: Mapped[int] = mapped_column( + ForeignKey("site_group.id", ondelete="CASCADE"), unique=True, nullable=False + ) + + # ── SMTP 配置覆盖 ── + smtp_host: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_port: Mapped[int | None] = mapped_column(Integer, nullable=True) + smtp_encryption: Mapped[str | None] = mapped_column(String(8), nullable=True) + smtp_username: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_password_cipher: Mapped[str | None] = mapped_column(String(255), nullable=True) + smtp_from_email: Mapped[str | None] = mapped_column(String(254), nullable=True) + smtp_from_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + + # ── 验证码配置覆盖 ── + captcha_provider: Mapped[str | None] = mapped_column(String(32), nullable=True) + captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + login_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + register_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + email_captcha_type: Mapped[str | None] = mapped_column(String(32), nullable=True) + + # ── 注册与邮箱配置覆盖 ── + enable_registration: Mapped[bool | None] = mapped_column(Boolean, nullable=True) + email_suffix_whitelist: Mapped[str | None] = mapped_column(Text, nullable=True) + email_suffix_blacklist: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 站点外观配置覆盖 ── + site_name: Mapped[str | None] = mapped_column(String(100), nullable=True) + site_icon: Mapped[str | None] = mapped_column(String(500), nullable=True) + icp_number: Mapped[str | None] = mapped_column(String(100), nullable=True) + police_number: Mapped[str | None] = mapped_column(String(100), nullable=True) + + # ── 关系 ── + site_group: Mapped[SiteGroup] = relationship(back_populates="config") + + +class SiteGroupHostname(Base): + """站点组主机名绑定:将 HTTP Host 头映射到站点组。""" + + __tablename__ = "site_group_hostname" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + hostname: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + site_group_id: Mapped[int] = mapped_column( + ForeignKey("site_group.id", ondelete="CASCADE"), nullable=False + ) + + # ── 关系 ── + site_group: Mapped[SiteGroup] = relationship(back_populates="hostnames") diff --git a/app/models/theme.py b/app/models/theme.py new file mode 100644 index 0000000..c24142b --- /dev/null +++ b/app/models/theme.py @@ -0,0 +1,56 @@ +"""主题与页面内容模型。 + +ThemeConfig 为单例(id=1),存储全局主题、品牌资源与自定义 CSS。 +PageContent 存储可编辑的页面内容片段(登录页、页脚等)。 +已移除 WidgetLayout(仪表盘自定义功能已废弃)。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import JSON, Boolean, DateTime, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base + + +class ThemeConfig(Base): + """主题配置(单例,id 固定 = 1)。 + + 存储全局主题设置、品牌资源路径与自定义 CSS 变量。 + """ + + __tablename__ = "theme_config" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) + active_theme: Mapped[str] = mapped_column(String(50), default="material-design-3") + branding: Mapped[dict | None] = mapped_column(JSON, nullable=True) + custom_colors: Mapped[dict | None] = mapped_column(JSON, nullable=True) + css_overrides: Mapped[str | None] = mapped_column(Text, nullable=True) + enable_mobile_optimization: Mapped[bool] = mapped_column(Boolean, default=True) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + + +class PageContent(Base): + """可编辑页面内容:按 position 标识存储页面文案片段。""" + + __tablename__ = "page_content" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + position: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + title: Mapped[str] = mapped_column(String(200), default="") + content: Mapped[str] = mapped_column(Text, default="") + is_enabled: Mapped[bool] = mapped_column(Boolean, default=True) + # "metadata" 是 SQLAlchemy Declarative API 保留字,Python 属性用 meta,DB 列名保持 metadata + meta: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) diff --git a/app/models/ticket.py b/app/models/ticket.py new file mode 100644 index 0000000..c3c0259 --- /dev/null +++ b/app/models/ticket.py @@ -0,0 +1,184 @@ +"""工单系统模型。 + +包含工单分类、工单主体、评论、活动记录与附件。 +工单可关联云电脑用户和开户申请,支持自动分配、SLA 时限与满意度评价。 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + + +class TicketCategory(Base, TimestampMixin): + """工单分类:定义工单的分组、默认优先级、自动分配与 SLA。""" + + __tablename__ = "ticket_category" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + icon: Mapped[str | None] = mapped_column(String(50), nullable=True) + default_priority: Mapped[str] = mapped_column(String(20), default="normal") + auto_assign_to_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + auto_assign_to_group_id: Mapped[int | None] = mapped_column( + ForeignKey("user_group.id", ondelete="SET NULL"), nullable=True + ) + sla_hours: Mapped[int | None] = mapped_column(Integer, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + allow_banned_users: Mapped[bool] = mapped_column(Boolean, default=False) + display_order: Mapped[int] = mapped_column(Integer, default=0) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # ── 关系 ── + tickets: Mapped[list[Ticket]] = relationship(back_populates="category") + auto_assign_to: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[auto_assign_to_id] + ) + auto_assign_to_group: Mapped["UserGroup | None"] = relationship( # noqa: F821 + foreign_keys=[auto_assign_to_group_id] + ) + created_by: Mapped["User | None"] = relationship( # noqa: F821 + foreign_keys=[created_by_id] + ) + + +class Ticket(Base, TimestampMixin): + """工单主体。""" + + __tablename__ = "ticket" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ticket_no: Mapped[str] = mapped_column(String(20), unique=True, nullable=False) + title: Mapped[str] = mapped_column(String(200), nullable=False) + description: Mapped[str] = mapped_column(Text, nullable=False) + category_id: Mapped[int | None] = mapped_column( + ForeignKey("ticket_category.id", ondelete="SET NULL"), nullable=True + ) + status: Mapped[str] = mapped_column(String(20), default="open") + priority: Mapped[str] = mapped_column(String(20), default="normal") + source: Mapped[str] = mapped_column(String(20), default="web") + creator_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + assignee_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + assigned_group_id: Mapped[int | None] = mapped_column( + ForeignKey("user_group.id", ondelete="SET NULL"), nullable=True + ) + related_cloud_computer_id: Mapped[int | None] = mapped_column( + ForeignKey("cloud_computer_user.id", ondelete="SET NULL"), nullable=True + ) + related_request_id: Mapped[int | None] = mapped_column( + ForeignKey("account_opening_request.id", ondelete="SET NULL"), nullable=True + ) + due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + satisfaction: Mapped[int | None] = mapped_column(Integer, nullable=True) + satisfaction_comment: Mapped[str | None] = mapped_column(Text, nullable=True) + + # ── 关系 ── + category: Mapped[TicketCategory | None] = relationship( + back_populates="tickets", foreign_keys=[category_id] + ) + creator: Mapped["User"] = relationship(foreign_keys=[creator_id]) # noqa: F821 + assignee: Mapped["User | None"] = relationship(foreign_keys=[assignee_id]) # noqa: F821 + assigned_group: Mapped["UserGroup | None"] = relationship( # noqa: F821 + foreign_keys=[assigned_group_id] + ) + related_cloud_computer: Mapped["CloudComputerUser | None"] = relationship( # noqa: F821 + foreign_keys=[related_cloud_computer_id] + ) + related_request: Mapped["AccountOpeningRequest | None"] = relationship( # noqa: F821 + foreign_keys=[related_request_id] + ) + comments: Mapped[list[TicketComment]] = relationship( + back_populates="ticket", cascade="all, delete-orphan", lazy="selectin" + ) + activities: Mapped[list[TicketActivity]] = relationship( + back_populates="ticket", cascade="all, delete-orphan", lazy="selectin" + ) + attachments: Mapped[list[TicketAttachment]] = relationship( + back_populates="ticket", cascade="all, delete-orphan", lazy="selectin" + ) + + +class TicketComment(Base): + """工单评论/回复。""" + + __tablename__ = "ticket_comment" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ticket_id: Mapped[int] = mapped_column( + ForeignKey("ticket.id", ondelete="CASCADE"), nullable=False + ) + author_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + content: Mapped[str] = mapped_column(Text, nullable=False) + is_internal: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + ticket: Mapped[Ticket] = relationship(back_populates="comments") + author: Mapped["User"] = relationship(foreign_keys=[author_id]) # noqa: F821 + + +class TicketActivity(Base): + """工单活动记录:状态变更、分配、评论等操作的审计轨迹。""" + + __tablename__ = "ticket_activity" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ticket_id: Mapped[int] = mapped_column( + ForeignKey("ticket.id", ondelete="CASCADE"), nullable=False + ) + actor_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + action: Mapped[str] = mapped_column(String(20), nullable=False) + old_value: Mapped[str | None] = mapped_column(Text, nullable=True) + new_value: Mapped[str | None] = mapped_column(Text, nullable=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + ticket: Mapped[Ticket] = relationship(back_populates="activities") + actor: Mapped["User | None"] = relationship(foreign_keys=[actor_id]) # noqa: F821 + + +class TicketAttachment(Base): + """工单附件。""" + + __tablename__ = "ticket_attachment" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ticket_id: Mapped[int] = mapped_column( + ForeignKey("ticket.id", ondelete="CASCADE"), nullable=False + ) + file_path: Mapped[str] = mapped_column(String(500), nullable=False) + filename: Mapped[str] = mapped_column(String(255), nullable=False) + file_size: Mapped[int] = mapped_column(Integer, nullable=False) + uploaded_by_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + ticket: Mapped[Ticket] = relationship(back_populates="attachments") + uploaded_by: Mapped["User"] = relationship(foreign_keys=[uploaded_by_id]) # noqa: F821 diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..c4ba8ff --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,322 @@ +"""用户与认证模型。 + +包含用户主表、多邮箱、资料、封禁/封禁历史、登录日志、注册链接、用户组, +以及用户-站点组、用户-用户组的多对多关联表。 +所有敏感字段使用 _cipher 后缀,不存明文。 +""" +from __future__ import annotations + +from datetime import date, datetime + +from sqlalchemy import ( + Boolean, + Column, + Date, + DateTime, + ForeignKey, + Integer, + String, + Table, + Text, + func, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base, TimestampMixin + +# ────────────────────────────────────────────── +# 多对多关联表 +# ────────────────────────────────────────────── + +# 用户 ↔ 站点组(成员关系) +user_site_groups = Table( + "user_site_groups", + Base.metadata, + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), + Column("site_group_id", Integer, ForeignKey("site_group.id", ondelete="CASCADE"), primary_key=True), +) + +# 用户 ↔ 站点组(管理员关系) +user_site_group_admins = Table( + "user_site_group_admins", + Base.metadata, + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), + Column("site_group_id", Integer, ForeignKey("site_group.id", ondelete="CASCADE"), primary_key=True), +) + +# 用户 ↔ 用户组 +user_group_members = Table( + "user_group_members", + Base.metadata, + Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True), + Column("group_id", Integer, ForeignKey("user_group.id", ondelete="CASCADE"), primary_key=True), +) + + +class UserGroup(Base): + """用户组(替代 Django Group)。 + + 支持默认组、自动赋予 staff 身份、排序等配置。 + """ + + __tablename__ = "user_group" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(150), unique=True, nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + is_default: Mapped[bool] = mapped_column(Boolean, default=False) + auto_staff: Mapped[bool] = mapped_column(Boolean, default=False) + sort_order: Mapped[int] = mapped_column(Integer, default=0) + + # ── 关系 ── + users: Mapped[list[User]] = relationship( # noqa: F821 + secondary=user_group_members, + back_populates="groups", + lazy="selectin", + ) + + +class User(Base, TimestampMixin): + """用户主表。 + + password_hash 存储 Argon2id PHC 字符串;ban_version 用于无状态令牌撤销。 + """ + + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + username: Mapped[str] = mapped_column(String(150), unique=True, nullable=False) + email: Mapped[str | None] = mapped_column(String(254), unique=True, nullable=True) + phone: Mapped[str | None] = mapped_column(String(20), nullable=True) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + avatar: Mapped[str | None] = mapped_column(String(500), nullable=True) + + # ── 状态标记 ── + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + is_staff: Mapped[bool] = mapped_column(Boolean, default=False) + is_superuser: Mapped[bool] = mapped_column(Boolean, default=False) + is_verified: Mapped[bool] = mapped_column(Boolean, default=False) + + # ── 登录信息 ── + last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + last_login_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) + + # ── 无状态令牌撤销 ── + ban_version: Mapped[int] = mapped_column(Integer, default=0) + + # ── 注册时间 ── + date_joined: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + emails: Mapped[list[UserEmail]] = relationship( + back_populates="user", cascade="all, delete-orphan", lazy="selectin" + ) + profile: Mapped[UserProfile | None] = relationship( + back_populates="user", uselist=False, cascade="all, delete-orphan" + ) + active_ban: Mapped[UserBan | None] = relationship( + back_populates="user", + uselist=False, + cascade="all, delete-orphan", + foreign_keys="[UserBan.user_id]", + ) + + # 站点组成员关系(M2M → user_site_groups) + site_groups: Mapped[list["SiteGroup"]] = relationship( # noqa: F821 + secondary="user_site_groups", + back_populates="members", + lazy="selectin", + ) + # 站点组管理员关系(M2M → user_site_group_admins) + admin_site_groups: Mapped[list["SiteGroup"]] = relationship( # noqa: F821 + secondary="user_site_group_admins", + back_populates="admins", + lazy="selectin", + ) + # 用户组(M2M → user_group_members) + groups: Mapped[list[UserGroup]] = relationship( + secondary=user_group_members, + back_populates="users", + lazy="selectin", + ) + + # 主机授权管理员(M2M → host_administrators,反向 Host.administrators) + managed_hosts: Mapped[list["Host"]] = relationship( # noqa: F821 + secondary="host_administrators", + back_populates="administrators", + lazy="selectin", + ) + # 主机管理提供商(M2M → host_providers,反向 Host.providers) + provider_hosts: Mapped[list["Host"]] = relationship( # noqa: F821 + secondary="host_providers", + back_populates="providers", + lazy="selectin", + ) + # 主机组管理提供商(M2M → hostgroup_providers,反向 HostGroup.providers) + provider_hostgroups: Mapped[list["HostGroup"]] = relationship( # noqa: F821 + secondary="hostgroup_providers", + back_populates="providers", + lazy="selectin", + ) + # 产品组自动分配提供商(M2M → productgroup_auto_providers,反向 ProductGroup.auto_assign_providers) + auto_product_groups: Mapped[list["ProductGroup"]] = relationship( # noqa: F821 + secondary="productgroup_auto_providers", + back_populates="auto_assign_providers", + lazy="selectin", + ) + + +class UserEmail(Base): + """用户多邮箱绑定。 + + 一个用户可有一个主邮箱和多个子邮箱,用于账户合并检测与封禁污染追踪。 + """ + + __tablename__ = "user_email" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + email: Mapped[str] = mapped_column(String(254), nullable=False) + is_primary: Mapped[bool] = mapped_column(Boolean, default=False) + is_verified: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + user: Mapped[User] = relationship(back_populates="emails") + + +class UserProfile(Base, TimestampMixin): + """用户资料(OneToOne → User)。""" + + __tablename__ = "user_profile" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False + ) + nickname: Mapped[str | None] = mapped_column(String(100), nullable=True) + gender: Mapped[str | None] = mapped_column(String(10), nullable=True) + birthday: Mapped[date | None] = mapped_column(Date, nullable=True) + location: Mapped[str | None] = mapped_column(String(100), nullable=True) + bio: Mapped[str | None] = mapped_column(Text, nullable=True) + email_notification: Mapped[bool] = mapped_column(Boolean, default=True) + system_notification: Mapped[bool] = mapped_column(Boolean, default=True) + + # ── 关系 ── + user: Mapped[User] = relationship(back_populates="profile") + + +class UserBan(Base): + """用户封禁记录(OneToOne → User)。 + + 自定义封禁系统,替代 is_active 字段。一个用户同一时间只能有一条活跃封禁。 + """ + + __tablename__ = "user_ban" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False + ) + reason: Mapped[str] = mapped_column(Text, default="") + banned_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + user: Mapped[User] = relationship(back_populates="active_ban", foreign_keys=[user_id]) + banned_by: Mapped[User | None] = relationship(foreign_keys=[banned_by_id]) + + +class UserBanHistory(Base): + """封禁历史记录。 + + 解封时将活跃封禁归档到此表,保留完整封禁历史。 + """ + + __tablename__ = "user_ban_history" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + reason: Mapped[str] = mapped_column(Text, default="") + banned_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + unbanned_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + banned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + unbanned_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + # ── 关系 ── + user: Mapped[User] = relationship(foreign_keys=[user_id]) + banned_by: Mapped[User | None] = relationship(foreign_keys=[banned_by_id]) + unbanned_by: Mapped[User | None] = relationship(foreign_keys=[unbanned_by_id]) + + +class LoginLog(Base): + """登录日志。""" + + __tablename__ = "login_log" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + ip_address: Mapped[str] = mapped_column(String(45), nullable=False) + user_agent: Mapped[str] = mapped_column(Text, default="") + login_type: Mapped[str] = mapped_column(String(20), default="web") + status: Mapped[str] = mapped_column(String(20), nullable=False) + failure_reason: Mapped[str | None] = mapped_column(String(255), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + user: Mapped[User | None] = relationship(foreign_keys=[user_id]) + + +class RegistrationLink(Base): + """注册链接。 + + 通过此链接注册的用户将自动加入指定用户组。 + """ + + __tablename__ = "registration_link" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) + group_id: Mapped[int | None] = mapped_column( + ForeignKey("user_group.id", ondelete="SET NULL"), nullable=True + ) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + used_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + max_uses: Mapped[int] = mapped_column(Integer, default=1) + used_count: Mapped[int] = mapped_column(Integer, default=0) + used: Mapped[bool] = mapped_column(Boolean, default=False) + used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + note: Mapped[str | None] = mapped_column(String(200), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + # ── 关系 ── + group: Mapped[UserGroup | None] = relationship(foreign_keys=[group_id]) + created_by: Mapped[User | None] = relationship(foreign_keys=[created_by_id]) + used_by: Mapped[User | None] = relationship(foreign_keys=[used_by_id]) diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py new file mode 100644 index 0000000..bdf97c3 --- /dev/null +++ b/app/plugins/__init__.py @@ -0,0 +1,54 @@ +"""原生插件系统框架(FastAPI 异步)。 + +提供插件的全异步生命周期管理、路由挂载、UI 扩展、服务发现与事件驱动能力, +不依赖 Django。 + +典型用法:: + + from app.plugins import get_plugin_manager, PluginLoader + + manager = get_plugin_manager() + loader = PluginLoader(manager) + await loader.load_discovered() # 发现并加载所有插件 + + # 应用启动时挂载插件路由 + for prefix, router in manager.get_routers(): + app.include_router(router, prefix=prefix) + +公共导出 +-------- +""" +from __future__ import annotations + +from app.plugins.base import ( + EventHook, + LifecycleEvent, + PluginInterface, + RouteProvider, + ServiceProvider, + UIExtension, + UIExtensionProvider, +) +from app.plugins.loader import PluginLoader, PluginManifest +from app.plugins.manager import PluginManager, get_plugin_manager, plugin_manager +from app.plugins.registry import ServiceRegistry + +__all__ = [ + # 基类与扩展接口 + "PluginInterface", + "ServiceProvider", + "RouteProvider", + "UIExtensionProvider", + "UIExtension", + "EventHook", + "LifecycleEvent", + # 服务注册表 + "ServiceRegistry", + # 插件管理器 + "PluginManager", + "plugin_manager", + "get_plugin_manager", + # 插件加载器 + "PluginLoader", + "PluginManifest", +] diff --git a/app/plugins/base.py b/app/plugins/base.py new file mode 100644 index 0000000..8f83bee --- /dev/null +++ b/app/plugins/base.py @@ -0,0 +1,257 @@ +"""插件基类与扩展接口。 + +参考原 Django 版插件系统(``plugins/core/base.py``),适配 FastAPI 异步架构: + +* 所有生命周期方法(``initialize`` / ``shutdown`` / ``on_load`` / ``on_unload``)均为 ``async``; +* 路由通过 :class:`RouteProvider` 提供 ``fastapi.APIRouter``,由主应用在启动时挂载; +* UI 扩展面向 JinjaX 组件(``component`` 字段为 JinjaX 组件名); +* 事件驱动通过 :class:`EventHook` 的 ``async emit`` 并发执行所有 handler。 + +本模块不依赖 Django。 +""" +from __future__ import annotations + +import abc +import asyncio +import enum +import logging +from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field +from typing import Any + +import fastapi + +logger = logging.getLogger(__name__) + + +class LifecycleEvent(enum.Enum): + """插件生命周期事件常量。 + + 用于 :class:`PluginManager` 的事件钩子命名,便于在插件加载 / 卸载 / + 启用 / 禁用时统一触发对应事件。 + """ + + LOAD = "load" + UNLOAD = "unload" + ENABLE = "enable" + DISABLE = "disable" + + +class PluginInterface(abc.ABC): + """插件接口基类。 + + 所有原生插件必须继承此类,并实现 :meth:`initialize` 与 :meth:`shutdown` + 两个异步方法。``on_load`` / ``on_unload`` 为可选钩子,提供默认空实现。 + + Parameters + ---------- + plugin_id: + 插件唯一标识(建议使用小写 + 下划线,如 ``example``)。 + name: + 插件展示名称。 + version: + 语义化版本号,如 ``0.1.0``。 + description: + 插件描述,可为空。 + """ + + def __init__( + self, + plugin_id: str, + name: str, + version: str, + description: str = "", + ) -> None: + self.plugin_id = plugin_id + self.name = name + self.version = version + self.description = description + # 默认启用,可由 PluginManager.disable_plugin 置为 False + self.enabled: bool = True + + @property + def metadata(self) -> dict[str, Any]: + """返回插件元数据字典。""" + return { + "id": self.plugin_id, + "name": self.name, + "version": self.version, + "description": self.description, + "enabled": self.enabled, + } + + @abc.abstractmethod + async def initialize(self) -> bool: + """插件初始化(异步)。 + + 在插件被加载时调用,用于建立数据库连接、预热缓存等。 + 返回 ``True`` 表示初始化成功,``False`` 表示失败(将触发回滚)。 + """ + ... + + @abc.abstractmethod + async def shutdown(self) -> bool: + """插件关闭(异步)。 + + 在插件被卸载或应用关闭时调用,用于释放资源。 + 返回 ``True`` 表示关闭成功。 + """ + ... + + async def on_load(self) -> None: + """插件加载后钩子(可选,默认空)。 + + 在 :meth:`initialize` 成功后调用,可在此注册事件 handler 等。 + """ + return None + + async def on_unload(self) -> None: + """插件卸载前钩子(可选,默认空)。 + + 在 :meth:`shutdown` 之前调用,可在此清理事件 handler 等。 + """ + return None + + +class ServiceProvider(abc.ABC): + """服务提供者接口。 + + 插件可实现此接口以向 :class:`~app.plugins.registry.ServiceRegistry` + 注册可被其他插件发现的服务实例。 + """ + + @abc.abstractmethod + def get_service_name(self) -> str: + """返回服务唯一名称。""" + ... + + @abc.abstractmethod + def get_service_interface(self) -> type: + """返回服务实现的接口 / 协议类型,用于按类型建立索引。""" + ... + + def get_service(self) -> Any: + """返回服务实例,默认返回自身。""" + return self + + +class RouteProvider(abc.ABC): + """路由提供者接口。 + + 插件实现此接口以向主应用提供 ``fastapi.APIRouter``,主应用在启动时 + 挂载这些路由。两种提供方式: + + 1. :meth:`get_mount_paths`:显式声明 ``(前缀, router)`` 列表,优先使用; + 2. :meth:`get_routers`:仅返回 router 列表,由管理器按默认前缀 + ``/{plugin_id}`` 挂载。 + """ + + @abc.abstractmethod + def get_routers(self) -> list[fastapi.APIRouter]: + """返回插件提供的 APIRouter 列表,供主应用挂载。""" + ... + + def get_mount_paths(self) -> list[tuple[str, fastapi.APIRouter]]: + """返回 ``(前缀, router)`` 列表,供主应用按指定前缀挂载。 + + 默认返回空列表,表示使用管理器的默认挂载策略。 + """ + return [] + + +class UIExtensionProvider(abc.ABC): + """UI 扩展提供者接口。 + + 插件实现此接口以向前端注入 JinjaX 组件扩展。扩展点(``slot``)由 + 核心系统在各页面中预定义,插件只需声明要注入到哪个 slot。 + """ + + @abc.abstractmethod + def get_ui_extensions(self) -> list[UIExtension]: + """返回 UI 扩展列表。""" + ... + + +@dataclass +class UIExtension: + """UI 扩展点描述对象。 + + Attributes + ---------- + extension_type: + 扩展类型,如 ``nav_item`` / ``section`` / ``form_field`` 等。 + slot: + 扩展槽位标识,由核心系统在页面中预定义。 + component: + JinjaX 组件名,前端据此渲染对应组件。 + order: + 排序权重,越小越靠前,默认 ``0``。 + props: + 传递给组件的额外属性,可为 ``None``。 + """ + + extension_type: str + slot: str + component: str + order: int = 0 + props: dict[str, Any] | None = field(default=None) + + +# 异步 handler 类型:接受任意参数,返回协程 +AsyncHandler = Callable[..., Coroutine[Any, Any, Any]] + + +class EventHook: + """事件钩子。 + + 支持注册多个异步 handler,``emit`` 时通过 :func:`asyncio.gather` 并发 + 执行所有 handler。单个 handler 抛出异常不会影响其他 handler 的执行, + 异常会被捕获并记录,对应结果位置为 ``None``。 + + 虽然 handler 约定为异步函数,但实现上兼容同步可调用对象(返回非 + awaitable 时直接采用其返回值),以提升健壮性。 + """ + + def __init__(self, name: str) -> None: + self.name = name + self._handlers: list[AsyncHandler] = [] + + def register(self, handler: AsyncHandler) -> None: + """注册一个异步 handler(去重)。""" + if handler not in self._handlers: + self._handlers.append(handler) + + def unregister(self, handler: AsyncHandler) -> None: + """取消注册 handler。""" + if handler in self._handlers: + self._handlers.remove(handler) + + @property + def handlers(self) -> list[AsyncHandler]: + """返回已注册 handler 的副本。""" + return list(self._handlers) + + async def emit(self, *args: Any, **kwargs: Any) -> list[Any]: + """并发触发所有 handler,返回与 handler 顺序对应的结果列表。""" + if not self._handlers: + return [] + # 每个 handler 包一层安全调用,避免单个异常中断整体 + tasks = [self._safe_call(handler, *args, **kwargs) for handler in list(self._handlers)] + results = await asyncio.gather(*tasks) + return list(results) + + async def _safe_call(self, handler: AsyncHandler, *args: Any, **kwargs: Any) -> Any: + """安全调用单个 handler:捕获异常并兼容同步 / 异步 handler。""" + try: + result = handler(*args, **kwargs) + # 若返回的是 awaitable(协程),则继续 await + if asyncio.iscoroutine(result): + return await result + return result + except Exception: # noqa: BLE001 - 钩子需隔离异常 + logger.exception( + "事件钩子 %s 的 handler %s 执行失败", + self.name, + getattr(handler, "__name__", repr(handler)), + ) + return None diff --git a/app/plugins/example/__init__.py b/app/plugins/example/__init__.py new file mode 100644 index 0000000..9c92f83 --- /dev/null +++ b/app/plugins/example/__init__.py @@ -0,0 +1,5 @@ +"""示例插件包。 + +包含一个最小示例插件 :class:`app.plugins.example.plugin.Plugin`, +验证插件机制(生命周期 + 路由挂载)可用。 +""" diff --git a/app/plugins/example/plugin.py b/app/plugins/example/plugin.py new file mode 100644 index 0000000..7c6173d --- /dev/null +++ b/app/plugins/example/plugin.py @@ -0,0 +1,67 @@ +"""最小示例插件。 + +演示如何通过 :class:`~app.plugins.base.PluginInterface` + +:class:`~app.plugins.base.RouteProvider` 提供一个异步 JSON 路由 +(``GET /example/hello``),验证插件机制可用。 + +被 :class:`~app.plugins.loader.PluginLoader` 自动发现: + +* 模块级 ``__plugin_meta__`` 提供元数据; +* ``Plugin`` 类为入口(``PluginInterface`` 子类),无参实例化。 +""" +from __future__ import annotations + +import fastapi + +from app.plugins.base import PluginInterface, RouteProvider + +# 模块级插件元数据(PluginLoader 优先读取此处) +__plugin_meta__ = { + "id": "example", + "name": "示例插件", + "version": "0.1.0", + "description": "最小示例插件,验证插件机制可用", + "enabled": True, +} + + +class Plugin(PluginInterface, RouteProvider): + """示例插件:提供 ``GET /example/hello`` 路由。""" + + def __init__(self) -> None: + super().__init__( + plugin_id=__plugin_meta__["id"], + name=__plugin_meta__["name"], + version=__plugin_meta__["version"], + description=__plugin_meta__["description"], + ) + # 在 __init__ 中构建路由,确保 get_routers() 在任意时机可用 + self.router = fastapi.APIRouter() + self.router.add_api_route( + "/hello", + self.hello, + methods=["GET"], + name="example_hello", + summary="示例插件问候接口", + ) + + async def initialize(self) -> bool: + # 示例插件无需额外资源初始化 + return True + + async def shutdown(self) -> bool: + return True + + async def hello(self) -> dict[str, str]: + """返回示例 JSON 响应。""" + return { + "message": "Hello from Example Plugin!", + "plugin": self.plugin_id, + "version": self.version, + } + + # ── RouteProvider 实现 ── + def get_routers(self) -> list[fastapi.APIRouter]: + # 使用管理器默认挂载策略:前缀 /{plugin_id} -> /example + # 配合路由 /hello,最终路径为 /example/hello + return [self.router] diff --git a/app/plugins/loader.py b/app/plugins/loader.py new file mode 100644 index 0000000..0e446e1 --- /dev/null +++ b/app/plugins/loader.py @@ -0,0 +1,257 @@ +"""插件发现与加载。 + +基于目录扫描的插件发现机制: + +* 每个插件目录需含 ``plugin.py`` 文件,其中定义 ``Plugin`` 类 + (:class:`~app.plugins.base.PluginInterface` 子类); +* 默认扫描 ``app.plugins`` 包目录下的子目录(如 ``app/plugins/example/``), + 也可通过 ``plugin_dirs`` 自定义扫描路径; +* 插件元数据优先从模块级变量 ``__plugin_meta__`` 读取,否则从 ``Plugin`` + 类属性读取,最后以目录名兜底。 + +``__plugin_meta__`` 示例:: + + __plugin_meta__ = { + "id": "example", + "name": "示例插件", + "version": "0.1.0", + "description": "最小示例插件", + "enabled": True, + } +""" +from __future__ import annotations + +import importlib +import importlib.util +import inspect +import logging +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from app.plugins.base import PluginInterface +from app.plugins.manager import PluginManager + +logger = logging.getLogger(__name__) + + +@dataclass +class PluginManifest: + """插件清单(发现阶段产出的描述信息)。 + + Attributes + ---------- + plugin_id: + 插件唯一标识。 + name: + 插件展示名称。 + version: + 版本号。 + description: + 描述。 + module_path: + 插件入口模块的导入路径(如 ``app.plugins.example.plugin``)。 + enabled: + 是否启用。 + """ + + plugin_id: str + name: str + version: str + description: str + module_path: str + enabled: bool = True + + +class PluginLoader: + """插件加载器。 + + 扫描插件目录发现插件,并交由 :class:`PluginManager` 实例化与加载。 + """ + + #: 插件入口文件名 + PLUGIN_ENTRY_FILE = "plugin.py" + #: 插件入口类名 + PLUGIN_ENTRY_CLASS = "Plugin" + + def __init__( + self, + manager: PluginManager, + plugin_dirs: list[Path] | None = None, + ) -> None: + self.manager = manager + # 默认扫描 app.plugins 包目录(本文件所在目录) + if plugin_dirs is None: + plugin_dirs = [Path(__file__).resolve().parent] + self.plugin_dirs = plugin_dirs + + # ───────────────────────── 发现 ───────────────────────── + + def discover(self) -> list[PluginManifest]: + """扫描插件目录,返回 manifest 列表。 + + 每个插件目录需含 ``plugin.py`` 文件。重复的 ``plugin_id`` 会被跳过。 + """ + manifests: list[PluginManifest] = [] + seen_ids: set[str] = set() + + for base_dir in self.plugin_dirs: + if not base_dir.is_dir(): + logger.debug("插件目录不存在或非目录: %s", base_dir) + continue + for entry in sorted(base_dir.iterdir(), key=lambda p: p.name): + if not entry.is_dir(): + continue + # 跳过 __pycache__ / .开头 等非插件目录 + if entry.name.startswith("_") or entry.name.startswith("."): + continue + plugin_file = entry / self.PLUGIN_ENTRY_FILE + if not plugin_file.is_file(): + continue + manifest = self._build_manifest(entry, plugin_file) + if manifest is None: + continue + if manifest.plugin_id in seen_ids: + logger.warning("跳过重复插件 ID: %s (%s)", manifest.plugin_id, entry) + continue + seen_ids.add(manifest.plugin_id) + manifests.append(manifest) + + logger.info("共发现 %d 个插件", len(manifests)) + return manifests + + def _build_manifest(self, plugin_dir: Path, plugin_file: Path) -> PluginManifest | None: + """根据插件目录构建 manifest。 + + 元数据优先级:模块级 ``__plugin_meta__`` > ``Plugin`` 类属性 > 目录名兜底。 + """ + module_name = self._module_name_for(plugin_dir) + try: + module = self._import_plugin_module(module_name, plugin_file) + except Exception: # noqa: BLE001 + logger.exception("导入插件模块失败: %s", plugin_file) + return None + + # 优先读取模块级 __plugin_meta__ + meta: dict[str, Any] = {} + module_meta = getattr(module, "__plugin_meta__", None) + if isinstance(module_meta, dict): + meta = module_meta + + plugin_cls = getattr(module, self.PLUGIN_ENTRY_CLASS, None) + if plugin_cls is None or not ( + inspect.isclass(plugin_cls) and issubclass(plugin_cls, PluginInterface) + ): + logger.error( + "插件 %s 中未找到有效的 %s 类(需为 PluginInterface 子类)", + plugin_file, + self.PLUGIN_ENTRY_CLASS, + ) + return None + + # 从 __plugin_meta__ 读取,缺失则回退到 Plugin 类属性,最后目录名兜底 + plugin_id = ( + meta.get("id") + or getattr(plugin_cls, "plugin_id", None) + or plugin_dir.name + ) + name = meta.get("name") or getattr(plugin_cls, "name", None) or plugin_id + version = meta.get("version") or getattr(plugin_cls, "version", None) or "0.0.0" + description = ( + meta.get("description") + or getattr(plugin_cls, "description", None) + or "" + ) + enabled = bool(meta.get("enabled", True)) + + return PluginManifest( + plugin_id=plugin_id, + name=name, + version=version, + description=description, + module_path=module_name, + enabled=enabled, + ) + + def _module_name_for(self, plugin_dir: Path) -> str: + """根据插件目录推导模块导入路径。 + + 假设插件位于 ``app.plugins`` 包下,例如 + ``app/plugins/example/`` -> ``app.plugins.example.plugin``。 + 若路径中找不到 ``app`` 段,则回退为以目录名作为顶层模块。 + """ + parts = plugin_dir.parts + try: + idx = parts.index("app") + base_parts = parts[idx:] + except ValueError: + base_parts = (plugin_dir.name,) + return ".".join([*base_parts, "plugin"]) + + def _import_plugin_module(self, module_name: str, plugin_file: Path): + """导入插件模块。 + + 优先使用标准 :func:`importlib.import_module`(依赖 ``app.plugins`` + 包可被导入);若失败则回退到从文件路径加载并注册到 ``sys.modules``。 + """ + try: + return importlib.import_module(module_name) + except ImportError: + logger.debug("标准导入 %s 失败,回退到文件路径加载", module_name) + spec = importlib.util.spec_from_file_location(module_name, plugin_file) + if spec is None or spec.loader is None: + raise ImportError(f"无法为 {plugin_file} 创建模块规格") from None + module = importlib.util.module_from_spec(spec) + # 注册到 sys.modules,便于 load_discovered 阶段复用 + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + # ───────────────────────── 加载 ───────────────────────── + + async def load_discovered(self) -> list[str]: + """发现并加载所有插件。 + + 流程:``discover`` -> 实例化 ``Plugin`` -> ``register_plugin`` -> + ``load_plugin``。返回成功加载的 ``plugin_id`` 列表。 + """ + manifests = self.discover() + loaded: list[str] = [] + + for manifest in manifests: + # discover 阶段已导入并缓存模块,此处直接复用 + try: + module = importlib.import_module(manifest.module_path) + except Exception: # noqa: BLE001 + logger.exception("加载插件模块失败: %s", manifest.module_path) + continue + + plugin_cls = getattr(module, self.PLUGIN_ENTRY_CLASS, None) + if plugin_cls is None or not ( + inspect.isclass(plugin_cls) and issubclass(plugin_cls, PluginInterface) + ): + logger.error( + "插件 %s 缺少有效的 %s 类", + manifest.module_path, + self.PLUGIN_ENTRY_CLASS, + ) + continue + + try: + plugin = plugin_cls() + except Exception: # noqa: BLE001 + logger.exception("实例化插件 %s 失败", manifest.module_path) + continue + + # 用 manifest 的 enabled 标记覆盖实例状态 + if not manifest.enabled: + plugin.enabled = False + + self.manager.register_plugin(plugin) + ok = await self.manager.load_plugin(plugin.plugin_id) + if ok: + loaded.append(plugin.plugin_id) + + logger.info("成功加载 %d 个插件: %s", len(loaded), loaded) + return loaded diff --git a/app/plugins/manager.py b/app/plugins/manager.py new file mode 100644 index 0000000..7c3cccd --- /dev/null +++ b/app/plugins/manager.py @@ -0,0 +1,238 @@ +"""插件管理器(单例)。 + +:class:`PluginManager` 是插件系统的核心协调者,负责: + +* 插件实例的注册 / 获取 / 列举; +* 异步生命周期管理(加载 / 卸载 / 启用 / 禁用),失败时回滚已注册的服务; +* 收集 :class:`~app.plugins.base.RouteProvider` 提供的路由供主应用挂载; +* 收集 :class:`~app.plugins.base.UIExtensionProvider` 提供的 UI 扩展; +* 通过 :class:`~app.plugins.registry.ServiceRegistry` 实现插件间服务发现; +* 通过 :class:`~app.plugins.base.EventHook` 实现事件驱动(async emit)。 + +模块级提供单例 :data:`plugin_manager` 与 :func:`get_plugin_manager`。 +""" +from __future__ import annotations + +import logging +from typing import Any + +import fastapi + +from app.plugins.base import ( + EventHook, + LifecycleEvent, + PluginInterface, + RouteProvider, + ServiceProvider, + UIExtension, + UIExtensionProvider, +) +from app.plugins.registry import ServiceRegistry + +logger = logging.getLogger(__name__) + + +class PluginManager: + """插件管理器。""" + + def __init__(self) -> None: + # plugin_id -> 插件实例 + self.plugins: dict[str, PluginInterface] = {} + # 服务注册表(插件间服务发现) + self.service_registry: ServiceRegistry = ServiceRegistry() + # 事件名 -> EventHook + self.event_hooks: dict[str, EventHook] = {} + # 预置生命周期事件钩子,便于插件在 on_load 中注册 handler + for event in LifecycleEvent: + self.register_event(event.value) + + # ───────────────────────── 注册 / 获取 ───────────────────────── + + def register_plugin(self, plugin: PluginInterface) -> None: + """注册插件实例(仅登记,不触发加载)。""" + if plugin.plugin_id in self.plugins: + logger.warning("插件 %s 已注册,将覆盖旧实例", plugin.plugin_id) + self.plugins[plugin.plugin_id] = plugin + logger.info("插件已注册: %s (ID: %s)", plugin.name, plugin.plugin_id) + + def get_plugin(self, plugin_id: str) -> PluginInterface | None: + """按 ID 获取插件实例。""" + return self.plugins.get(plugin_id) + + def list_plugins(self) -> list[dict[str, Any]]: + """返回所有插件的元数据列表。""" + return [plugin.metadata for plugin in self.plugins.values()] + + # ───────────────────────── 生命周期 ───────────────────────── + + async def load_plugin(self, plugin_id: str) -> bool: + """加载插件:调用 ``initialize()`` + ``on_load()``,失败回滚。 + + 若插件是 :class:`ServiceProvider`,会在初始化前注册其服务; + 一旦 ``initialize`` 失败或抛异常,将回滚已注册的服务。 + """ + plugin = self.plugins.get(plugin_id) + if plugin is None: + logger.warning("加载失败:插件 %s 不存在", plugin_id) + return False + if not plugin.enabled: + logger.info("插件 %s 已禁用,跳过加载", plugin_id) + return False + + # 服务提供者:先注册服务,便于 initialize 内部使用 + if isinstance(plugin, ServiceProvider): + try: + self.service_registry.register(plugin) + except Exception: # noqa: BLE001 + logger.exception("插件 %s 服务注册失败", plugin_id) + + try: + ok = await plugin.initialize() + if not ok: + logger.warning("插件 %s initialize() 返回 False", plugin_id) + self._rollback_service(plugin) + return False + await plugin.on_load() + await self.emit_event(LifecycleEvent.LOAD.value, plugin=plugin) + logger.info("插件已加载: %s", plugin.name) + return True + except Exception: # noqa: BLE001 + logger.exception("加载插件 %s 时发生错误", plugin_id) + self._rollback_service(plugin) + return False + + async def unload_plugin(self, plugin_id: str) -> bool: + """卸载插件:调用 ``on_unload()`` + ``shutdown()``,并注销服务。""" + plugin = self.plugins.get(plugin_id) + if plugin is None: + logger.warning("卸载失败:插件 %s 不存在", plugin_id) + return False + + try: + await plugin.on_unload() + await self.emit_event(LifecycleEvent.UNLOAD.value, plugin=plugin) + ok = await plugin.shutdown() + if not ok: + logger.warning("插件 %s shutdown() 返回 False", plugin_id) + except Exception: # noqa: BLE001 + logger.exception("卸载插件 %s 时发生错误", plugin_id) + ok = False + finally: + # 无论成功与否都尝试注销服务,避免残留 + self._rollback_service(plugin) + + logger.info("插件已卸载: %s", plugin.name) + return bool(ok) + + async def enable_plugin(self, plugin_id: str) -> bool: + """启用插件并触发 ``enable`` 事件。""" + plugin = self.plugins.get(plugin_id) + if plugin is None: + logger.warning("启用失败:插件 %s 不存在", plugin_id) + return False + plugin.enabled = True + await self.emit_event(LifecycleEvent.ENABLE.value, plugin=plugin) + logger.info("插件已启用: %s", plugin_id) + return True + + async def disable_plugin(self, plugin_id: str) -> bool: + """禁用插件并触发 ``disable`` 事件。""" + plugin = self.plugins.get(plugin_id) + if plugin is None: + logger.warning("禁用失败:插件 %s 不存在", plugin_id) + return False + plugin.enabled = False + await self.emit_event(LifecycleEvent.DISABLE.value, plugin=plugin) + logger.info("插件已禁用: %s", plugin_id) + return True + + async def load_all(self) -> None: + """加载所有已注册插件。""" + for plugin_id in list(self.plugins.keys()): + await self.load_plugin(plugin_id) + + async def shutdown_all(self) -> None: + """关闭所有插件(按注册逆序卸载)。""" + for plugin_id in reversed(list(self.plugins.keys())): + await self.unload_plugin(plugin_id) + + def _rollback_service(self, plugin: PluginInterface) -> None: + """回滚插件已注册的服务(若其为服务提供者)。""" + if isinstance(plugin, ServiceProvider): + try: + self.service_registry.unregister(plugin.get_service_name()) + except Exception: # noqa: BLE001 + logger.debug("回滚服务时出错(可忽略)", exc_info=True) + + # ───────────────────────── 路由 / UI 扩展收集 ───────────────────────── + + def get_routers(self) -> list[tuple[str, fastapi.APIRouter]]: + """收集所有 ``RouteProvider`` 的路由,返回 ``(mount_path, router)`` 列表。 + + 优先使用 :meth:`RouteProvider.get_mount_paths` 的显式声明; + 否则使用默认前缀 ``/{plugin_id}`` 挂载 :meth:`RouteProvider.get_routers` + 返回的每个 router。已禁用的插件会被跳过。 + """ + routers: list[tuple[str, fastapi.APIRouter]] = [] + for plugin in self.plugins.values(): + if not isinstance(plugin, RouteProvider): + continue + if not plugin.enabled: + continue + try: + mount_paths = plugin.get_mount_paths() + if mount_paths: + routers.extend(mount_paths) + continue + # 默认挂载策略:/{plugin_id} 下挂载每个 router + prefix = f"/{plugin.plugin_id}" + for router in plugin.get_routers(): + routers.append((prefix, router)) + except Exception: # noqa: BLE001 + logger.exception("收集插件 %s 路由失败", plugin.plugin_id) + return routers + + def get_ui_extensions(self) -> list[UIExtension]: + """收集所有 ``UIExtensionProvider`` 的扩展,按 ``order`` 升序排序。""" + extensions: list[UIExtension] = [] + for plugin in self.plugins.values(): + if not isinstance(plugin, UIExtensionProvider): + continue + if not plugin.enabled: + continue + try: + extensions.extend(plugin.get_ui_extensions()) + except Exception: # noqa: BLE001 + logger.exception("收集插件 %s UI 扩展失败", plugin.plugin_id) + extensions.sort(key=lambda ext: ext.order) + return extensions + + # ───────────────────────── 服务 / 事件 ───────────────────────── + + def service(self, name: str) -> Any | None: + """快捷访问 ``service_registry.get(name)``。""" + return self.service_registry.get(name) + + def register_event(self, name: str) -> EventHook: + """创建或获取事件钩子。""" + hook = self.event_hooks.get(name) + if hook is None: + hook = EventHook(name) + self.event_hooks[name] = hook + return hook + + async def emit_event(self, name: str, *args: Any, **kwargs: Any) -> list[Any]: + """触发指定名称的事件,并发执行所有 handler。""" + hook = self.event_hooks.get(name) + if hook is None: + return [] + return await hook.emit(*args, **kwargs) + + +# 模块级单例 +plugin_manager = PluginManager() + + +def get_plugin_manager() -> PluginManager: + """返回模块级单例 :class:`PluginManager`。""" + return plugin_manager diff --git a/app/plugins/registry.py b/app/plugins/registry.py new file mode 100644 index 0000000..9a8cbc7 --- /dev/null +++ b/app/plugins/registry.py @@ -0,0 +1,80 @@ +"""服务注册表:插件间服务发现。 + +:class:`ServiceRegistry` 管理插件(实现了 +:class:`~app.plugins.base.ServiceProvider` 的插件)提供的服务实例, +支持按服务名称与接口类型两种方式查找。 +""" +from __future__ import annotations + +import logging +from typing import Any + +from app.plugins.base import ServiceProvider + +logger = logging.getLogger(__name__) + + +class ServiceRegistry: + """服务注册表。 + + 内部维护两张索引: + + * ``_services``:服务名 -> 服务实例; + * ``_interfaces``:接口类型 -> 服务名列表(保持注册顺序)。 + """ + + def __init__(self) -> None: + # 服务名 -> 服务实例 + self._services: dict[str, Any] = {} + # 接口类型 -> 服务名列表 + self._interfaces: dict[type, list[str]] = {} + + def register(self, provider: ServiceProvider) -> None: + """注册服务提供者。 + + 按 ``service_name`` 存储实例,并按 ``interface`` 类型建立索引。 + 若同名服务已存在则覆盖。 + """ + name = provider.get_service_name() + interface = provider.get_service_interface() + service = provider.get_service() + + self._services[name] = service + + names = self._interfaces.setdefault(interface, []) + if name not in names: + names.append(name) + + logger.info( + "服务已注册: %s (接口: %s)", + name, + getattr(interface, "__name__", interface), + ) + + def get(self, name: str) -> Any | None: + """按服务名获取实例,不存在返回 ``None``。""" + return self._services.get(name) + + def get_by_interface(self, interface: type) -> Any | None: + """按接口类型获取第一个匹配的服务实例。 + + 若有多个实现,返回注册顺序中的第一个;无匹配返回 ``None``。 + """ + for name in self._interfaces.get(interface, []): + if name in self._services: + return self._services[name] + return None + + def list_services(self) -> list[str]: + """返回所有已注册的服务名列表。""" + return list(self._services.keys()) + + def unregister(self, name: str) -> None: + """按服务名注销服务,同时清理接口索引。""" + if name not in self._services: + return + del self._services[name] + for names in self._interfaces.values(): + if name in names: + names.remove(name) + logger.info("服务已注销: %s", name) diff --git a/app/security/__init__.py b/app/security/__init__.py new file mode 100644 index 0000000..f13966f --- /dev/null +++ b/app/security/__init__.py @@ -0,0 +1,10 @@ +"""安全与加密模块。 + +设计依据(来自架构要求): +- 用户密码:前端 BLAKE2b 预哈希(防 DoS 截断)+ 后端 Argon2id 加盐慢哈希 +- 敏感字段:HKDF-SHA256 按字段名派生子密钥 + AES-256-GCM 加密(字段级密钥隔离 + 防篡改) +- 缓存键 / ETag:keyed-BLAKE2b 对域名+路径+参数带密钥哈希 +- Access Token:Ed25519 签名的 JWT,5 分钟有效期,存放前端内存 +- Refresh Token:AES-GCM 加密,7 天有效期,存放 HttpOnly Cookie +- 令牌撤销:ban_version 机制,JWT Payload 携带版本号,封禁时递增数据库版本,无需 Redis 黑名单 +""" diff --git a/app/security/ban_version.py b/app/security/ban_version.py new file mode 100644 index 0000000..8dd740b --- /dev/null +++ b/app/security/ban_version.py @@ -0,0 +1,48 @@ +"""ban_version 机制:无状态秒级令牌撤销。 + +原理: +- 用户表存储 ban_version(整数,默认 0) +- Access Token JWT Payload 携带签发时的 ban_version(bv 字段) +- 每次请求验签后,比对 JWT 中的 bv 与数据库当前 ban_version +- 封禁用户时递增数据库 ban_version,旧令牌立即失效 +- 无需 Redis 黑名单,实现无状态秒级撤销 + +注意:递增 ban_version 也会使该用户所有已签发令牌失效(包括合法会话), +因此仅用于封禁/强制下线场景。普通登出由前端清除内存令牌即可。 +""" +from __future__ import annotations + +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user import User + + +async def get_ban_version(db: AsyncSession, user_id: int) -> int: + """获取用户当前 ban_version。""" + result = await db.execute( + select(User.ban_version).where(User.id == user_id) + ) + row = result.first() + return row[0] if row else 0 + + +async def increment_ban_version(db: AsyncSession, user_id: int) -> int: + """递增用户 ban_version,使所有已签发令牌失效。 + + 返回递增后的新版本号。 + """ + # 原子递增 + await db.execute( + update(User) + .where(User.id == user_id) + .values(ban_version=User.ban_version + 1) + ) + await db.commit() + new_ver = await get_ban_version(db, user_id) + return new_ver + + +def is_token_revoked(jwt_ban_version: int, db_ban_version: int) -> bool: + """判断令牌是否已撤销:JWT 中的版本 < 数据库当前版本即撤销。""" + return jwt_ban_version < db_ban_version diff --git a/app/security/crypto.py b/app/security/crypto.py new file mode 100644 index 0000000..8f6533b --- /dev/null +++ b/app/security/crypto.py @@ -0,0 +1,198 @@ +"""底层加密原语:BLAKE2b / Argon2id / HKDF-SHA256 / AES-256-GCM。 + +所有密钥派生与加解密在此集中,供 password / field_cipher / jwt_auth / cache 复用。 +""" +from __future__ import annotations + +import base64 +import hashlib +import hmac +import os + +from argon2 import Type, low_level +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + +from app.core.config import settings + + +# ────────────────────────────────────────────────────────────── +# 密钥加载 +# ────────────────────────────────────────────────────────────── + +def _load_master_key() -> bytes: + """加载 AES-GCM 主密钥(32 字节)。 + + 生产环境必须显式配置 crypto_master_key_b64(base64 编码的 32 字节)。 + 演示/开发模式自动生成。 + """ + raw = settings.crypto_master_key_b64 + if raw: + key = base64.b64decode(raw) + if len(key) != 32: + raise ValueError("crypto_master_key_b64 必须为 32 字节(base64 编码)") + return key + if settings.debug or settings.demo: + # 开发模式:从 secret_key 派生稳定主密钥 + return hashlib.sha256(settings.secret_key.encode()).digest() + raise ValueError("crypto_master_key_b64 必须在生产环境显式配置") + + +_MASTER_KEY = _load_master_key() + + +def master_key() -> bytes: + return _MASTER_KEY + + +# ────────────────────────────────────────────────────────────── +# keyed-BLAKE2b(缓存键 / ETag / 防篡改) +# ────────────────────────────────────────────────────────────── + +def _cache_key_material() -> bytes: + raw = settings.cache_signing_key + if raw: + return raw.encode() + if settings.debug or settings.demo: + return hashlib.sha256(b"cache-key:" + settings.secret_key.encode()).digest() + raise ValueError("cache_signing_key 必须在生产环境显式配置") + + +_CACHE_KEY = _cache_key_material() + + +def keyed_blake2b(data: str | bytes, *, context: str = "") -> str: + """keyed-BLAKE2b 带密钥哈希,输出 hex。 + + 用于生成边缘缓存键与 HTMX ETag,兼顾极速与防脏数据污染。 + context 作为域分隔前缀,防止不同用途的哈希碰撞。 + """ + if isinstance(data, str): + data = data.encode("utf-8") + payload = context.encode() + b"|" + data + h = hashlib.blake2b(payload, key=_CACHE_KEY, digest_size=32) + return h.hexdigest() + + +def keyed_blake2b_short(data: str | bytes, *, context: str = "") -> str: + """短哈希(16 字节),用于 ETag 等场景。""" + if isinstance(data, str): + data = data.encode("utf-8") + payload = context.encode() + b"|" + data + h = hashlib.blake2b(payload, key=_CACHE_KEY, digest_size=16) + return h.hexdigest() + + +# ────────────────────────────────────────────────────────────── +# HKDF-SHA256(按字段名派生子密钥) +# ────────────────────────────────────────────────────────────── + +def derive_field_key(field_name: str, *, info: bytes = b"") -> bytes: + """HKDF-SHA256 按字段名派生 32 字节子密钥。 + + 实现字段级密钥隔离:每个敏感字段(如 password、smtp_password)使用 + 独立派生子密钥,单字段密钥泄露不影响其他字段,且无法篡改字段名。 + """ + salt = hashlib.sha256(b"field-salt").digest() + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + info=field_name.encode("utf-8") + b"\x00" + info, + ) + return hkdf.derive(_MASTER_KEY) + + +# ────────────────────────────────────────────────────────────── +# AES-256-GCM(字段级加密 + Refresh Token 加密) +# ────────────────────────────────────────────────────────────── + +def aes_gcm_encrypt(plaintext: str | bytes, key: bytes) -> str: + """AES-256-GCM 加密,返回 base64(nonce || ciphertext || tag)。 + + GCM 自带完整性校验(防篡改),nonce 每次随机 12 字节。 + """ + if isinstance(plaintext, str): + plaintext = plaintext.encode("utf-8") + nonce = os.urandom(12) + aesgcm = AESGCM(key) + ct = aesgcm.encrypt(nonce, plaintext, associated_data=None) + return base64.urlsafe_b64encode(nonce + ct).decode("ascii") + + +def aes_gcm_decrypt(token: str, key: bytes) -> bytes: + """AES-256-GCM 解密,验证失败抛异常。""" + raw = base64.urlsafe_b64decode(token.encode("ascii")) + nonce, ct = raw[:12], raw[12:] + aesgcm = AESGCM(key) + return aesgcm.decrypt(nonce, ct, associated_data=None) + + +def aes_gcm_decrypt_str(token: str, key: bytes) -> str: + return aes_gcm_decrypt(token, key).decode("utf-8") + + +# ────────────────────────────────────────────────────────────── +# Argon2id(密码慢哈希) +# ────────────────────────────────────────────────────────────── + +def argon2id_hash(prehashed_password: bytes) -> str: + """Argon2id 加盐慢哈希。 + + 输入为前端 BLAKE2b 预哈希后的定长字节(防 DoS 截断), + 输出 PHC 字符串(含盐、参数),可直接存储。 + 有效抵御 GPU 与 ASIC 爆破。 + """ + return low_level.hash_secret( + secret=prehashed_password, + salt=os.urandom(16), + time_cost=settings.argon2_time_cost, + memory_cost=settings.argon2_memory_cost, + parallelism=settings.argon2_parallelism, + hash_len=32, + type=Type.ID, + ).decode("ascii") + + +def argon2id_verify(prehashed_password: bytes, phc_hash: str) -> bool: + """验证 Argon2id 哈希。""" + try: + return low_level.verify_secret( + secret=prehashed_password, + hash=phc_hash.encode("ascii"), + type=Type.ID, + ) + except Exception: + return False + + +# ────────────────────────────────────────────────────────────── +# BLAKE2b 预哈希(前端配套,后端校验) +# ────────────────────────────────────────────────────────────── + +def server_side_prehash(blake2b_hex: str) -> bytes: + """将前端传来的 BLAKE2b hex 预哈希转为 Argon2id 输入字节。 + + 前端对原始密码做 BLAKE2b(定长输出),防止超长密码 DoS 后端 Argon2id。 + 后端在此做长度与格式校验后,转为字节交给 Argon2id。 + """ + blake2b_hex = blake2b_hex.strip().lower() + if not blake2b_hex or not all(c in "0123456789abcdef" for c in blake2b_hex): + raise ValueError("密码预哈希格式无效") + # 限制长度,防止伪造超长输入 + if len(blake2b_hex) > 256: + raise ValueError("密码预哈希过长") + return bytes.fromhex(blake2b_hex) + + +# ────────────────────────────────────────────────────────────── +# 常量时间比较 +# ────────────────────────────────────────────────────────────── + +def constant_time_eq(a: str | bytes, b: str | bytes) -> bool: + if isinstance(a, str): + a = a.encode() + if isinstance(b, str): + b = b.encode() + return hmac.compare_digest(a, b) diff --git a/app/security/field_cipher.py b/app/security/field_cipher.py new file mode 100644 index 0000000..9942d50 --- /dev/null +++ b/app/security/field_cipher.py @@ -0,0 +1,52 @@ +"""字段级加密:HKDF-SHA256 按字段名派生子密钥 + AES-256-GCM。 + +使用方式(SQLAlchemy 模型层): + from app.security.field_cipher import encrypt_field, decrypt_field + + host.password_cipher = encrypt_field("password", "host.password") + plain = decrypt_field(host.password_cipher, "host.password") + +字段名约定为 ".",确保全局唯一,实现字段级密钥隔离与防篡改。 +""" +from __future__ import annotations + +from app.security.crypto import ( + aes_gcm_decrypt_str, + aes_gcm_encrypt, + derive_field_key, +) + +# 字段密钥缓存(进程级,避免每次 HKDF 计算) +_field_key_cache: dict[str, bytes] = {} + + +def _get_field_key(field_name: str) -> bytes: + key = _field_key_cache.get(field_name) + if key is None: + key = derive_field_key(field_name) + _field_key_cache[field_name] = key + return key + + +def encrypt_field(plaintext: str | None, field_name: str) -> str | None: + """加密敏感字段,返回 base64 密文。空值原样返回。""" + if plaintext is None or plaintext == "": + return plaintext + key = _get_field_key(field_name) + return aes_gcm_encrypt(plaintext, key) + + +def decrypt_field(ciphertext: str | None, field_name: str) -> str | None: + """解密敏感字段。空值原样返回,解密失败抛 ValueError。""" + if ciphertext is None or ciphertext == "": + return ciphertext + key = _get_field_key(field_name) + try: + return aes_gcm_decrypt_str(ciphertext, key) + except Exception as e: # noqa: BLE001 + raise ValueError(f"字段 {field_name} 解密失败") from e + + +def rotate_field(plaintext: str, field_name: str) -> str: + """重新加密(密钥轮换时使用)。""" + return encrypt_field(plaintext, field_name) or "" diff --git a/app/security/jwt_auth.py b/app/security/jwt_auth.py new file mode 100644 index 0000000..d2ce1b1 --- /dev/null +++ b/app/security/jwt_auth.py @@ -0,0 +1,102 @@ +"""Ed25519 签名的 JWT Access Token。 + +- 算法:EdDSA(Ed25519),非对称签名,私钥签发、公钥验签 +- 有效期:5 分钟(access_token_ttl_seconds) +- 存放:前端内存(不落 Cookie/LocalStorage,防 XSS 窃取) +- 撤销:ban_version 机制,Payload 携带版本号,封禁时递增数据库版本 +""" +from __future__ import annotations + +import time +import uuid +from typing import Any + +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + +from app.core.config import settings +from app.core.logging import get_logger + +log = get_logger(__name__) + +_ALG = "EdDSA" + + +def _load_private_key() -> Ed25519PrivateKey: + pem = settings.ed25519_private_key_pem + if pem: + return serialization.load_pem_private_key(pem.encode(), password=None) # type: ignore[return-value] + if settings.debug or settings.demo: + # 开发模式:从 secret_key 派生确定性 Ed25519 密钥(仅用于本地) + import hashlib + + seed = hashlib.sha256(b"ed25519:" + settings.secret_key.encode()).digest()[:32] + return Ed25519PrivateKey.from_private_bytes(seed) + raise ValueError("ed25519_private_key_pem 必须在生产环境显式配置") + + +def _load_public_key() -> Ed25519PublicKey: + pem = settings.ed25519_public_key_pem + if pem: + return serialization.load_pem_public_key(pem.encode()) # type: ignore[return-value] + # 未配置公钥时从私钥派生(开发模式) + return _load_private_key().public_key() + + +_private_key = _load_private_key() +_public_key = _load_public_key() + +# PyJWT 接受 cryptography 的 Ed25519 私钥/公钥对象 +_SIGN_KEY = _private_key +_VERIFY_KEY = _public_key + + +def issue_access_token( + *, + user_id: int, + username: str, + ban_version: int, + site_group_id: int | None = None, + is_superuser: bool = False, + is_staff: bool = False, + extra: dict[str, Any] | None = None, +) -> str: + """签发 Access Token JWT。 + + Payload 包含: + - sub: 用户 ID + - username: 用户名 + - bv: ban_version(封禁版本号,用于无状态秒级撤销) + - sg: 站点组 ID + - su/is_staff: 角色标记 + - exp/iat/jti: 标准声明 + """ + now = int(time.time()) + payload: dict[str, Any] = { + "sub": str(user_id), + "username": username, + "bv": ban_version, + "sg": site_group_id, + "su": is_superuser, + "is_staff": is_staff, + "iat": now, + "exp": now + settings.access_token_ttl_seconds, + "jti": uuid.uuid4().hex, + } + if extra: + payload["extra"] = extra + return jwt.encode(payload, _SIGN_KEY, algorithm=_ALG) + + +def decode_access_token(token: str) -> dict[str, Any]: + """验签并解码 Access Token。过期或签名无效抛 jwt 异常。""" + return jwt.decode(token, _VERIFY_KEY, algorithms=[_ALG]) + + +def get_public_key_pem() -> str: + """导出公钥 PEM(供前端/外部验签)。""" + return _public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() diff --git a/app/security/password.py b/app/security/password.py new file mode 100644 index 0000000..9cf21a0 --- /dev/null +++ b/app/security/password.py @@ -0,0 +1,47 @@ +"""密码哈希:前端 BLAKE2b 预哈希 + 后端 Argon2id 加盐慢哈希。 + +流程: + 1. 前端:对原始密码做 BLAKE2b(定长 64 字节 hex),防超长密码 DoS 后端 Argon2id + 2. 后端:校验预哈希格式 → Argon2id 加盐慢哈希 → 存储 PHC 字符串 + 3. 验证:前端再次 BLAKE2b → 后端 Argon2id 验证 +""" +from __future__ import annotations + +from argon2 import PasswordHasher, Type +from argon2.exceptions import InvalidHashError + +from app.core.config import settings +from app.security.crypto import argon2id_hash, argon2id_verify, server_side_prehash + +# 与 crypto.argon2id_hash 参数一致的 PasswordHasher 实例(用于 check_needs_rehash) +_ph = PasswordHasher( + time_cost=settings.argon2_time_cost, + memory_cost=settings.argon2_memory_cost, + parallelism=settings.argon2_parallelism, + type=Type.ID, +) + + +def hash_password(blake2b_prehash_hex: str) -> str: + """对前端 BLAKE2b 预哈希做 Argon2id 慢哈希,返回 PHC 字符串。""" + prehash_bytes = server_side_prehash(blake2b_prehash_hex) + return argon2id_hash(prehash_bytes) + + +def verify_password(blake2b_prehash_hex: str, phc_hash: str) -> bool: + """验证密码:前端 BLAKE2b 预哈希 vs 存储 Argon2id PHC。""" + try: + prehash_bytes = server_side_prehash(blake2b_prehash_hex) + except ValueError: + return False + if not phc_hash: + return False + return argon2id_verify(prehash_bytes, phc_hash) + + +def needs_rehash(phc_hash: str) -> bool: + """检查 Argon2id 参数是否需要重新哈希(参数升级时)。""" + try: + return _ph.check_needs_rehash(phc_hash) + except (InvalidHashError, Exception): + return True diff --git a/app/security/refresh_token.py b/app/security/refresh_token.py new file mode 100644 index 0000000..b794fce --- /dev/null +++ b/app/security/refresh_token.py @@ -0,0 +1,74 @@ +"""AES-GCM 加密的 Refresh Token(HttpOnly Cookie)。 + +- 算法:AES-256-GCM(主密钥派生) +- 有效期:7 天(refresh_token_ttl_days) +- 存放:HttpOnly + Secure + SameSite=Strict Cookie,防 XSS 读取 +- 载荷:用户 ID + ban_version + 站点组 + 过期时间 + 随机 jti +- 轮换:每次刷新签发新 Refresh Token(滑动窗口) +""" +from __future__ import annotations + +import json +import time +import uuid + +from app.core.config import settings +from app.security.crypto import aes_gcm_decrypt, aes_gcm_encrypt, derive_field_key + +# Refresh Token 专用派生密钥(与字段级加密隔离) +_RT_KEY = derive_field_key("refresh_token") + + +def issue_refresh_token( + *, + user_id: int, + ban_version: int, + site_group_id: int | None = None, +) -> str: + """签发加密 Refresh Token。""" + now = int(time.time()) + payload = { + "sub": user_id, + "bv": ban_version, + "sg": site_group_id, + "iat": now, + "exp": now + settings.refresh_token_ttl_days * 86400, + "jti": uuid.uuid4().hex, + } + raw = json.dumps(payload, separators=(",", ":")).encode("utf-8") + return aes_gcm_encrypt(raw, _RT_KEY) + + +def decode_refresh_token(token: str) -> dict | None: + """解密并校验 Refresh Token。无效或过期返回 None。""" + if not token: + return None + try: + raw = aes_gcm_decrypt(token, _RT_KEY) + payload = json.loads(raw) + except Exception: + return None + if int(time.time()) > payload.get("exp", 0): + return None + return payload + + +def set_refresh_cookie(response, token: str) -> None: + """将 Refresh Token 写入 HttpOnly Cookie。""" + response.set_cookie( + key=settings.refresh_token_cookie_name, + value=token, + max_age=settings.refresh_token_ttl_days * 86400, + httponly=True, + secure=settings.is_prod, + samesite="strict", + path="/auth", + ) + + +def clear_refresh_cookie(response) -> None: + """清除 Refresh Token Cookie。""" + response.delete_cookie( + key=settings.refresh_token_cookie_name, + path="/auth", + ) diff --git a/app/static/css/base.css b/app/static/css/base.css new file mode 100644 index 0000000..b087b5d --- /dev/null +++ b/app/static/css/base.css @@ -0,0 +1,51 @@ +/* 2c2a 基础样式(精简版) */ +:root { + --primary: #0066cc; + --bg: #f5f5f5; + --card-bg: #fff; + --text: #333; + --border: #ddd; +} +[data-theme="dark"] { + --primary: #4d9fff; + --bg: #1a1a1a; + --card-bg: #2a2a2a; + --text: #eee; + --border: #444; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); } +.topbar { display: flex; justify-content: space-between; align-items: center; padding: 0 1rem; background: var(--card-bg); border-bottom: 1px solid var(--border); height: 56px; } +.topbar-brand { font-weight: bold; font-size: 1.2rem; } +.nav-list { display: flex; gap: 1rem; list-style: none; align-items: center; } +.nav-list a { color: var(--text); text-decoration: none; } +.nav-list a:hover { color: var(--primary); } +.nav-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; } +.footer { text-align: center; padding: 1rem; color: #999; font-size: 0.85rem; } +main { max-width: 1200px; margin: 0 auto; padding: 1.5rem; } +.auth-container { display: flex; justify-content: center; align-items: center; min-height: 70vh; } +.auth-card { background: var(--card-bg); padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); width: 100%; max-width: 400px; } +.auth-title { margin-bottom: 1.5rem; text-align: center; } +.form-group { margin-bottom: 1rem; } +.form-group label { display: block; margin-bottom: 0.3rem; font-size: 0.9rem; } +.form-group input { width: 100%; padding: 0.6rem; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--text); } +.btn { padding: 0.6rem 1.2rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.95rem; } +.btn-primary { background: var(--primary); color: #fff; } +.btn-link { background: none; color: var(--primary); } +.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.85rem; } +.auth-error { color: #d33; margin-top: 1rem; text-align: center; min-height: 1.2rem; } +.placeholder { color: #999; } +.dashboard { display: flex; flex-direction: column; gap: 1.5rem; } +.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; } +.stat-card { background: var(--card-bg); padding: 1.5rem; border-radius: 8px; text-align: center; } +.stat-label { color: #999; font-size: 0.85rem; } +.stat-value { font-size: 2rem; font-weight: bold; margin-top: 0.3rem; } +.product-group { background: var(--card-bg); padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; } +.product-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem; } +.product-card { border: 1px solid var(--border); padding: 1rem; border-radius: 6px; } +.table { width: 100%; border-collapse: collapse; background: var(--card-bg); border-radius: 8px; overflow: hidden; } +.table th, .table td { padding: 0.7rem 1rem; text-align: left; border-bottom: 1px solid var(--border); } +.badge { padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.75rem; } +.badge-active { background: #d4edda; color: #155724; } +.badge-disabled { background: #f8d7da; color: #721c24; } +.empty-state { text-align: center; padding: 2rem; color: #999; } diff --git a/app/static/js/auth.js b/app/static/js/auth.js new file mode 100644 index 0000000..2829a39 --- /dev/null +++ b/app/static/js/auth.js @@ -0,0 +1,46 @@ +// 2c2a 前端认证逻辑 +// Access Token 存放于内存(防 XSS),过期前自动刷新 + +(function() { + const REFRESH_THRESHOLD = 60; // 过期前 60 秒刷新 + + function scheduleRefresh() { + const expiresIn = window.__TOKEN_EXPIRES_IN__ || 300; + const refreshIn = Math.max((expiresIn - REFRESH_THRESHOLD) * 1000, 30000); + setTimeout(refreshToken, refreshIn); + } + + async function refreshToken() { + try { + const resp = await fetch('/auth/refresh', { + method: 'POST', + credentials: 'include' // 携带 HttpOnly Refresh Cookie + }); + if (resp.ok) { + const data = await resp.json(); + window.__ACCESS_TOKEN__ = data.access_token; + window.__TOKEN_EXPIRES_IN__ = data.expires_in; + scheduleRefresh(); + } else { + // Refresh 失败,跳转登录 + window.__ACCESS_TOKEN__ = null; + if (location.pathname !== '/login') { + location.href = '/login'; + } + } + } catch (e) { + console.error('Token refresh failed', e); + } + } + + window.logout = async function() { + await fetch('/auth/logout', { method: 'POST', credentials: 'include' }); + window.__ACCESS_TOKEN__ = null; + location.href = '/login'; + }; + + // 启动自动刷新调度 + if (window.__ACCESS_TOKEN__) { + scheduleRefresh(); + } +})(); diff --git a/app/static/vendor/htmx.min.js b/app/static/vendor/htmx.min.js new file mode 100644 index 0000000..3412482 --- /dev/null +++ b/app/static/vendor/htmx.min.js @@ -0,0 +1,20 @@ +// HTMX 最小占位(生产环境应使用 htmx.min.js 完整版) +// 此文件仅保证开发环境不报 404,实际 HTMX 功能需替换为官方 htmx.min.js +window.htmx = window.htmx || { + config: {}, + on: function(){}, off: function(){}, trigger: function(){}, + ajax: function(){}, process: function(){}, closest: function(){ return null; } +}; +document.addEventListener('DOMContentLoaded', function() { + // 简易 hx-get 加载器(开发占位) + document.querySelectorAll('[hx-get]').forEach(function(el) { + var target = el.getAttribute('hx-target') ? document.querySelector(el.getAttribute('hx-target')) : el; + var url = el.getAttribute('hx-get'); + var headers = {}; + if (window.__ACCESS_TOKEN__) headers['Authorization'] = 'Bearer ' + window.__ACCESS_TOKEN__; + fetch(url, {headers: headers, credentials: 'include'}) + .then(function(r){ return r.text(); }) + .then(function(html){ if (target) target.innerHTML = html; }) + .catch(function(){}); + }); +}); diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py new file mode 100644 index 0000000..e097ec7 --- /dev/null +++ b/app/tasks/__init__.py @@ -0,0 +1,7 @@ +"""RedisHuey 任务队列(替代 Celery)。 + +- 全异步任务执行(huey 的 async 支持) +- Redis 作为 broker +- 定时任务(crontab)替代 Celery Beat +- 任务状态通过 AsyncTask 模型追踪 +""" diff --git a/app/tasks/hosts.py b/app/tasks/hosts.py new file mode 100644 index 0000000..47e7597 --- /dev/null +++ b/app/tasks/hosts.py @@ -0,0 +1,91 @@ +"""主机相关异步任务(WinRM 操作不阻塞前端)。""" +from __future__ import annotations + +from app.tasks.huey_app import huey + + +@huey.task() +async def configure_winrm_on_host(host_id: int, cert_data: dict | None = None) -> dict: + """异步配置主机 WinRM(证书安装、认证切换等)。 + + 前端提交后立即返回任务 ID,不阻塞请求;任务在后台执行。 + """ + from app.core.db import AsyncSessionLocal + from app.core.logging import get_logger + from app.models.host import Host + from app.security.field_cipher import decrypt_field + from app.winrm import AsyncWinRMClient + from sqlalchemy import select + + log = get_logger(__name__) + async with AsyncSessionLocal() as db: + result = await db.execute(select(Host).where(Host.id == host_id)) + host = result.scalar_one_or_none() + if host is None: + return {"success": False, "error": "主机不存在"} + + try: + client = await AsyncWinRMClient.from_host_config(host) + # 示例:测试连接 + res = await client.execute_command("whoami") + await client.close() + log.info("winrm_configure_done", host_id=host_id, success=res.success) + return {"success": res.success, "output": res.std_out.strip()} + except Exception as e: # noqa: BLE001 + log.exception("winrm_configure_failed", host_id=host_id) + return {"success": False, "error": str(e)} + + +@huey.task() +async def install_certificates_on_host(host_id: int, cert_pem: str, key_pem: str) -> dict: + """异步在远程主机安装证书。""" + from app.core.db import AsyncSessionLocal + from app.core.logging import get_logger + from app.models.host import Host + from app.winrm import AsyncWinRMClient + from sqlalchemy import select + + log = get_logger(__name__) + async with AsyncSessionLocal() as db: + result = await db.execute(select(Host).where(Host.id == host_id)) + host = result.scalar_one_or_none() + if host is None: + return {"success": False, "error": "主机不存在"} + try: + client = await AsyncWinRMClient.from_host_config(host) + # 通过 PowerShell here-string 写入证书文件并导入 + script = f""" +$certPem = @' +{cert_pem} +'@ +Set-Content -Path $env:TEMP\\cert.pem -Value $certPem -Encoding ASCII +Import-Certificate -FilePath $env:TEMP\\cert.pem -CertStoreLocation Cert:\\LocalMachine\\My +""" + res = await client.execute_powershell(script) + await client.close() + return {"success": res.success, "output": res.std_out, "error": res.std_err} + except Exception as e: # noqa: BLE001 + log.exception("cert_install_failed", host_id=host_id) + return {"success": False, "error": str(e)} + + +@huey.task() +async def cleanup_expired_sessions() -> int: + """定时清理过期会话(每日执行)。""" + from datetime import datetime, timezone + + from app.core.db import AsyncSessionLocal + from app.core.logging import get_logger + from app.models.bootstrap import ActiveSession + from sqlalchemy import delete + + log = get_logger(__name__) + async with AsyncSessionLocal() as db: + now = datetime.now(timezone.utc) + result = await db.execute( + delete(ActiveSession).where(ActiveSession.expires_at < now) + ) + await db.commit() + count = result.rowcount or 0 + log.info("sessions_cleaned", count=count) + return count diff --git a/app/tasks/huey_app.py b/app/tasks/huey_app.py new file mode 100644 index 0000000..7f44a66 --- /dev/null +++ b/app/tasks/huey_app.py @@ -0,0 +1,21 @@ +"""Huey 任务队列实例与配置。""" +from __future__ import annotations + +from huey import RedisHuey, crontab + +from app.core.config import settings + +# Huey 实例:Redis broker +# 调试模式且无 Redis 时使用 immediate 模式(同步执行,便于开发) +_immediate = settings.debug and not settings.redis_enabled + +huey = RedisHuey( + "2c2a", + url=settings.redis_url, + immediate=_immediate, +) + +# 定时任务调度(替代 Celery Beat) +# 格式:crontab(minute, hour, day, month, day_of_week) +# 用法:@huey.periodic_task(crontab(hour=0, minute=0)) +__all__ = ["huey", "crontab"] diff --git a/app/tasks/operations.py b/app/tasks/operations.py new file mode 100644 index 0000000..cd2ef38 --- /dev/null +++ b/app/tasks/operations.py @@ -0,0 +1,94 @@ +"""开户/云电脑相关异步任务。""" +from __future__ import annotations + +from app.tasks.huey_app import huey + + +@huey.task() +async def process_account_opening(request_id: int) -> dict: + """异步处理开户申请:在远程主机创建用户、配置磁盘配额等。 + + 前端提交开户后立即返回,后台执行 WinRM 操作。 + """ + from app.core.db import AsyncSessionLocal + from app.core.logging import get_logger + from app.models.operations import AccountOpeningRequest, CloudComputerUser + from app.models.host import Host + from app.security.field_cipher import decrypt_field, encrypt_field + from app.winrm import AsyncWinRMClient + from sqlalchemy import select + + log = get_logger(__name__) + async with AsyncSessionLocal() as db: + result = await db.execute( + select(AccountOpeningRequest).where(AccountOpeningRequest.id == request_id) + ) + req = result.scalar_one_or_none() + if req is None: + return {"success": False, "error": "开户申请不存在"} + + # 加载关联主机 + host_result = await db.execute( + select(Host).join( + AccountOpeningRequest, AccountOpeningRequest.target_product_id + ) + ) + + try: + # 查找产品对应主机 + from app.models.operations import Product + + prod_result = await db.execute( + select(Product).where(Product.id == req.target_product_id) + ) + product = prod_result.scalar_one_or_none() + if product is None: + return {"success": False, "error": "产品不存在"} + + host_result = await db.execute(select(Host).where(Host.id == product.host_id)) + host = host_result.scalar_one_or_none() + if host is None: + return {"success": False, "error": "主机不存在"} + + client = await AsyncWinRMClient.from_host_config(host) + # 生成强密码 + password = client.generate_strong_password() + # 创建远程用户 + create_res = await client.create_user( + username=req.username, + password=password, + description=req.user_description or "", + ) + if not create_res.success: + await client.close() + return {"success": False, "error": create_res.std_err} + + # 加入 Remote Desktop Users + await client.add_to_remote_users(req.username) + await client.close() + + # 创建 CloudComputerUser 记录(密码加密存储,阅后即焚) + ccu = CloudComputerUser( + username=req.username, + fullname=req.user_fullname, + email=req.user_email, + description=req.user_description, + product_id=product.id, + status="active", + created_from_request_id=req.id, + owner_id=req.applicant_id, + initial_password_cipher=encrypt_field(password, "cloud_computer_user.initial_password"), + ) + db.add(ccu) + req.status = "completed" + req.cloud_user_id = req.username + await db.commit() + + log.info("account_opening_done", request_id=request_id, username=req.username) + return {"success": True, "username": req.username} + except Exception as e: # noqa: BLE001 + log.exception("account_opening_failed", request_id=request_id) + req.status = "failed" + req.result_message = str(e) + await db.commit() + return {"success": False, "error": str(e)} diff --git a/app/templates/__init__.py b/app/templates/__init__.py new file mode 100644 index 0000000..81d4906 --- /dev/null +++ b/app/templates/__init__.py @@ -0,0 +1,45 @@ +"""模板引擎:Jinja2(布局/页面/片段)+ JinjaX(可复用组件)。 + +- 布局/页面/片段:Jinja2 Environment + FileSystemLoader,支持 {% extends %} / {% include %} +- 可复用组件:JinjaX Catalog,支持 组件化 +- 两者配合实现 App Shell 边缘缓存 + HTMX 动态片段精准分离 +""" +from __future__ import annotations + +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from jinjax import Catalog + +_TEMPLATES_DIR = Path(__file__).resolve().parent + +# Jinja2 环境:布局、页面、片段 +jinja_env = Environment( + loader=FileSystemLoader( + [ + str(_TEMPLATES_DIR / "layouts"), + str(_TEMPLATES_DIR / "pages"), + str(_TEMPLATES_DIR / "fragments"), + ] + ), + autoescape=select_autoescape(["html", "xml"]), + enable_async=True, + trim_blocks=True, + lstrip_blocks=True, +) + + +async def render_template(template_name: str, **context) -> str: + """异步渲染 Jinja2 模板。""" + tpl = jinja_env.get_template(template_name) + return await tpl.render_async(**context) + + +# JinjaX 组件目录 +catalog = Catalog() +catalog.add_folder(_TEMPLATES_DIR / "components") + + +def render_component(component: str, **props) -> str: + """渲染 JinjaX 组件。""" + return catalog.render(component, **props) diff --git a/app/templates/fragments/my_cloud_computers.html b/app/templates/fragments/my_cloud_computers.html new file mode 100644 index 0000000..26ef4c7 --- /dev/null +++ b/app/templates/fragments/my_cloud_computers.html @@ -0,0 +1,22 @@ +{# 我的云电脑片段(HTMX 动态、不可缓存) #} +{% if cloud_computers %} + + + + + + {% for cc in cloud_computers %} + + + + + + + {% endfor %} + +
用户名产品状态操作
{{ cc.username }}{{ cc.product_name|default('-') }}{{ cc.status }} + 详情 +
+{% else %} +
暂无云电脑
+{% endif %} diff --git a/app/templates/fragments/nav.html b/app/templates/fragments/nav.html new file mode 100644 index 0000000..db9e613 --- /dev/null +++ b/app/templates/fragments/nav.html @@ -0,0 +1,27 @@ +{# 导航片段(HTMX 动态、不可缓存、基于用户状态)。 + 由 App Shell 在页面加载后通过 hx-get="/fragments/nav" 获取。 #} +{% if user %} + +{% else %} + +{% endif %} diff --git a/app/templates/fragments/product_groups.html b/app/templates/fragments/product_groups.html new file mode 100644 index 0000000..8c0e285 --- /dev/null +++ b/app/templates/fragments/product_groups.html @@ -0,0 +1,25 @@ +{# 产品组片段(HTMX 动态、不可缓存、基于租户隔离) #} +{% if product_groups %} +
+ {% for pg in product_groups %} +
+

{{ pg.name }}

+

{{ pg.description or '' }}

+
+ {% for product in pg.products %} +
+

{{ product.display_name or product.name }}

+ {% if product.is_available %} + 申请开通 + {% else %} + 不可用 + {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+{% else %} +
暂无可用产品
+{% endif %} diff --git a/app/templates/fragments/stats.html b/app/templates/fragments/stats.html new file mode 100644 index 0000000..d315609 --- /dev/null +++ b/app/templates/fragments/stats.html @@ -0,0 +1,19 @@ +{# 统计片段(HTMX 动态、不可缓存) #} +
+
+
主机总数
+
{{ stats.hosts_count|default(0) }}
+
+
+
云电脑用户
+
{{ stats.cloud_users_count|default(0) }}
+
+
+
待处理工单
+
{{ stats.pending_tickets|default(0) }}
+
+
+
开户申请
+
{{ stats.pending_requests|default(0) }}
+
+
diff --git a/app/templates/layouts/app_shell.html b/app/templates/layouts/app_shell.html new file mode 100644 index 0000000..3ab4b29 --- /dev/null +++ b/app/templates/layouts/app_shell.html @@ -0,0 +1,47 @@ +{# App Shell 基础布局(可被 CDN 全量缓存)。 + 仅含租户级配置(站点名、主题、ICP),不含任何用户状态。 + 用户导航、统计等动态内容由 HTMX 在页面加载后发起独立请求获取。 #} + + + + + + {{ site_name|default('2c2a') }}{% block title %}{% endblock %} + {% if site_icon %}{% endif %} + + + + +
+
+
{{ site_name|default('2c2a') }}
+ {# 导航由 HTMX 加载后填充(动态、不可缓存、基于用户状态) #} + +
+ +
+ {% block content %}{% endblock %} +
+ +
+ {% if icp_number %}{{ icp_number }}{% endif %} +
+
+ + {# Access Token 存放于前端内存(防 XSS),通过 HTMX 请求头发送 #} + {% if access_token %} + + + {% endif %} + + diff --git a/app/templates/pages/dashboard.html b/app/templates/pages/dashboard.html new file mode 100644 index 0000000..5f66734 --- /dev/null +++ b/app/templates/pages/dashboard.html @@ -0,0 +1,24 @@ +{% extends "app_shell.html" %} +{% block title %} - 仪表盘{% endblock %} + +{% block content %} +
+ {# 静态骨架:产品组容器,内容由 HTMX 加载(动态、基于用户/租户) #} +
+

加载产品组...

+
+ + {# 统计片段:HTMX 加载后填充(动态、不可缓存) #} +
+

加载统计...

+
+ + {# 我的云电脑:HTMX 加载 #} +
+

加载我的云电脑...

+
+
+{% endblock %} diff --git a/app/templates/pages/login.html b/app/templates/pages/login.html new file mode 100644 index 0000000..afb52d8 --- /dev/null +++ b/app/templates/pages/login.html @@ -0,0 +1,59 @@ +{% extends "app_shell.html" %} +{% block title %} - 登录{% endblock %} + +{% block content %} +
+
+

登录

+
+
+ + +
+
+ + +
+ +
+
+
+
+ + +{% endblock %} diff --git a/app/templates/pages/register.html b/app/templates/pages/register.html new file mode 100644 index 0000000..1fd8e53 --- /dev/null +++ b/app/templates/pages/register.html @@ -0,0 +1,55 @@ +{% extends "app_shell.html" %} +{% block title %} - 注册{% endblock %} + +{% block content %} +
+
+

注册

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +{% endblock %} diff --git a/app/tenant/__init__.py b/app/tenant/__init__.py new file mode 100644 index 0000000..e109bd0 --- /dev/null +++ b/app/tenant/__init__.py @@ -0,0 +1 @@ +"""租户(站点隔离)模块:按请求域名解析租户配置。""" diff --git a/app/tenant/dependencies.py b/app/tenant/dependencies.py new file mode 100644 index 0000000..41f4a2c --- /dev/null +++ b/app/tenant/dependencies.py @@ -0,0 +1,49 @@ +"""租户 FastAPI 依赖注入与中间件。 + +- TenantMiddleware:每个请求解析租户并挂到 request.state.tenant +- get_tenant:依赖注入,返回 TenantContext +- get_db_with_tenant:组合数据库会话与租户上下文 +""" +from __future__ import annotations + +from collections.abc import AsyncIterator + +from fastapi import Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings +from app.core.db import get_db +from app.core.exceptions import TenantError +from app.tenant.resolver import TenantContext, resolve_tenant_by_hostname + + +async def get_tenant(request: Request) -> TenantContext: + """从 request.state 获取租户上下文(由中间件注入)。""" + tenant: TenantContext | None = getattr(request.state, "tenant", None) + if tenant is None: + raise TenantError("租户上下文未初始化") + return tenant + + +def get_client_ip(request: Request) -> str: + """获取真实客户端 IP(考虑可信代理)。""" + if settings.use_x_forwarded_for: + xf = request.headers.get("x-forwarded-for") + if xf: + # 取第一个 IP + return xf.split(",")[0].strip() + return request.client.host if request.client else "0.0.0.0" + + +async def get_db_tenant( + request: Request, +) -> AsyncIterator[tuple[AsyncSession, TenantContext]]: + """组合依赖:数据库会话 + 租户上下文。""" + tenant = await get_tenant(request) + async for db in get_db(): + yield db, tenant + + +# 便捷别名 +DBTenant = Depends(get_db_tenant) +TenantDep = Depends(get_tenant) diff --git a/app/tenant/middleware.py b/app/tenant/middleware.py new file mode 100644 index 0000000..24f75c1 --- /dev/null +++ b/app/tenant/middleware.py @@ -0,0 +1,49 @@ +"""租户中间件:按请求域名解析租户并注入 request.state.tenant。 + +此中间件在所有路由前执行,确保每个请求都带有租户上下文。 +解析结果缓存到 Redis,避免每次查库。 +""" +from __future__ import annotations + +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import Response + +from app.core.db import AsyncSessionLocal +from app.core.logging import get_logger +from app.tenant.resolver import TenantContext, resolve_tenant_by_hostname + +log = get_logger(__name__) + + +class TenantMiddleware(BaseHTTPMiddleware): + """按域名解析租户的中间件。""" + + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + # 静态文件与健康检查跳过租户解析 + path = request.url.path + if path.startswith("/static") or path in ("/health", "/favicon.ico"): + request.state.tenant = TenantContext( + hostname=request.url.hostname or "localhost", is_default=True + ) + return await call_next(request) + + hostname = request.url.hostname or "localhost" + + # 用独立短生命周期会话解析租户(不污染路由层会话) + async with AsyncSessionLocal() as db: + try: + tenant = await resolve_tenant_by_hostname(db, hostname) + except Exception as e: # noqa: BLE001 + log.warning("tenant_resolve_failed", hostname=hostname, error=str(e)) + tenant = TenantContext(hostname=hostname, is_default=True) + + request.state.tenant = tenant + response = await call_next(request) + # 在响应头标注租户信息(便于调试,不含敏感数据) + response.headers["X-Tenant"] = tenant.hostname + if not tenant.is_default: + response.headers["X-Tenant-Group"] = str(tenant.site_group_id) + return response diff --git a/app/tenant/resolver.py b/app/tenant/resolver.py new file mode 100644 index 0000000..3a9bca2 --- /dev/null +++ b/app/tenant/resolver.py @@ -0,0 +1,175 @@ +"""基于域名的租户解析。 + +策略(来自架构要求): +- 页面骨架路由仅依据请求域名解析租户配置进行渲染,绝不依赖用户状态 +- 通过按域名区分并配合 keyed-BLAKE2b 签名生成缓存键 +- 解析结果缓存到 Redis(TTL 5 分钟),避免每次请求查库 +- 未匹配域名时回退到默认租户(全局配置) +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.cache.keys import tenant_cache_key +from app.core.config import settings +from app.core.logging import get_logger +from app.core.redis import get_redis +from app.models.tenant import SiteGroup, SiteGroupConfig, SiteGroupHostname + +log = get_logger(__name__) + + +@dataclass +class TenantContext: + """租户上下文:一次请求的租户解析结果。""" + + hostname: str + site_group: Optional[SiteGroup] = None + site_group_id: Optional[int] = None + is_default: bool = True # 是否回退到默认(全局)租户 + + @property + def display_name(self) -> str: + if self.site_group and self.site_group.site_name: + return self.site_group.site_name + return settings.app_name + + +async def resolve_tenant_by_hostname( + db: AsyncSession, hostname: str +) -> TenantContext: + """按域名解析租户。 + + 1. 先查 Redis 缓存(keyed-BLAKE2b 域名哈希键) + 2. 缓存未命中查 SiteGroupHostname 表 + 3. 命中且 SiteGroup 激活则返回该租户,否则回退默认租户 + 4. 结果写回缓存 + """ + hostname = hostname.split(":")[0].lower().strip() # 去端口、小写 + cache_key = tenant_cache_key(hostname) + + # 尝试 Redis 缓存 + if settings.redis_enabled: + try: + redis = await get_redis() + cached = await redis.get(cache_key) + if cached is not None: + if cached == "0": + return TenantContext(hostname=hostname, is_default=True) + sg_id = int(cached) + return TenantContext( + hostname=hostname, site_group_id=sg_id, is_default=False + ) + except Exception as e: # noqa: BLE001 + log.warning("tenant_cache_read_failed", error=str(e)) + + # 查库 + result = await db.execute( + select(SiteGroupHostname, SiteGroup) + .join(SiteGroup, SiteGroupHostname.site_group_id == SiteGroup.id) + .where(SiteGroupHostname.hostname == hostname) + .where(SiteGroup.is_active == True) # noqa: E712 + ) + row = result.first() + + if row is None: + # 未匹配,缓存 0(默认租户) + await _cache_tenant(cache_key, "0") + return TenantContext(hostname=hostname, is_default=True) + + _sg_hostname, sg = row + await _cache_tenant(cache_key, str(sg.id)) + return TenantContext( + hostname=hostname, site_group=sg, site_group_id=sg.id, is_default=False + ) + + +async def _cache_tenant(cache_key: str, value: str) -> None: + if not settings.redis_enabled: + return + try: + redis = await get_redis() + await redis.set(cache_key, value, ex=settings.tenant_cache_ttl) + except Exception as e: # noqa: BLE001 + log.warning("tenant_cache_write_failed", error=str(e)) + + +async def invalidate_tenant_cache(hostname: str) -> None: + """租户配置变更时清除缓存。""" + if not settings.redis_enabled: + return + try: + redis = await get_redis() + await redis.delete(tenant_cache_key(hostname)) + except Exception as e: # noqa: BLE001 + log.warning("tenant_cache_invalidate_failed", error=str(e)) + + +async def get_effective_config( + db: AsyncSession, tenant: TenantContext +) -> dict: + """获取生效配置:站点组配置覆盖全局配置(非空字段优先)。""" + # 全局 SystemConfig(单例 id=1) + from app.models.tenant import SystemConfig + + sys_result = await db.execute(select(SystemConfig).where(SystemConfig.id == 1)) + sys_cfg = sys_result.scalar_one_or_none() + + sg_cfg = None + if tenant.site_group_id: + sg_result = await db.execute( + select(SiteGroupConfig).where( + SiteGroupConfig.site_group_id == tenant.site_group_id + ) + ) + sg_cfg = sg_result.scalar_one_or_none() + + # 合并:站点组非空字段覆盖全局 + merged: dict = {} + if sys_cfg: + merged.update(_config_to_dict(sys_cfg)) + if sg_cfg: + for k, v in _sg_config_to_dict(sg_cfg).items(): + if v is not None and v != "": + merged[k] = v + return merged + + +def _config_to_dict(cfg) -> dict: + return { + "smtp_host": cfg.smtp_host, + "smtp_port": cfg.smtp_port, + "smtp_encryption": cfg.smtp_encryption, + "smtp_username": cfg.smtp_username, + "smtp_from_email": cfg.smtp_from_email, + "smtp_from_name": cfg.smtp_from_name, + "captcha_provider": cfg.captcha_provider, + "captcha_type": cfg.captcha_type, + "enable_registration": cfg.enable_registration, + "site_name": cfg.site_name, + "site_icon": cfg.site_icon, + "icp_number": cfg.icp_number, + "police_number": cfg.police_number, + } + + +def _sg_config_to_dict(cfg) -> dict: + return { + "smtp_host": cfg.smtp_host, + "smtp_port": cfg.smtp_port, + "smtp_encryption": cfg.smtp_encryption, + "smtp_username": cfg.smtp_username, + "smtp_from_email": cfg.smtp_from_email, + "smtp_from_name": cfg.smtp_from_name, + "captcha_provider": cfg.captcha_provider, + "captcha_type": cfg.captcha_type, + "enable_registration": cfg.enable_registration, + "site_name": cfg.site_name, + "site_icon": cfg.site_icon, + "icp_number": cfg.icp_number, + "police_number": cfg.police_number, + } diff --git a/app/web/__init__.py b/app/web/__init__.py new file mode 100644 index 0000000..ade0bf5 --- /dev/null +++ b/app/web/__init__.py @@ -0,0 +1 @@ +"""Web 路由:页面骨架(App Shell,可缓存)+ HTMX 动态片段(不可缓存)。""" diff --git a/app/web/fragments.py b/app/web/fragments.py new file mode 100644 index 0000000..7701633 --- /dev/null +++ b/app/web/fragments.py @@ -0,0 +1,185 @@ +"""HTMX 动态片段路由(不可缓存、基于用户状态与租户依赖)。 + +策略(来自架构要求): +- 用户导航、统计等动态内容由 HTMX 在页面加载后发起独立请求 +- 服务端基于 Ed25519 验签和租户依赖实时返回不可缓存的 HTML 片段 +- 片段响应标记 no-store,确保数据安全隔离 +- 支持 HTMX OOB(Out of Band)swap +""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.dependencies import CurrentUser, get_current_user_optional +from app.cache.fragments import fragment_response, oob_fragment +from app.core.db import get_db +from app.models.operations import AccountOpeningRequest, CloudComputerUser, Product, ProductGroup +from app.models.host import Host +from app.models.ticket import Ticket +from app.templates import render_template +from app.tenant.dependencies import get_tenant +from app.tenant.resolver import TenantContext + +router = APIRouter(prefix="/fragments", tags=["fragments"]) + + +@router.get("/nav") +async def nav_fragment( + request: Request, + user: CurrentUser | None = Depends(get_current_user_optional), + tenant: TenantContext = Depends(get_tenant), + db: AsyncSession = Depends(get_db), +): + """导航片段(基于用户状态,不可缓存)。""" + cfg = {} + try: + from app.tenant.resolver import get_effective_config + + cfg = await get_effective_config(db, tenant) + except Exception: # noqa: BLE001 + pass + + html = await render_template( + "nav.html", + user=user, + enable_registration=cfg.get("enable_registration", False), + ) + return fragment_response(html, request=request) + + +@router.get("/stats") +async def stats_fragment( + request: Request, + user: CurrentUser = Depends(get_current_user_optional), + tenant: TenantContext = Depends(get_tenant), + db: AsyncSession = Depends(get_db), +): + """统计片段(基于用户权限,不可缓存)。 + + 普通用户看自己的统计,管理员看全局/租户统计。 + """ + if user is None: + return fragment_response("
", request=request) + + # 站点隔离过滤 + sg_filter = [] + if tenant.site_group_id and not user.is_superuser: + sg_filter = [Host.site_group_id == tenant.site_group_id] + + hosts_count = 0 + cloud_users_count = 0 + pending_tickets = 0 + pending_requests = 0 + + try: + if user.is_staff or user.is_superuser: + hosts_result = await db.execute( + select(func.count(Host.id)).where(*sg_filter) + ) + hosts_count = hosts_result.scalar() or 0 + else: + hosts_count = 0 + + # 云电脑用户数 + cc_filter = [] + if not (user.is_staff or user.is_superuser): + cc_filter = [CloudComputerUser.owner_id == user.id] + cc_result = await db.execute( + select(func.count(CloudComputerUser.id)).where(*cc_filter) + ) + cloud_users_count = cc_result.scalar() or 0 + + # 待处理工单 + ticket_filter = [Ticket.status.in_(["open", "pending", "processing"])] + if not (user.is_staff or user.is_superuser): + ticket_filter.append(Ticket.creator_id == user.id) + ticket_result = await db.execute( + select(func.count(Ticket.id)).where(*ticket_filter) + ) + pending_tickets = ticket_result.scalar() or 0 + + # 待处理开户申请 + req_filter = [AccountOpeningRequest.status == "pending"] + if not (user.is_staff or user.is_superuser): + req_filter.append(AccountOpeningRequest.applicant_id == user.id) + req_result = await db.execute( + select(func.count(AccountOpeningRequest.id)).where(*req_filter) + ) + pending_requests = req_result.scalar() or 0 + except Exception: # noqa: BLE001 + pass + + html = await render_template( + "stats.html", + stats={ + "hosts_count": hosts_count, + "cloud_users_count": cloud_users_count, + "pending_tickets": pending_tickets, + "pending_requests": pending_requests, + }, + ) + return fragment_response(html, request=request) + + +@router.get("/product-groups") +async def product_groups_fragment( + request: Request, + user: CurrentUser | None = Depends(get_current_user_optional), + tenant: TenantContext = Depends(get_tenant), + db: AsyncSession = Depends(get_db), +): + """产品组片段(基于租户隔离与可见性,不可缓存)。""" + # 站点隔离 + 仅公开可见 + filters = [ProductGroup.is_active == True, ProductGroup.visibility == "public"] # noqa: E712 + if tenant.site_group_id: + filters.append( + (ProductGroup.site_group_id == tenant.site_group_id) + | (ProductGroup.site_group_id.is_(None)) + ) + + result = await db.execute( + select(ProductGroup) + .where(*filters) + .order_by(ProductGroup.display_order) + ) + groups = result.scalars().all() + + # 加载每组的产品 + product_groups = [] + for pg in groups: + prod_result = await db.execute( + select(Product) + .where( + Product.product_group_id == pg.id, + Product.is_available == True, # noqa: E712 + ) + .order_by(Product.id) + ) + products = prod_result.scalars().all() + product_groups.append({"name": pg.name, "description": pg.description, "products": products}) + + html = await render_template("product_groups.html", product_groups=product_groups) + return fragment_response(html, request=request) + + +@router.get("/my-cloud-computers") +async def my_cloud_computers_fragment( + request: Request, + user: CurrentUser = Depends(get_current_user_optional), + db: AsyncSession = Depends(get_db), +): + """我的云电脑片段(基于用户,不可缓存)。""" + if user is None: + return fragment_response("
", request=request) + + result = await db.execute( + select(CloudComputerUser) + .where(CloudComputerUser.owner_id == user.id) + .order_by(CloudComputerUser.created_at.desc()) + ) + cloud_computers = result.scalars().all() + + html = await render_template("my_cloud_computers.html", cloud_computers=cloud_computers) + return fragment_response(html, request=request) diff --git a/app/web/shell.py b/app/web/shell.py new file mode 100644 index 0000000..1e476b6 --- /dev/null +++ b/app/web/shell.py @@ -0,0 +1,80 @@ +"""App Shell 页面骨架路由(可被 CDN 全量缓存)。 + +策略(来自架构要求): +- 页面骨架路由仅依据请求域名解析租户配置进行渲染,绝不依赖用户状态 +- 通过按域名区分并配合 keyed-BLAKE2b 签名生成缓存键 +- 实现 CDN 边缘节点全量高速缓存与防污染 +- 用户导航、统计等动态内容由 HTMX 在页面加载后独立请求获取 +""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession + +from app.cache.app_shell import render_app_shell +from app.core.db import get_db +from app.templates import render_template +from app.tenant.dependencies import get_tenant +from app.tenant.resolver import TenantContext, get_effective_config + +router = APIRouter(tags=["shell"]) + + +async def _shell_context(tenant: TenantContext, db: AsyncSession) -> dict: + """构建 App Shell 渲染上下文(仅租户级配置,无用户状态)。""" + cfg = await get_effective_config(db, tenant) + return { + "site_name": cfg.get("site_name") or "2c2a", + "site_icon": cfg.get("site_icon"), + "icp_number": cfg.get("icp_number"), + "theme": "light", + "enable_registration": cfg.get("enable_registration", False), + } + + +@router.get("/") +async def dashboard_shell( + request: Request, + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """仪表盘 App Shell(可缓存)。 + + 返回的 HTML 仅含租户配置 + 骨架占位,动态内容由 HTMX 加载。 + """ + + async def render() -> str: + ctx = await _shell_context(tenant, db) + return await render_template("dashboard.html", **ctx) + + return await render_app_shell(request, tenant, "/", render) + + +@router.get("/login") +async def login_page( + request: Request, + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """登录页 App Shell(可缓存)。""" + + async def render() -> str: + ctx = await _shell_context(tenant, db) + return await render_template("login.html", **ctx) + + return await render_app_shell(request, tenant, "/login", render) + + +@router.get("/register") +async def register_page( + request: Request, + db: AsyncSession = Depends(get_db), + tenant: TenantContext = Depends(get_tenant), +): + """注册页 App Shell(可缓存)。""" + + async def render() -> str: + ctx = await _shell_context(tenant, db) + return await render_template("register.html", **ctx) + + return await render_app_shell(request, tenant, "/register", render) diff --git a/app/winrm/__init__.py b/app/winrm/__init__.py new file mode 100644 index 0000000..9500fc4 --- /dev/null +++ b/app/winrm/__init__.py @@ -0,0 +1,26 @@ +"""异步 WinRM 客户端包。 + +基于 aiohttp 实现的异步 WS-Management 客户端,替代同步 pywinrm。 + +典型用法:: + + from app.winrm import AsyncWinRMClient + + async with AsyncWinRMClient( + "10.0.0.1", username="admin", password="***" + ) as client: + result = await client.execute_command("whoami") + if result.success: + print(result.std_out) + +或从 Host 模型构造:: + + client = await AsyncWinRMClient.from_host_config(host) + try: + await client.create_user("alice", "P@ssw0rd!") + finally: + await client.close() +""" +from app.winrm.client import AsyncWinRMClient, CommandInjectionError, WinRMResult + +__all__ = ["AsyncWinRMClient", "WinRMResult", "CommandInjectionError"] diff --git a/app/winrm/client.py b/app/winrm/client.py new file mode 100644 index 0000000..21c9380 --- /dev/null +++ b/app/winrm/client.py @@ -0,0 +1,844 @@ +"""高层异步 WinRM 客户端,封装命令执行与用户管理。 + +基于 :class:`app.winrm.transport.WinRMTransport` 实现 WS-Management 协议交互, +对外提供与同步 ``utils.winrm_client.WinrmClient`` 一致的方法集合(异步版)。 + +设计要点 +-------- +- 全异步,绝不阻塞事件循环(aiohttp + ``asyncio.sleep``) +- 连接池复用(由 transport 维护 ``aiohttp.ClientSession``,limit=10) +- demo 模式短路:所有 ``execute_*`` 直接返回模拟成功结果(对应 2C2A_DEMO 模式) +- 注入防护:用户名白名单 + PowerShell 字符串转义 + here-string 防护 +""" +from __future__ import annotations + +import asyncio +import base64 +import logging +import os +import secrets +import string +from dataclasses import dataclass +from typing import Optional +from xml.etree import ElementTree as ET +from xml.sax.saxutils import escape as xml_escape + +from app.winrm.commands import ( + ADD_TO_REMOTE_USERS_PS, + CHECK_USER_EXISTS_PS, + CREATE_USER_PS, + CREATE_USER_RESET_PS, + DELETE_USER_PS, + DISABLE_USER_PS, + ENABLE_USER_PS, + GET_PASSWORD_POLICY_PS, + GET_USER_INFO_PS, + GRANT_ADMIN_PS, + LIST_USERS_PS, + RESET_PASSWORD_PS, + REVOKE_ADMIN_PS, + escape_ps_string, + validate_groupname, + validate_username, +) +from app.winrm.transport import ( + ACTION_COMMAND, + ACTION_CREATE, + ACTION_DELETE, + ACTION_RECEIVE, + ACTION_SIGNAL, + RSRC_CMD, + SIGNAL_TERMINATE, + WinRMTransport, + WinRMTransportError, +) + +logger = logging.getLogger("2c2a") + +# 默认密码策略(获取失败或 demo 模式时使用) +DEFAULT_PASSWORD_POLICY = { + "minimum_length": 8, + "complexity_required": True, + "history_size": 0, + "max_age_days": 42, + "min_age_days": 1, +} + + +class CommandInjectionError(Exception): + """命令注入防护异常。 + + 当用户名 / 组名 / 字符串未通过白名单或转义校验时抛出。 + """ + + +@dataclass +class WinRMResult: + """WinRM 命令执行结果。""" + + status_code: int + std_out: str + std_err: str + + @property + def success(self) -> bool: + """状态码为 0 视为成功。""" + return self.status_code == 0 + + +def _safe_int(line: str, default: int) -> int: + """从 ``Key = Value`` 形式的行中安全解析整数,失败返回默认值。""" + try: + return int(line.split("=", 1)[1].strip()) + except (IndexError, ValueError): + return default + + +class AsyncWinRMClient: + """异步 WinRM 客户端。 + + 用法:: + + async with AsyncWinRMClient("10.0.0.1", username="admin", password="***") as c: + result = await c.execute_command("whoami") + if result.success: + print(result.std_out) + """ + + def __init__( + self, + host: str, + port: int = 5985, + username: Optional[str] = None, + password: Optional[str] = None, + auth_method: str = "ntlm", + use_ssl: bool = False, + cert_pem_path: Optional[str] = None, + cert_key_path: Optional[str] = None, + timeout: int = 30, + max_retries: int = 3, + demo: bool = False, + ): + """ + :param host: 主机名或 IP 地址 + :param port: WinRM 端口,默认 5985(HTTP);HTTPS 通常为 5986 + :param username: 登录用户名(NTLM 必填) + :param password: 登录密码(NTLM 必填) + :param auth_method: 认证方式,``ntlm`` 或 ``certificate`` + :param use_ssl: 是否使用 SSL + :param cert_pem_path: 客户端证书 PEM 路径(证书认证必填) + :param cert_key_path: 客户端私钥 PEM 路径(证书认证必填) + :param timeout: 单次请求超时(秒) + :param max_retries: 失败最大重试次数 + :param demo: demo 模式,所有方法返回模拟成功结果,不实际连接 + """ + self.host = host + self.port = port + self.username = username + self.password = password + self.auth_method = auth_method + self.use_ssl = use_ssl + self.cert_pem_path = cert_pem_path + self.cert_key_path = cert_key_path + self.timeout = timeout + self.max_retries = max_retries + # demo 模式:显式传入或环境变量 2C2A_DEMO=1 均生效 + self.demo = bool(demo) or os.environ.get("2C2A_DEMO", "").lower() == "1" + + # 缓存的密码策略,供同步方法 generate_strong_password 使用 + self._cached_policy: Optional[dict] = None + + # 构造认证信息与端点 + if auth_method == "ntlm": + if not username or not password: + raise ValueError("NTLM 认证必须提供用户名和密码") + auth = {"method": "ntlm", "username": username, "password": password} + verify_ssl = True + elif auth_method == "certificate": + if not cert_pem_path or not cert_key_path: + raise ValueError("证书认证必须提供证书和私钥路径") + # 证书认证强制 SSL + if not self.use_ssl: + self.use_ssl = True + auth = { + "method": "certificate", + "cert_pem_path": cert_pem_path, + "key_pem_path": cert_key_path, + "username": username or "", + } + verify_ssl = True + else: + raise ValueError(f"不支持的认证方式: {auth_method}") + + protocol = "https" if self.use_ssl else "http" + self.endpoint = f"{protocol}://{host}:{port}/wsman" + + self._transport = WinRMTransport( + endpoint=self.endpoint, + auth=auth, + timeout=timeout, + max_retries=max_retries, + use_ssl=self.use_ssl, + verify_ssl=verify_ssl, + ) + + logger.info( + "初始化异步 WinRM 客户端: host=%s, port=%s, ssl=%s, auth=%s, demo=%s", + host, + port, + self.use_ssl, + auth_method, + self.demo, + ) + + # ------------------------------------------------------------------ + # 工厂方法 + # ------------------------------------------------------------------ + @classmethod + async def from_host_config(cls, host) -> "AsyncWinRMClient": + """从 Host 模型(鸭子类型)构造客户端。 + + ``host`` 需含属性: ``hostname`` / ``port`` / ``username`` / ``auth_method`` + / ``use_ssl``,以及加密的密码字段。 + + 密码字段是加密存储的,需调用 + ``app.security.field_cipher.decrypt_field`` 解密(字段名 ``host.password``)。 + 证书认证时读取 ``cert_pem_path`` / ``cert_key_path``。 + + 为兼容不同实现,优先用 ``decrypt_field`` 解密原始 ``_password`` 字段, + 失败则回退到模型自身的 ``password`` 属性。 + """ + auth_method = getattr(host, "auth_method", "ntlm") + use_ssl = getattr(host, "use_ssl", False) + port = getattr(host, "port", 5986 if use_ssl else 5985) + hostname = getattr(host, "hostname") + username = getattr(host, "username", None) or None + + # 解密密码:优先用 field_cipher 解密原始加密字段 + password: Optional[str] = None + encrypted = getattr(host, "_password", None) + if encrypted: + try: + from app.security.field_cipher import decrypt_field + + password = decrypt_field(encrypted, "host.password") + except Exception as e: # noqa: BLE001 + logger.warning("field_cipher 解密密码失败,尝试 password 属性: %s", e) + password = None + if not password: + # 回退到模型自身的 password 属性(可能已解密) + try: + password = getattr(host, "password", None) + except Exception: # noqa: BLE001 + password = None + + cert_pem_path = getattr(host, "cert_pem_path", None) or None + cert_key_path = getattr(host, "cert_key_path", None) or None + + return cls( + host=hostname, + port=port, + username=username, + password=password, + auth_method=auth_method, + use_ssl=use_ssl, + cert_pem_path=cert_pem_path, + cert_key_path=cert_key_path, + ) + + # ------------------------------------------------------------------ + # 注入防护 + # ------------------------------------------------------------------ + def _validate_username(self, username: str) -> str: + """用户名白名单校验,无效抛 :class:`CommandInjectionError`。""" + try: + return validate_username(username) + except ValueError as e: + raise CommandInjectionError(str(e)) from e + + def _validate_groupname(self, group: str) -> str: + """组名校验,无效抛 :class:`CommandInjectionError`。""" + try: + return validate_groupname(group) + except ValueError as e: + raise CommandInjectionError(str(e)) from e + + def _escape_ps_string(self, s: str) -> str: + """PowerShell 字符串转义,超长抛 :class:`CommandInjectionError`。""" + try: + return escape_ps_string(s) + except ValueError as e: + raise CommandInjectionError(str(e)) from e + + def _escape_for_here_string(self, s: str) -> str: + """防止 here-string 注入:禁止出现 ``@"`` 与 ``"@`` 分隔符。""" + if not s: + return s + s = s.replace("\x00", "") + if '@"' in s or '"@' in s: + raise CommandInjectionError("内容包含非法的 here-string 分隔符") + return s + + # ------------------------------------------------------------------ + # demo 模式短路 + # ------------------------------------------------------------------ + def _demo_result(self) -> WinRMResult: + """demo 模式统一返回的模拟成功结果。""" + return WinRMResult(status_code=0, std_out="DEMO MODE", std_err="") + + # ------------------------------------------------------------------ + # 命令执行(WS-Management 协议编排) + # ------------------------------------------------------------------ + async def execute_command( + self, command: str, arguments: Optional[list[str]] = None + ) -> WinRMResult: + """执行远程 cmd 命令。 + + :param command: 命令名,如 ``whoami`` / ``cmd`` + :param arguments: 命令参数列表 + """ + if self.demo: + logger.info("DEMO 模式: 模拟执行命令: %s %s", command, arguments) + return self._demo_result() + return await self._run_shell_command(RSRC_CMD, command, arguments or []) + + async def execute_powershell(self, script: str) -> WinRMResult: + """执行 PowerShell 脚本。 + + 将脚本以 UTF-16LE 编码后 base64,通过 + ``powershell.exe -EncodedCommand`` 执行。这是 pywinrm 的标准做法, + 可正确处理 Unicode 与特殊字符,避免命令行转义问题。 + """ + if self.demo: + logger.info("DEMO 模式: 模拟执行 PowerShell 脚本") + return self._demo_result() + encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii") + return await self._run_shell_command( + RSRC_CMD, + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], + ) + + async def _run_shell_command( + self, resource_uri: str, command: str, arguments: list[str] + ) -> WinRMResult: + """完整的 WS-Management 命令执行流程:建壳 -> 执行 -> 接收 -> 清理。""" + shell_id: Optional[str] = None + try: + # 1. 创建 Shell + shell_id = await self._create_shell(resource_uri) + # 2. 执行命令,获取 command_id + command_id = await self._send_command( + resource_uri, shell_id, command, arguments + ) + # 3. 接收输出(轮询直到结束) + stdout, stderr, exit_code = await self._receive_output( + resource_uri, shell_id, command_id + ) + # 4. 发送终止信号 + await self._signal_command(resource_uri, shell_id, command_id) + return WinRMResult( + status_code=exit_code, std_out=stdout, std_err=stderr + ) + except WinRMTransportError as e: + logger.error("命令执行失败: %s", e) + return WinRMResult(status_code=-1, std_out="", std_err=str(e)) + except Exception as e: # noqa: BLE001 + logger.exception("命令执行异常") + return WinRMResult(status_code=-1, std_out="", std_err=str(e)) + finally: + if shell_id: + try: + await self._delete_shell(resource_uri, shell_id) + except Exception: # noqa: BLE001 + logger.warning("删除 Shell 失败: %s", shell_id, exc_info=True) + + async def _create_shell(self, resource_uri: str) -> str: + """创建 Shell,返回 ShellId。""" + body = ( + "" + "stdin" + "stdout stderr" + "" + ) + envelope = self._transport._build_envelope( + action=ACTION_CREATE, + resource_uri=resource_uri, + selectors=None, + body=body, + ) + resp = await self._transport.post(envelope) + return self._parse_shell_id(resp) + + async def _send_command( + self, + resource_uri: str, + shell_id: str, + command: str, + arguments: list[str], + ) -> str: + """发送命令,返回 CommandId。""" + args_xml = "".join( + f"{xml_escape(a)}" for a in arguments + ) + # MS-WSMV 规范:外层 rsp:Command 包裹命令行与参数 + body = ( + "" + f"{xml_escape(command)}" + f"{args_xml}" + "" + ) + envelope = self._transport._build_envelope( + action=ACTION_COMMAND, + resource_uri=resource_uri, + selectors={"ShellId": shell_id}, + body=body, + ) + resp = await self._transport.post(envelope) + return self._parse_command_id(resp) + + async def _receive_output( + self, resource_uri: str, shell_id: str, command_id: str + ) -> tuple[str, str, int]: + """轮询接收命令输出,返回 (stdout, stderr, exit_code)。 + + 使用 ``asyncio.sleep`` 间隔轮询,不阻塞事件循环; + 最大轮询次数由 timeout 限定,避免无限等待。 + """ + stdout_parts: list[str] = [] + stderr_parts: list[str] = [] + exit_code = 0 + # 每 0.1s 轮询一次,最多持续 timeout 秒 + max_iterations = max(1, self.timeout * 10) + done = False + + for _ in range(max_iterations): + body = ( + "" + f"{xml_escape(command_id)}" + "stdout" + "stderr" + "" + ) + envelope = self._transport._build_envelope( + action=ACTION_RECEIVE, + resource_uri=resource_uri, + selectors={"ShellId": shell_id}, + body=body, + ) + resp = await self._transport.post(envelope) + done, out, err, code = self._parse_receive_response(resp) + if out: + stdout_parts.append(out) + if err: + stderr_parts.append(err) + if code is not None: + exit_code = code + if done: + break + await asyncio.sleep(0.1) + else: + logger.warning( + "接收输出超时,shell=%s command=%s", shell_id, command_id + ) + + return "".join(stdout_parts), "".join(stderr_parts), exit_code + + async def _signal_command( + self, resource_uri: str, shell_id: str, command_id: str + ) -> None: + """发送终止信号。""" + body = ( + "" + f"{xml_escape(command_id)}" + f"{SIGNAL_TERMINATE}" + "" + ) + envelope = self._transport._build_envelope( + action=ACTION_SIGNAL, + resource_uri=resource_uri, + selectors={"ShellId": shell_id}, + body=body, + ) + await self._transport.post(envelope) + + async def _delete_shell( + self, resource_uri: str, shell_id: str + ) -> None: + """删除 Shell,释放服务端资源。""" + envelope = self._transport._build_envelope( + action=ACTION_DELETE, + resource_uri=resource_uri, + selectors={"ShellId": shell_id}, + body="", + ) + await self._transport.post(envelope) + + # ------------------------------------------------------------------ + # SOAP 响应解析 + # ------------------------------------------------------------------ + @staticmethod + def _local(tag: str) -> str: + """去除命名空间前缀,返回本地标签名。""" + return tag.split("}", 1)[-1] if "}" in tag else tag + + def _parse_shell_id(self, resp: str) -> str: + """从 Create 响应中解析 ShellId。""" + try: + root = ET.fromstring(resp) + except ET.ParseError as e: + raise WinRMTransportError(f"解析 ShellId 响应失败: {e}") from e + for elem in root.iter(): + if self._local(elem.tag) == "Selector" and elem.get("Name") == "ShellId": + return elem.text or "" + if self._local(elem.tag) == "ShellId": + return elem.text or "" + raise WinRMTransportError("无法从响应中解析 ShellId") + + def _parse_command_id(self, resp: str) -> str: + """从 Command 响应中解析 CommandId。""" + try: + root = ET.fromstring(resp) + except ET.ParseError as e: + raise WinRMTransportError(f"解析 CommandId 响应失败: {e}") from e + for elem in root.iter(): + if self._local(elem.tag) == "CommandId": + return elem.text or "" + raise WinRMTransportError("无法从响应中解析 CommandId") + + def _parse_receive_response( + self, resp: str + ) -> tuple[bool, str, str, Optional[int]]: + """解析 Receive 响应。 + + :returns: (是否结束, stdout 增量, stderr 增量, exit_code 或 None) + """ + done = False + out = "" + err = "" + exit_code: Optional[int] = None + try: + root = ET.fromstring(resp) + except ET.ParseError: + # 解析失败视为结束,避免死循环 + return True, "", "", None + + for elem in root.iter(): + tag = self._local(elem.tag) + if tag == "Stream": + name = elem.get("Name", "") + text = elem.text or "" + try: + decoded = base64.b64decode(text).decode( + "utf-8", errors="ignore" + ) + except Exception: # noqa: BLE001 + decoded = text + if name == "stdout": + out += decoded + elif name == "stderr": + err += decoded + elif tag == "CommandState": + state = elem.get("State", "") + if "Done" in state or "Terminated" in state: + done = True + for child in elem: + if self._local(child.tag) == "ExitCode": + try: + exit_code = int((child.text or "0").strip()) + except ValueError: + exit_code = 0 + return done, out, err, exit_code + + # ------------------------------------------------------------------ + # 用户管理方法(全部 async,内部调用 execute_powershell) + # ------------------------------------------------------------------ + async def create_user( + self, + username: str, + password: str, + description: Optional[str] = None, + group: Optional[str] = None, + ) -> WinRMResult: + """创建本地用户,并加入 Users 组与(可选)指定组。""" + try: + self._validate_username(username) + self._escape_ps_string(password) + if description: + self._escape_ps_string(description) + if group: + self._validate_groupname(group) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + + safe_user = self._escape_ps_string(username) + safe_pass = self._escape_ps_string(password) + safe_desc = self._escape_ps_string(description or "") + extra_group = "" + if group: + safe_group = self._escape_ps_string(group) + extra_group = ( + f'Add-LocalGroupMember -Group "{safe_group}" ' + f'-Member "{safe_user}" -ErrorAction Stop' + ) + script = CREATE_USER_PS.format( + username=safe_user, + password=safe_pass, + description=safe_desc, + extra_group=extra_group, + ) + logger.info("创建用户: %s", username) + result = await self.execute_powershell(script) + await self.add_to_remote_users(username) + return result + + async def create_user_with_reset_password_on_next_login( + self, + username: str, + password: str, + description: Optional[str] = None, + group: Optional[str] = None, + ) -> WinRMResult: + """创建本地用户,并要求首次登录时修改密码。""" + try: + self._validate_username(username) + self._escape_ps_string(password) + if description: + self._escape_ps_string(description) + if group: + self._validate_groupname(group) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + + safe_user = self._escape_ps_string(username) + safe_pass = self._escape_ps_string(password) + safe_desc = self._escape_ps_string(description or "") + extra_group = "" + if group: + safe_group = self._escape_ps_string(group) + extra_group = ( + f'Add-LocalGroupMember -Group "{safe_group}" ' + f'-Member "{safe_user}" -ErrorAction Stop' + ) + script = CREATE_USER_RESET_PS.format( + username=safe_user, + password=safe_pass, + description=safe_desc, + extra_group=extra_group, + ) + logger.info("创建用户(首登改密): %s", username) + result = await self.execute_powershell(script) + await self.add_to_remote_users(username) + return result + + async def delete_user(self, username: str) -> WinRMResult: + """删除本地用户。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = DELETE_USER_PS.format(username=safe_user) + logger.info("删除用户: %s", username) + return await self.execute_powershell(script) + + async def enable_user(self, username: str) -> WinRMResult: + """启用本地用户。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = ENABLE_USER_PS.format(username=safe_user) + logger.info("启用用户: %s", username) + return await self.execute_powershell(script) + + async def disable_user(self, username: str) -> WinRMResult: + """禁用本地用户。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = DISABLE_USER_PS.format(username=safe_user) + logger.info("禁用用户: %s", username) + return await self.execute_powershell(script) + + async def get_user_info(self, username: str) -> WinRMResult: + """获取单个用户信息(JSON)。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = GET_USER_INFO_PS.format(username=safe_user) + return await self.execute_powershell(script) + + async def list_users(self) -> WinRMResult: + """列出所有本地用户(JSON)。""" + return await self.execute_powershell(LIST_USERS_PS) + + async def check_user_exists(self, username: str) -> bool: + """检查用户是否存在。""" + try: + self._validate_username(username) + except CommandInjectionError: + return False + safe_user = self._escape_ps_string(username) + try: + script = CHECK_USER_EXISTS_PS.format(username=safe_user) + result = await self.execute_powershell(script) + return result.success and "True" in result.std_out + except Exception: # noqa: BLE001 + return False + + async def reset_password(self, username: str, password: str) -> WinRMResult: + """重置用户密码。""" + try: + self._validate_username(username) + self._escape_ps_string(password) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + safe_pass = self._escape_ps_string(password) + script = RESET_PASSWORD_PS.format( + username=safe_user, password=safe_pass + ) + result = await self.execute_powershell(script) + if result.success: + await self.add_to_remote_users(username) + return result + + async def add_to_remote_users(self, username: str) -> WinRMResult: + """加入 Remote Desktop Users 组。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = ADD_TO_REMOTE_USERS_PS.format(username=safe_user) + return await self.execute_powershell(script) + + async def grant_admin_privileges(self, username: str) -> WinRMResult: + """授予管理员权限(加入 Administrators 组)。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = GRANT_ADMIN_PS.format(username=safe_user) + logger.info("授予管理员权限: %s", username) + return await self.execute_powershell(script) + + async def revoke_admin_privileges(self, username: str) -> WinRMResult: + """撤销管理员权限(移出 Administrators 组)。""" + try: + self._validate_username(username) + except CommandInjectionError as e: + logger.warning("输入验证失败: %s", e) + return WinRMResult(1, "", str(e)) + safe_user = self._escape_ps_string(username) + script = REVOKE_ADMIN_PS.format(username=safe_user) + logger.info("撤销管理员权限: %s", username) + return await self.execute_powershell(script) + + # ------------------------------------------------------------------ + # 密码策略与强密码生成 + # ------------------------------------------------------------------ + async def get_password_policy(self) -> dict: + """通过 secedit 导出并解析密码策略。 + + 返回字典包含: ``minimum_length`` / ``complexity_required`` / + ``history_size`` / ``max_age_days`` / ``min_age_days``。 + 结果会缓存到 ``self._cached_policy``,供同步方法 + :meth:`generate_strong_password` 使用。 + """ + if self.demo: + self._cached_policy = dict(DEFAULT_PASSWORD_POLICY) + return self._cached_policy + + try: + result = await self.execute_powershell(GET_PASSWORD_POLICY_PS) + policy: dict = {} + if result.success: + for raw_line in result.std_out.strip().split("\n"): + line = raw_line.strip() + if line.startswith("MinimumPasswordLength"): + policy["minimum_length"] = _safe_int(line, 8) + elif line.startswith("PasswordComplexity"): + policy["complexity_required"] = bool(_safe_int(line, 1)) + elif line.startswith("PasswordHistorySize"): + policy["history_size"] = _safe_int(line, 0) + elif line.startswith("MaximumPasswordAge"): + policy["max_age_days"] = _safe_int(line, 42) + elif line.startswith("MinimumPasswordAge"): + policy["min_age_days"] = _safe_int(line, 1) + # 补全默认值 + for key, value in DEFAULT_PASSWORD_POLICY.items(): + policy.setdefault(key, value) + self._cached_policy = policy + logger.info("获取密码策略成功: %s", policy) + return policy + except Exception as e: # noqa: BLE001 + logger.error("获取密码策略失败: %s", e) + self._cached_policy = dict(DEFAULT_PASSWORD_POLICY) + return self._cached_policy + + def generate_strong_password(self, length: int = 16) -> str: + """根据密码策略生成强密码(同步,纯计算)。 + + 使用 ``self._cached_policy``(由 :meth:`get_password_policy` 缓存), + 若未缓存则使用默认策略。复杂度要求开启时,保证至少包含大写字母、 + 小写字母、数字与特殊字符各一个。 + """ + policy = self._cached_policy or DEFAULT_PASSWORD_POLICY + min_len = policy.get("minimum_length", 8) + complexity = policy.get("complexity_required", True) + + actual_length = max(length, min_len) + + if complexity: + uppercase = secrets.choice(string.ascii_uppercase) + lowercase = secrets.choice(string.ascii_lowercase) + digit = secrets.choice(string.digits) + special = secrets.choice("!@#$%^&*()_+-=[]{}|;:,.<>?") + remaining = max(0, actual_length - 4) + alphabet = ( + string.ascii_letters + + string.digits + + "!@#$%^&*()_+-=[]{}|;:,.<>?" + ) + rest = "".join(secrets.choice(alphabet) for _ in range(remaining)) + chars = list(uppercase + lowercase + digit + special + rest) + secrets.SystemRandom().shuffle(chars) + password = "".join(chars) + else: + alphabet = string.ascii_letters + string.digits + password = "".join( + secrets.choice(alphabet) for _ in range(actual_length) + ) + + logger.info("生成强密码完成,长度: %d", len(password)) + return password + + # ------------------------------------------------------------------ + # 生命周期管理 + # ------------------------------------------------------------------ + async def close(self): + """关闭底层 transport 的 aiohttp session。""" + await self._transport.close() + + async def __aenter__(self) -> "AsyncWinRMClient": + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + await self.close() diff --git a/app/winrm/commands.py b/app/winrm/commands.py new file mode 100644 index 0000000..8f950f3 --- /dev/null +++ b/app/winrm/commands.py @@ -0,0 +1,149 @@ +"""PowerShell 命令模板常量与构造函数。 + +所有模板中需要注入的参数,在调用方必须先经过 :func:`escape_ps_string` 转义, +再通过 ``str.format`` 注入,从而避免 PowerShell 命令注入风险。 + +注意:模板里出现的 ``{{`` / ``}}`` 是 ``str.format`` 的字面花括号转义, +对应 PowerShell 脚本中的单个 ``{`` / ``}``。 +""" +from __future__ import annotations + +import re + +# 用户名白名单:字母、数字、点、下划线、连字符,长度 1-20 +USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,20}$") +# 组名白名单:相对宽松,允许字母、数字、点、下划线、连字符、空格、中英文括号及中文 +GROUPNAME_PATTERN = re.compile(r"^[A-Za-z0-9._\-\s()()\u4e00-\u9fa5]{1,64}$") + +# 字符串最大长度,防止超长输入造成缓冲或日志膨胀 +MAX_STRING_LENGTH = 4096 + + +def validate_username(name: str) -> str: + """校验用户名,符合白名单则原样返回,否则抛 ``ValueError``。""" + if not name: + raise ValueError("用户名不能为空") + if len(name) > 20: + raise ValueError("用户名长度不能超过 20 个字符") + if not USERNAME_PATTERN.match(name): + raise ValueError("用户名格式无效: 只允许字母、数字、点、下划线和连字符") + return name + + +def validate_groupname(name: str) -> str: + """校验组名,相对宽松(允许中文及中英文括号等),失败抛 ``ValueError``。""" + if not name: + raise ValueError("组名不能为空") + if len(name) > 64: + raise ValueError("组名长度不能超过 64 个字符") + if not GROUPNAME_PATTERN.match(name): + raise ValueError("组名格式无效: 含非法字符") + return name + + +def escape_ps_string(s: str) -> str: + """转义 PowerShell 双引号字符串中的特殊字符。 + + 转义字符为反引号 (````),需转义的目标字符: + - 反引号 `` ` `` + - 双引号 `` "`` + - 美元符 `` $`` + - 换行 ``\\n`` / 回车 ``\\r`` + + 超长字符串抛 ``ValueError``,防止注入超长 payload。 + """ + if not s: + return s + if len(s) > MAX_STRING_LENGTH: + raise ValueError(f"字符串长度超过最大限制 {MAX_STRING_LENGTH}") + return ( + s.replace("\x00", "") # 去除 NUL,避免截断绕过 + .replace("`", "``") + .replace('"', '`"') + .replace("$", "`$") + .replace("\n", "`n") + .replace("\r", "`r") + ) + + +# --------------------------------------------------------------------------- +# PowerShell 脚本模板 +# +# 约定:占位符 {username} / {password} / {description} / {group} / {extra_group} +# 在调用方注入前,必须先经过 escape_ps_string 转义。 +# --------------------------------------------------------------------------- + +# 创建本地用户(不强制首登改密) +CREATE_USER_PS = """\ +$ErrorActionPreference = 'Stop' +$pw = ConvertTo-SecureString "{password}" -AsPlainText -Force +New-LocalUser -Name "{username}" -Password $pw -Description "{description}" -ErrorAction Stop +net user "{username}" /logonpasswordchg:NO +Add-LocalGroupMember -Group "Users" -Member "{username}" -ErrorAction Stop +{extra_group} +""" + +# 创建本地用户(首登强制修改密码) +CREATE_USER_RESET_PS = """\ +$ErrorActionPreference = 'Stop' +$pw = ConvertTo-SecureString "{password}" -AsPlainText -Force +New-LocalUser -Name "{username}" -Password $pw -Description "{description}" -ErrorAction Stop +net user "{username}" /logonpasswordchg:YES +Add-LocalGroupMember -Group "Users" -Member "{username}" -ErrorAction Stop +{extra_group} +""" + +# 删除用户 +DELETE_USER_PS = 'Remove-LocalUser -Name "{username}" -ErrorAction Stop' + +# 启用用户 +ENABLE_USER_PS = 'Enable-LocalUser -Name "{username}" -ErrorAction Stop' + +# 禁用用户 +DISABLE_USER_PS = 'Disable-LocalUser -Name "{username}" -ErrorAction Stop' + +# 获取单个用户信息(JSON) +GET_USER_INFO_PS = 'Get-LocalUser -Name "{username}" | ConvertTo-Json' + +# 列出所有本地用户(JSON) +LIST_USERS_PS = "Get-LocalUser | ConvertTo-Json" + +# 重置密码 +RESET_PASSWORD_PS = """\ +$ErrorActionPreference = 'Stop' +$pw = ConvertTo-SecureString "{password}" -AsPlainText -Force +Set-LocalUser -Name "{username}" -Password $pw +net user "{username}" /logonpasswordchg:NO +""" + +# 加入指定组(参数化 group 名) +ADD_TO_GROUP_PS = ( + 'Add-LocalGroupMember -Group "{group}" -Member "{username}" ' + "-ErrorAction SilentlyContinue" +) + +# 加入 Remote Desktop Users 组 +ADD_TO_REMOTE_USERS_PS = ( + 'Add-LocalGroupMember -Group "Remote Desktop Users" -Member "{username}" ' + "-ErrorAction SilentlyContinue" +) + +# 授予管理员权限(加入 Administrators) +GRANT_ADMIN_PS = 'net localgroup Administrators "{username}" /add' + +# 撤销管理员权限(移出 Administrators) +REVOKE_ADMIN_PS = 'net localgroup Administrators "{username}" /delete' + +# 检查用户是否存在(存在则输出 True) +CHECK_USER_EXISTS_PS = ( + '$u = Get-LocalUser -Name "{username}" -ErrorAction Stop; $true' +) + +# 获取密码策略:通过 secedit 导出并解析关键字段 +# 注意:脚本内 Where-Object 的花括号是 PowerShell 语法,无需 format 注入参数, +# 因此保持单花括号(本模板不含占位符,不经过 str.format)。 +GET_PASSWORD_POLICY_PS = """\ +secedit /export /cfg "$env:TEMP\\secpol.cfg" | Out-Null +Get-Content "$env:TEMP\\secpol.cfg" | Where-Object { $_ -match '^(MinimumPasswordLength|PasswordComplexity|PasswordHistorySize|MaximumPasswordAge|MinimumPasswordAge)\\s*=' } +Remove-Item "$env:TEMP\\secpol.cfg" -ErrorAction SilentlyContinue +""" diff --git a/app/winrm/transport.py b/app/winrm/transport.py new file mode 100644 index 0000000..b81c580 --- /dev/null +++ b/app/winrm/transport.py @@ -0,0 +1,281 @@ +"""基于 aiohttp 的 WS-Management 协议传输层。 + +用异步 HTTP 客户端替代同步 pywinrm 的传输层,提供: + +- NTLM 认证(用户名 / 密码) +- 证书认证(客户端证书 + 私钥,通过 SSL context 加载) +- 连接池复用(``aiohttp.TCPConnector`` limit=10) +- 失败重试 + 指数退避(``asyncio.sleep``,绝不阻塞事件循环) +- WS-Management SOAP envelope 构造辅助方法 + +.. note:: + 完整的 NTLM 握手(Type1 / Type2 / Type3 三轮协商)需要 ``pyspnego`` 或 + ``requests_ntlm`` 等库。本实现先用 ``aiohttp.BasicAuth`` 占位,保证结构完整; + 生产环境应替换为真正的 NTLM 协商。证书认证则已完整实现(SSL context 加载 + 客户端证书)。 +""" +from __future__ import annotations + +import asyncio +import logging +import ssl +import uuid +from typing import Optional +from xml.sax.saxutils import escape as xml_escape + +import aiohttp + +logger = logging.getLogger("2c2a") + +# --------------------------------------------------------------------------- +# WS-Management / SOAP 命名空间 +# --------------------------------------------------------------------------- +NS = { + "s": "http://www.w3.org/2003/05/soap-envelope", # SOAP 1.2 + "wsa": "http://schemas.xmlsoap.org/ws/2004/08/addressing", + "wsman": "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd", + "wxf": "http://schemas.xmlsoap.org/ws/2004/09/transfer", + "rsp": "http://schemas.microsoft.com/wbem/wsman/1/windows/shell", + "cfg": "http://schemas.microsoft.com/wbem/wsman/1/config", +} + +# 资源 URI +RSRC_CMD = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd" +RSRC_PS = "http://schemas.microsoft.com/powershell/Microsoft.PowerShell" + +# 常用 WS-Addressing Action +ACTION_CREATE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create" +ACTION_COMMAND = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command" +ACTION_RECEIVE = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive" +ACTION_SIGNAL = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal" +ACTION_DELETE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete" + +# 终止信号码 +SIGNAL_TERMINATE = ( + "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate" +) + + +class WinRMTransportError(Exception): + """WS-Management 传输层错误。""" + + +class WinRMTransport: + """WS-Management 协议异步传输层。 + + 封装 aiohttp 的会话管理、认证、SOAP envelope 构造与重试逻辑, + 供 :class:`app.winrm.client.AsyncWinRMClient` 调用。 + """ + + def __init__( + self, + endpoint: str, + auth: dict, + timeout: int = 30, + max_retries: int = 3, + use_ssl: bool = False, + verify_ssl: bool = True, + ): + """ + :param endpoint: WS-Management 端点,形如 ``http://host:5985/wsman`` + :param auth: 认证信息字典: + + * NTLM: ``{"method": "ntlm", "username": "...", "password": "..."}`` + * 证书: ``{"method": "certificate", + "cert_pem_path": "...", "key_pem_path": "...", + "username": "..."(可选)}`` + :param timeout: 单次请求总超时(秒) + :param max_retries: 失败最大重试次数 + :param use_ssl: 是否使用 SSL(影响 ssl context 构造) + :param verify_ssl: 是否校验服务器证书 + """ + self.endpoint = endpoint + self.auth = auth + self.timeout = timeout + self.max_retries = max_retries + self.use_ssl = use_ssl + self.verify_ssl = verify_ssl + + # aiohttp 会话(懒加载,避免在事件循环外创建) + self._session: Optional[aiohttp.ClientSession] = None + # 预构造 SSL 上下文(证书认证 / 服务器证书校验) + self._ssl_context: Optional[ssl.SSLContext] = self._build_ssl_context() + + # ------------------------------------------------------------------ + # SSL / 认证相关 + # ------------------------------------------------------------------ + def _build_ssl_context(self) -> Optional[ssl.SSLContext]: + """构造 SSL 上下文。 + + - 证书认证:加载客户端证书与私钥(强制 SSL) + - ``verify_ssl=False``:关闭服务器证书校验(仅用于内网 / 自签场景) + - 非 SSL 的 NTLM:返回 None,由 aiohttp 自行处理 + """ + method = self.auth.get("method") + if not self.use_ssl and method != "certificate": + return None + + ctx = ssl.create_default_context() + if not self.verify_ssl: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + if method == "certificate": + cert_pem = self.auth.get("cert_pem_path") + key_pem = self.auth.get("key_pem_path") + if not cert_pem or not key_pem: + raise ValueError("证书认证必须提供 cert_pem_path 与 key_pem_path") + ctx.load_cert_chain(certfile=cert_pem, keyfile=key_pem) + return ctx + + def _build_auth(self) -> Optional[aiohttp.BasicAuth]: + """构造 aiohttp 认证对象。 + + NTLM 完整握手需要 ``pyspnego``;此处先用 ``BasicAuth`` 占位, + 保证结构完整。生产环境应替换为真正的 NTLM 协商(见模块文档)。 + """ + method = self.auth.get("method") + if method == "ntlm": + username = self.auth.get("username", "") + password = self.auth.get("password", "") + # 占位:实际 NTLM 需 Type1/2/3 多轮协商 + return aiohttp.BasicAuth(username, password) + # 证书认证依赖 SSL context,不需要 BasicAuth + return None + + # ------------------------------------------------------------------ + # Session 管理(懒加载 + 连接池复用) + # ------------------------------------------------------------------ + async def _get_session(self) -> aiohttp.ClientSession: + """懒加载 ``aiohttp.ClientSession``,连接池 limit=10。 + + 会话在首次请求时创建并复用,避免每次请求新建连接。 + 若会话已关闭则重建。 + """ + if self._session is None or self._session.closed: + connector = aiohttp.TCPConnector( + limit=10, # 连接池上限 + limit_per_host=10, + ssl=self._ssl_context, + ) + client_timeout = aiohttp.ClientTimeout(total=self.timeout) + self._session = aiohttp.ClientSession( + connector=connector, + timeout=client_timeout, + auth=self._build_auth(), + headers={"Content-Type": "application/soap+xml;charset=UTF-8"}, + ) + return self._session + + # ------------------------------------------------------------------ + # SOAP Envelope 构造 + # ------------------------------------------------------------------ + def _build_envelope( + self, + action: str, + resource_uri: str, + selectors: Optional[dict] = None, + body: str = "", + ) -> str: + """构造 WS-Management SOAP envelope。 + + :param action: WS-Addressing Action URI + :param resource_uri: WS-Management 资源 URI + :param selectors: 选择器集合(如 ``{"ShellId": "..."}``), + 用于定位具体资源实例 + :param body: 内层 SOAP Body 的 XML 片段(已由调用方构造) + :returns: 完整的 SOAP envelope 字符串 + + 所有外部输入均经过 :func:`xml.sax.saxutils.escape`,避免 XML 注入。 + """ + # 构造选择器集合节点 + selector_set = "" + if selectors: + sel_items = "".join( + f'' + f"{xml_escape(str(value))}" + for name, value in selectors.items() + ) + selector_set = f"{sel_items}" + + message_id = f"uuid:{uuid.uuid4()}" + + envelope = f""" + + + {xml_escape(action)} + {xml_escape(self.endpoint)} + {xml_escape(resource_uri)} + {selector_set} + {message_id} + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + {body} +""" + return envelope + + # ------------------------------------------------------------------ + # 发送请求(带重试 + 指数退避) + # ------------------------------------------------------------------ + async def post(self, body: str) -> str: + """发送 SOAP 请求,返回响应体文本。 + + 带最大 ``max_retries`` 次重试与指数退避(1s, 2s, 4s ...), + 退避使用 ``asyncio.sleep``,绝不阻塞事件循环。 + + - HTTP 200:返回响应体 + - HTTP 5xx:触发重试 + - HTTP 4xx 等其他状态:返回响应体(含错误信息),由调用方解析 + """ + last_exc: Optional[Exception] = None + for attempt in range(self.max_retries): + try: + session = await self._get_session() + async with session.post( + self.endpoint, + data=body.encode("utf-8"), + headers={ + "Content-Type": "application/soap+xml;charset=UTF-8", + }, + ) as resp: + text = await resp.text() + if resp.status == 200: + return text + # 5xx 视为可重试错误 + if 500 <= resp.status < 600: + raise aiohttp.ClientResponseError( + resp.request_info, + resp.history, + status=resp.status, + message=f"WS-Management 服务端错误: {resp.status}", + ) + # 4xx 等不可重试错误,返回响应体供调用方解析 + logger.warning( + "WS-Management 请求返回非 200: status=%s", resp.status + ) + return text + except (aiohttp.ClientError, asyncio.TimeoutError) as e: + last_exc = e + logger.warning( + "WS-Management 请求失败 (尝试 %d/%d): %s", + attempt + 1, + self.max_retries, + e, + ) + if attempt < self.max_retries - 1: + # 指数退避:1s, 2s, 4s ... + backoff = 2 ** attempt + await asyncio.sleep(backoff) + + # 所有重试均失败 + raise WinRMTransportError( + f"WS-Management 请求在 {self.max_retries} 次重试后仍失败: {last_exc}" + ) + + async def close(self): + """关闭 aiohttp session,释放连接池资源。""" + if self._session is not None and not self._session.closed: + await self._session.close() + self._session = None diff --git a/apps/__init__.py b/apps/__init__.py deleted file mode 100755 index 093aa29..0000000 --- a/apps/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -2c2a应用模块 -""" diff --git a/apps/accounts/__init__.py b/apps/accounts/__init__.py deleted file mode 100755 index c217044..0000000 --- a/apps/accounts/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -2c2a用户管理应用 -""" -default_app_config = 'apps.accounts.apps.AccountsConfig' diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py deleted file mode 100755 index 290a9bb..0000000 --- a/apps/accounts/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.accounts.views_admin_users) diff --git a/apps/accounts/apps.py b/apps/accounts/apps.py deleted file mode 100755 index b6bae01..0000000 --- a/apps/accounts/apps.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -用户管理应用配置 -""" -from django.apps import AppConfig - - -class AccountsConfig(AppConfig): - """用户管理应用配置类""" - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.accounts' - verbose_name = '用户管理' - - def ready(self): - """应用就绪时执行的初始化操作""" - # 导入信号处理器 - try: - import apps.accounts.signals - except ImportError: - pass diff --git a/apps/accounts/captcha_service.py b/apps/accounts/captcha_service.py deleted file mode 100644 index f032ffc..0000000 --- a/apps/accounts/captcha_service.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging -from typing import Tuple, Optional -from django.http import HttpRequest - -logger = logging.getLogger(__name__) - - -class CaptchaValidationError(Exception): - def __init__(self, message: str): - self.message = message - super().__init__(message) - - -class CaptchaService: - - @staticmethod - def validate_captcha( - request: HttpRequest, - scene: str, - raise_exception: bool = False - ) -> Tuple[bool, Optional[str]]: - from apps.dashboard.models import SystemConfig - - provider = SystemConfig.get_config().get_captcha_config(scene=scene) - - try: - if provider == 'tianai': - return CaptchaService._validate_tianai(request) - else: - logger.debug(f"No captcha validation required for scene '{scene}', provider: {provider}") - return True, None - except CaptchaValidationError as e: - if raise_exception: - raise - return False, e.message - - @staticmethod - def _validate_tianai(request: HttpRequest) -> Tuple[bool, Optional[str]]: - token = request.POST.get('captcha_token') - - if not token: - logger.warning("Tianai captcha validation failed: missing token") - raise CaptchaValidationError('请完成验证码验证') - - from django_tianai_captcha.conf import get_captcha_application - app = get_captcha_application() - - is_valid = app.secondary_verification(token) - - if not is_valid: - logger.warning("Tianai captcha secondary verification failed") - raise CaptchaValidationError('验证码校验失败') - - logger.info("Tianai captcha validation succeeded") - return True, None - - -def validate_captcha(request: HttpRequest, scene: str) -> Tuple[bool, Optional[str]]: - return CaptchaService.validate_captcha(request, scene, raise_exception=False) diff --git a/apps/accounts/email_service.py b/apps/accounts/email_service.py deleted file mode 100644 index da4230e..0000000 --- a/apps/accounts/email_service.py +++ /dev/null @@ -1,136 +0,0 @@ -import smtplib -import ssl -import logging -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.utils import formataddr -from typing import Optional, List -from django.core.exceptions import ValidationError - -logger = logging.getLogger(__name__) - - -class EmailService: - def __init__( - self, - smtp_host: str, - smtp_port: int, - smtp_username: str, - smtp_password: str, - smtp_from_email: str, - smtp_encryption: str = "TLS", - smtp_from_name: Optional[str] = None, - ): - self.smtp_host = smtp_host - self.smtp_port = smtp_port - self.smtp_username = smtp_username - self.smtp_password = smtp_password - self.smtp_from_email = smtp_from_email - self.smtp_encryption = smtp_encryption - self.smtp_from_name = smtp_from_name - - def send_email( - self, - to_emails: List[str], - subject: str, - text_body: str, - html_body: Optional[str] = None, - from_email: Optional[str] = None, - ) -> bool: - if not all( - [ - self.smtp_host, - self.smtp_port, - self.smtp_username, - self.smtp_password, - self.smtp_from_email, - ] - ): - raise ValidationError("SMTP配置不完整") - - sender = from_email or self.smtp_from_email - sender_name = self.smtp_from_name - - msg = MIMEMultipart('alternative') - msg['From'] = formataddr((sender_name, sender)) if sender_name else sender - msg['To'] = ', '.join(to_emails) - msg['Subject'] = subject - - part1 = MIMEText(text_body, "plain", "utf-8") - msg.attach(part1) - - if html_body: - part2 = MIMEText(html_body, "html", "utf-8") - msg.attach(part2) - - timeout = 15 - - if self.smtp_encryption == "SSL": - server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port, timeout=timeout) - else: - server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=timeout) - - try: - server.ehlo() - - if self.smtp_encryption == "TLS": - context = ssl.create_default_context() - server.starttls(context=context) - server.ehlo() - - server.login(self.smtp_username, self.smtp_password) - text = msg.as_string() - server.sendmail(sender, to_emails, text) - finally: - try: - server.quit() - except smtplib.SMTPServerDisconnected: - pass - - return True - - @classmethod - def from_system_config(cls, config): - return cls( - smtp_host=config.smtp_host, - smtp_port=config.smtp_port, - smtp_username=config.smtp_username, - smtp_password=config.smtp_password, - smtp_from_email=config.smtp_from_email, - smtp_encryption=config.smtp_encryption, - smtp_from_name=config.smtp_from_name, - ) - - @classmethod - def from_effective_config(cls, ec): - """从 EffectiveConfig 实例创建 EmailService""" - return cls( - smtp_host=ec.smtp_host, - smtp_port=ec.smtp_port, - smtp_username=ec.smtp_username, - smtp_password=ec.smtp_password, - smtp_from_email=ec.smtp_from_email, - smtp_encryption=ec.smtp_encryption, - smtp_from_name=ec.smtp_from_name, - ) - - @staticmethod - def send_email_async( - to_emails, - subject, - text_body, - html_body=None, - from_email=None, - site_group_id=None, - ): - """异步发送邮件,通过 Celery 任务执行""" - from .tasks import send_email_task - - send_email_task.delay( - to_emails=to_emails, - subject=subject, - text_body=text_body, - html_body=html_body, - from_email=from_email, - site_group_id=site_group_id, - ) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py deleted file mode 100755 index 6790e6f..0000000 --- a/apps/accounts/forms.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -用户管理表单 -""" - -from django import forms -from django.contrib.auth.forms import UserCreationForm, PasswordChangeForm -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from .models import UserProfile -from config.demo_middleware import is_demo_mode - -User = get_user_model() - -MD_INPUT_CLASS = ( - "w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 " - "text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 " - "focus:ring-md-primary transition" -) -MD_SELECT_CLASS = ( - "w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 " - "text-md-on-surface appearance-none focus:outline-none focus:ring-2 " - "focus:ring-md-primary transition cursor-pointer" -) -MD_CHECKBOX_CLASS = ( - "w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 text-md-primary " - "focus:ring-md-primary focus:ring-2 transition cursor-pointer accent-md-primary" -) - - -class UserRegistrationForm(UserCreationForm): - """用户注册表单""" - - email = forms.EmailField( - required=True, - label=_("邮箱"), - widget=forms.EmailInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入邮箱地址")} - ), - ) - email_code = forms.CharField( - required=True, - label=_("邮箱验证码"), - widget=forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入邮箱收到的验证码")} - ), - ) - # 移除不需要的confirm_password字段,因为UserCreationForm使用password1和password2 - agree_terms = forms.BooleanField( - required=True, - label=_("我已阅读并同意服务条款和隐私政策"), - widget=forms.CheckboxInput(attrs={"class": MD_CHECKBOX_CLASS}), - ) - - class Meta: - model = User - fields = ("username", "email", "password1", "password2") - widgets = { - "username": forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入用户名")} - ), - } - - def clean_password1(self): - """在DEMO模式下,对密码不做复杂度要求""" - import os - - password = self.cleaned_data.get("password1") - if os.environ.get("2C2A_DEMO", "").lower() == "1": - # 在DEMO模式下,接受任何密码 - return password - else: - # 在非DEMO模式下,保持原有验证逻辑 - return password - - def clean_password2(self): - """在DEMO模式下,对密码不做复杂度要求""" - import os - - password1 = self.cleaned_data.get("password1") - password2 = self.cleaned_data.get("password2") - - if password1 and password2 and password1 != password2: - raise forms.ValidationError(_("两次输入的密码不一致")) - - if os.environ.get("2C2A_DEMO", "").lower() == "1": - # 在DEMO模式下,接受任何密码 - return password2 - else: - # 在非DEMO模式下,保持原有验证逻辑 - return password2 - - def clean_email(self): - """验证邮箱后缀""" - email = self.cleaned_data.get("email") - - from utils.site_group import get_effective_config - - ec = get_effective_config() - - if not ec.is_email_suffix_allowed(email): - email_suffix = "@" + email.split("@")[1] if "@" in email else "" - suffix_data = ec.get_email_suffix_lists() - if suffix_data["whitelist"]: - raise forms.ValidationError(f"邮箱后缀 {email_suffix} 不在允许的列表中") - else: - raise forms.ValidationError(f"邮箱后缀 {email_suffix} 已被禁止使用") - - from django.core.validators import validate_email - from django.core.exceptions import ValidationError - - try: - validate_email(email) - except ValidationError: - raise forms.ValidationError("请输入有效的邮箱地址") - - from django.contrib.auth import get_user_model - - User = get_user_model() - if User.objects.filter(email=email).exists(): - raise forms.ValidationError("该邮箱已被注册") - - return email - - def clean_agree_terms(self): - """验证用户是否同意条款""" - agree_terms = self.cleaned_data.get("agree_terms") - if not agree_terms: - raise forms.ValidationError(_("您必须同意服务条款和隐私政策才能注册")) - return agree_terms - - def save(self, commit=True): - """保存用户""" - user = super().save(commit=False) - user.email = self.cleaned_data["email"] - if commit: - user.save() - # 创建用户资料 - from .models import UserProfile - - UserProfile.objects.create(user=user) - return user - - -class UserUpdateForm(forms.ModelForm): - """用户信息更新表单""" - - class Meta: - model = User - fields = ("username", "email") - widgets = { - "username": forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "readonly": "readonly"} - ), - "email": forms.EmailInput( - attrs={ - "class": MD_INPUT_CLASS, - "readonly": "readonly", # 邮箱不允许修改 - } - ), - } - - -class DemoPasswordChangeForm(PasswordChangeForm): - """DEMO模式下的密码更改表单 - 禁止更改密码""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if is_demo_mode(): - # 在DEMO模式下,禁用所有字段 - for field_name in self.fields: - self.fields[field_name].widget.attrs["disabled"] = True - # 添加错误信息 - self.fields["old_password"].help_text = "DEMO模式下不允许修改密码" - - def clean(self): - """验证表单""" - cleaned_data = super().clean() - - if is_demo_mode(): - raise forms.ValidationError("DEMO模式下不允许修改密码") - - return cleaned_data - - -class UserLoginForm(forms.Form): - """用户登录表单""" - - username = forms.CharField( - label=_("用户名"), - widget=forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入用户名")} - ), - ) - password = forms.CharField( - label=_("密码"), - widget=forms.PasswordInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入密码")} - ), - ) - remember = forms.BooleanField( - required=False, - label=_("记住我"), - widget=forms.CheckboxInput(attrs={"class": MD_CHECKBOX_CLASS}), - ) - - -class UserProfileForm(forms.ModelForm): - """用户资料表单""" - - class Meta: - model = UserProfile - fields = ( - "nickname", - "gender", - "birthday", - "location", - "bio", - "email_notification", - "system_notification", - ) - widgets = { - "nickname": forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入昵称")} - ), - "gender": forms.Select(attrs={"class": MD_SELECT_CLASS}), - "birthday": forms.DateInput( - attrs={"class": MD_INPUT_CLASS, "type": "date"} - ), - "location": forms.TextInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入所在地")} - ), - "bio": forms.Textarea( - attrs={ - "class": MD_INPUT_CLASS, - "rows": 4, - "placeholder": _("请输入个人简介"), - } - ), - "email_notification": forms.CheckboxInput( - attrs={"class": MD_CHECKBOX_CLASS} - ), - "system_notification": forms.CheckboxInput( - attrs={"class": MD_CHECKBOX_CLASS} - ), - } - - -class PasswordChangeForm(forms.Form): - """密码修改表单""" - - old_password = forms.CharField( - label=_("当前密码"), - widget=forms.PasswordInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入当前密码")} - ), - ) - new_password = forms.CharField( - label=_("新密码"), - widget=forms.PasswordInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请输入新密码")} - ), - min_length=8, - ) - confirm_password = forms.CharField( - label=_("确认密码"), - widget=forms.PasswordInput( - attrs={"class": MD_INPUT_CLASS, "placeholder": _("请再次输入新密码")} - ), - min_length=8, - ) - - def clean(self): - cleaned_data = super().clean() - new_password = cleaned_data.get("new_password") - confirm_password = cleaned_data.get("confirm_password") - - if new_password and confirm_password and new_password != confirm_password: - raise forms.ValidationError(_("两次输入的密码不一致")) - - return cleaned_data diff --git a/apps/accounts/forms_admin.py b/apps/accounts/forms_admin.py deleted file mode 100644 index 7963630..0000000 --- a/apps/accounts/forms_admin.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -超管后台 - 用户管理表单 - -包含创建用户、编辑用户、重置密码三个表单。 -""" - -from django import forms -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.contrib.auth.password_validation import validate_password - -from apps.accounts.models import GroupProfile - -User = get_user_model() - - -class GroupChoiceField(forms.ModelChoiceField): - def label_from_instance(self, obj): - try: - profile = obj.profile - desc = profile.description - if desc: - return f'{obj.name} - {desc}' - except GroupProfile.DoesNotExist: - pass - return obj.name - - -_SELECT_ATTRS = { - 'class': ( - 'w-full bg-white/5 backdrop-blur-xl border border-white/10 ' - 'rounded-md px-4 py-3 text-white appearance-none ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition cursor-pointer' - ), -} - - -class AdminUserCreateForm(forms.ModelForm): - - password1 = forms.CharField( - label='密码', - widget=forms.PasswordInput( - attrs={'autocomplete': 'new-password'} - ), - help_text='密码需满足安全策略要求', - ) - password2 = forms.CharField( - label='确认密码', - widget=forms.PasswordInput( - attrs={'autocomplete': 'new-password'} - ), - help_text='再次输入密码以确认', - ) - groups = GroupChoiceField( - queryset=Group.objects.select_related('profile').all(), - required=False, - widget=forms.Select(attrs=_SELECT_ATTRS), - label='用户组', - ) - - class Meta: - model = User - fields = [ - 'username', 'email', 'first_name', 'last_name', - 'groups', - ] - - def clean_password2(self): - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') - if password1 and password2 and password1 != password2: - raise forms.ValidationError('两次输入的密码不一致') - validate_password(password2) - return password2 - - def save(self, commit=True): - user = super().save(commit=False) - user.set_password(self.cleaned_data['password1']) - group = self.cleaned_data.get('groups') - if group: - try: - user.is_staff = group.profile.auto_staff - except GroupProfile.DoesNotExist: - user.is_staff = False - else: - user.is_staff = False - if commit: - user.save() - if group: - user.groups.set([group]) - else: - user.groups.clear() - return user - - -class AdminUserUpdateForm(forms.ModelForm): - - groups = GroupChoiceField( - queryset=Group.objects.select_related('profile').all(), - required=False, - widget=forms.Select(attrs=_SELECT_ATTRS), - label='用户组', - ) - - class Meta: - model = User - fields = [ - 'username', 'email', 'first_name', 'last_name', - 'groups', - ] - - def save(self, commit=True): - user = super().save(commit=False) - group = self.cleaned_data.get('groups') - if group: - try: - user.is_staff = group.profile.auto_staff - except GroupProfile.DoesNotExist: - user.is_staff = False - else: - user.is_staff = False - if commit: - user.save() - if group: - user.groups.set([group]) - else: - user.groups.clear() - return user - - -class AdminPasswordResetForm(forms.Form): - """超管重置用户密码表单""" - - new_password1 = forms.CharField( - label='新密码', - widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), - help_text='密码需满足安全策略要求', - ) - new_password2 = forms.CharField( - label='确认新密码', - widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), - help_text='再次输入新密码以确认', - ) - - def clean_new_password2(self): - password1 = self.cleaned_data.get('new_password1') - password2 = self.cleaned_data.get('new_password2') - if password1 and password2 and password1 != password2: - raise forms.ValidationError('两次输入的密码不一致') - validate_password(password2) - return password2 diff --git a/apps/accounts/forms_superadmin.py b/apps/accounts/forms_superadmin.py deleted file mode 100644 index 837d846..0000000 --- a/apps/accounts/forms_superadmin.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -超级管理员表单 - -用于超级管理员分配提供商给主机和主机组。 -""" - -from django import forms -from django.contrib.auth import get_user_model - -from utils.provider import PROVIDER_GROUP_NAME - -User = get_user_model() - - -def _get_provider_queryset(): - """获取属于'提供商'组的用户查询集""" - return User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_active=True, - ).order_by('username') - - -class HostProviderAssignForm(forms.Form): - """ - 主机提供商分配表单 - - 允许超级管理员为指定主机分配多个提供商。 - """ - providers = forms.ModelMultipleChoiceField( - queryset=_get_provider_queryset(), - required=False, - label='管理提供商', - help_text='选择可以管理此主机的提供商用户,留空表示不分配任何提供商', - widget=forms.SelectMultiple(attrs={ - 'size': 12, - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - ) - - def __init__(self, *args, **kwargs): - self.host = kwargs.pop('host', None) - super().__init__(*args, **kwargs) - if self.host: - self.fields['providers'].initial = self.host.providers.all() - - -class HostGroupProviderAssignForm(forms.Form): - """ - 主机组提供商分配表单 - - 允许超级管理员为指定主机组分配多个提供商。 - """ - providers = forms.ModelMultipleChoiceField( - queryset=_get_provider_queryset(), - required=False, - label='管理提供商', - help_text='选择可以管理此主机组的提供商用户,留空表示不分配任何提供商', - widget=forms.SelectMultiple(attrs={ - 'size': 12, - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - ) - - def __init__(self, *args, **kwargs): - self.hostgroup = kwargs.pop('hostgroup', None) - super().__init__(*args, **kwargs) - if self.hostgroup: - self.fields['providers'].initial = self.hostgroup.providers.all() diff --git a/apps/accounts/management/commands/create_demo_superuser.py b/apps/accounts/management/commands/create_demo_superuser.py deleted file mode 100755 index 98cdea7..0000000 --- a/apps/accounts/management/commands/create_demo_superuser.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -创建DEMO超级管理员用户命令 -""" -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -import os - - -class Command(BaseCommand): - help = '在DEMO模式下创建超级管理员用户' - - def add_arguments(self, parser): - parser.add_argument( - '--username', - type=str, - default='SuperAdmin', - help='超级管理员用户名 (默认: SuperAdmin)' - ) - parser.add_argument( - '--email', - type=str, - default='superadmin@example.com', - help='超级管理员邮箱 (默认: superadmin@example.com)' - ) - parser.add_argument( - '--password', - type=str, - default='DemoSuperAdmin123!', - help='超级管理员密码 (默认: DemoSuperAdmin123!)' - ) - - def handle(self, *args, **options): - if os.environ.get('2C2A_DEMO', '').lower() != '1': - self.stdout.write( - self.style.ERROR('请设置 2C2A_DEMO=1 环境变量以在DEMO模式下运行此命令') - ) - return - - User = get_user_model() - - username = options['username'] - email = options['email'] - password = options['password'] - - # 检查用户是否已存在 - if User.objects.filter(username=username).exists(): - self.stdout.write( - self.style.WARNING(f'用户 {username} 已存在,跳过创建') - ) - return - - # 创建超级用户 - user = User.objects.create_superuser( - username=username, - email=email, - password=password, - first_name='Demo', - last_name='SuperAdmin' - ) - - self.stdout.write( - self.style.SUCCESS( - f'成功创建超级管理员用户: {username}\n' - f'用户名: {username}\n' - f'邮箱: {email}\n' - f'密码: {password}' - ) - ) - self.stdout.write( - self.style.WARNING( - '注意:在DEMO模式下,此用户拥有完整的超级用户权限' - ) - ) \ No newline at end of file diff --git a/apps/accounts/management/commands/setup_demo_users.py b/apps/accounts/management/commands/setup_demo_users.py deleted file mode 100755 index 8ef3e36..0000000 --- a/apps/accounts/management/commands/setup_demo_users.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -DEMO模式用户初始化命令 -""" -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType -import os - - -class Command(BaseCommand): - help = '初始化DEMO模式下的用户和权限' - - def handle(self, *args, **options): - if os.environ.get('2C2A_DEMO', '').lower() != '1': - self.stdout.write( - self.style.WARNING('非DEMO模式,跳过用户初始化') - ) - return - - User = get_user_model() - - # 创建User用户 - user, created = User.objects.get_or_create( - username='User', - defaults={ - 'email': 'user@example.com', - 'first_name': 'Demo', - 'last_name': 'User', - 'is_active': True, - } - ) - if created: - user.set_password('demo_user_password') - user.save() - self.stdout.write( - self.style.SUCCESS('成功创建User用户') - ) - else: - self.stdout.write( - self.style.WARNING('User用户已存在') - ) - - # 创建Admin用户 - admin, created = User.objects.get_or_create( - username='Admin', - defaults={ - 'email': 'admin@example.com', - 'first_name': 'Demo', - 'last_name': 'Admin', - 'is_staff': True, - 'is_active': True, - } - ) - if created: - admin.set_password('demo_admin_password') - # 分配特定权限 - self.assign_demo_permissions(admin) - admin.save() - self.stdout.write( - self.style.SUCCESS('成功创建Admin用户并分配权限') - ) - else: - self.stdout.write( - self.style.WARNING('Admin用户已存在') - ) - - def assign_demo_permissions(self, user): - """ - 为DEMO模式下的Admin用户分配特定权限 - """ - permissions = [ - # View登录日志 - ('accounts', 'loginlog', 'view'), - # View开户申请 - ('operations', 'accountopeningrequest', 'view'), - # Change开户申请 - ('operations', 'accountopeningrequest', 'change'), - # View云电脑用户 - ('operations', 'cloudcomputeruser', 'view'), - # Change云电脑用户 - ('operations', 'cloudcomputeruser', 'change'), - # View产品 - ('operations', 'product', 'view'), - ] - - for app_label, model, perm_action in permissions: - try: - content_type = ContentType.objects.get(app_label=app_label, model=model) - permission_codename = f'{perm_action}_{model}' - permission = Permission.objects.get(content_type=content_type, codename=permission_codename) - user.user_permissions.add(permission) - except ContentType.DoesNotExist: - self.stdout.write( - self.style.ERROR(f"ContentType not found: {app_label}.{model}") - ) - except Permission.DoesNotExist: - self.stdout.write( - self.style.ERROR(f"Permission not found: {app_label}.{model}.{perm_action}") - ) \ No newline at end of file diff --git a/apps/accounts/management/commands/setup_provider_group.py b/apps/accounts/management/commands/setup_provider_group.py deleted file mode 100644 index c73060b..0000000 --- a/apps/accounts/management/commands/setup_provider_group.py +++ /dev/null @@ -1,294 +0,0 @@ -""" -设置提供商组和管理权限的管理命令 - -提供商组权限说明: -- 可以管理自己创建的产品、云电脑用户 -- 可以查看和处理开户申请 -- 不能访问系统配置、主机管理等敏感功能 - -使用方法: -1. 创建提供商组并设置权限: - python manage.py setup_provider_group - -2. 将用户添加到提供商组: - python manage.py setup_provider_group --add-user username1 username2 - -3. 从提供商组移除用户: - python manage.py setup_provider_group --remove-user username1 - -4. 列出提供商组中的所有用户: - python manage.py setup_provider_group --list-users -""" -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType - -User = get_user_model() - - -class Command(BaseCommand): - help = '设置提供商组和默认权限,管理组成员' - - PROVIDER_GROUP_NAME = '主机提供商' - - PROVIDER_PERMISSIONS = [ - # 产品权限 - 提供商可以创建和管理自己的产品 - ('operations', 'product', 'add'), - ('operations', 'product', 'view'), - ('operations', 'product', 'change'), - - # 开户申请权限 - 提供商可以查看和处理申请 - ('operations', 'accountopeningrequest', 'view'), - ('operations', 'accountopeningrequest', 'change'), - - # 云电脑用户权限 - 提供商可以管理自己产品下的用户 - ('operations', 'cloudcomputeruser', 'view'), - ('operations', 'cloudcomputeruser', 'change'), - - # 系统任务权限 - 只读查看 - ('operations', 'systemtask', 'view'), - - # 主机权限 - 提供商可以创建和管理自己的主机 - ('hosts', 'host', 'add'), - ('hosts', 'host', 'view'), - ('hosts', 'host', 'change'), - ('hosts', 'host', 'delete'), - - # 主机组权限 - 提供商可以创建和管理自己的主机组 - ('hosts', 'hostgroup', 'add'), - ('hosts', 'hostgroup', 'view'), - ('hosts', 'hostgroup', 'change'), - ('hosts', 'hostgroup', 'delete'), - - # 用户资料权限 - 查看自己的资料 - ('accounts', 'userprofile', 'view'), - ('accounts', 'userprofile', 'change'), - ] - - def add_arguments(self, parser): - parser.add_argument( - '--add-user', - nargs='+', - type=str, - help='将指定用户添加到提供商组', - ) - parser.add_argument( - '--remove-user', - nargs='+', - type=str, - help='从提供商组移除指定用户', - ) - parser.add_argument( - '--list-users', - action='store_true', - help='列出提供商组中的所有用户', - ) - parser.add_argument( - '--force', - action='store_true', - help='强制更新权限(即使权限已存在)', - ) - - def handle(self, *args, **options): - add_users = options.get('add_user') - remove_users = options.get('remove_user') - list_users = options.get('list_users') - force = options.get('force', False) - - group = self._get_or_create_group() - - if add_users: - self._add_users_to_group(group, add_users) - elif remove_users: - self._remove_users_from_group(group, remove_users) - elif list_users: - self._list_group_users(group) - else: - self._setup_permissions(group, force) - - def _get_or_create_group(self): - """获取或创建提供商组""" - group, created = Group.objects.get_or_create( - name=self.PROVIDER_GROUP_NAME - ) - - if created: - self.stdout.write( - self.style.SUCCESS( - f'成功创建组: {self.PROVIDER_GROUP_NAME}' - ) - ) - from apps.accounts.models import GroupProfile - GroupProfile.objects.get_or_create( - group=group, - defaults={ - 'is_default': True, - 'description': '主机提供商,可管理分配给自己的主机、产品、开户申请和工单。', - 'sort_order': 1, - } - ) - else: - self.stdout.write( - self.style.WARNING(f'组已存在: {self.PROVIDER_GROUP_NAME}') - ) - - return group - - def _setup_permissions(self, group, force=False): - """设置权限""" - permissions_added = 0 - permissions_skipped = 0 - - for app_label, model_name, action in self.PROVIDER_PERMISSIONS: - try: - content_type = ContentType.objects.get( - app_label=app_label, - model=model_name - ) - codename = f'{action}_{model_name}' - permission = Permission.objects.get( - content_type=content_type, - codename=codename - ) - - if force or permission not in group.permissions.all(): - group.permissions.add(permission) - permissions_added += 1 - self.stdout.write( - self.style.SUCCESS(f' 添加权限: {codename}') - ) - else: - permissions_skipped += 1 - - except ContentType.DoesNotExist: - self.stdout.write( - self.style.ERROR( - f' ContentType不存在: ' - f'{app_label}.{model_name}' - ) - ) - except Permission.DoesNotExist: - self.stdout.write( - self.style.ERROR( - f' Permission不存在: ' - f'{app_label}.{model_name}.{action}' - ) - ) - - self.stdout.write( - self.style.SUCCESS( - f'\n设置完成!新增权限: {permissions_added}, ' - f'已存在: {permissions_skipped}' - ) - ) - - self._print_usage_info() - - def _add_users_to_group(self, group, usernames): - """将用户添加到提供商组""" - added_count = 0 - not_found = [] - - for username in usernames: - try: - user = User.objects.get(username=username) - if group not in user.groups.all(): - user.groups.add(group) - added_count += 1 - self.stdout.write( - self.style.SUCCESS(f' 已添加用户: {username}') - ) - else: - self.stdout.write( - self.style.WARNING(f' 用户已在组中: {username}') - ) - except User.DoesNotExist: - not_found.append(username) - self.stdout.write( - self.style.ERROR(f' 用户不存在: {username}') - ) - - self.stdout.write( - self.style.SUCCESS(f'\n成功添加 {added_count} 个用户到提供商组') - ) - - if not_found: - self.stdout.write( - self.style.WARNING(f'未找到的用户: {", ".join(not_found)}') - ) - - def _remove_users_from_group(self, group, usernames): - """从提供商组移除用户""" - removed_count = 0 - not_found = [] - - for username in usernames: - try: - user = User.objects.get(username=username) - if group in user.groups.all(): - user.groups.remove(group) - removed_count += 1 - self.stdout.write( - self.style.SUCCESS(f' 已移除用户: {username}') - ) - else: - self.stdout.write( - self.style.WARNING(f' 用户不在组中: {username}') - ) - except User.DoesNotExist: - not_found.append(username) - self.stdout.write( - self.style.ERROR(f' 用户不存在: {username}') - ) - - self.stdout.write( - self.style.SUCCESS(f'\n成功从提供商组移除 {removed_count} 个用户') - ) - - if not_found: - self.stdout.write( - self.style.WARNING(f'未找到的用户: {", ".join(not_found)}') - ) - - def _list_group_users(self, group): - """列出提供商组中的所有用户""" - users = group.user_set.all() - - if users.exists(): - self.stdout.write( - self.style.SUCCESS(f'\n提供商组中的用户 ({users.count()} 个):') - ) - for user in users: - self.stdout.write(f' - {user.username} ({user.email})') - else: - self.stdout.write( - self.style.WARNING('\n提供商组中暂无用户') - ) - - def _print_usage_info(self): - """打印使用说明""" - self.stdout.write('\n提供商组权限说明:') - self.stdout.write(' - 可以创建和管理自己的主机和主机组') - self.stdout.write(' - 可以创建和管理自己的产品') - self.stdout.write(' - 可以查看和处理开户申请') - self.stdout.write(' - 可以管理自己产品下的云电脑用户') - self.stdout.write(' - 不能访问系统配置等敏感功能') - self.stdout.write('\n数据隔离说明:') - self.stdout.write(' - 提供商只能看到自己创建的数据') - self.stdout.write(' - 不同提供商之间的数据完全隔离') - self.stdout.write(' - 超级用户可以管理所有提供商的数据') - self.stdout.write('\n使用方法:') - self.stdout.write(' python manage.py setup_provider_group') - self.stdout.write( - ' python manage.py setup_provider_group ' - '--add-user username1 username2' - ) - self.stdout.write( - ' python manage.py setup_provider_group ' - '--remove-user username1' - ) - self.stdout.write( - ' python manage.py setup_provider_group ' - '--list-users' - ) diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py deleted file mode 100755 index dce3bc0..0000000 --- a/apps/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,104 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:24 - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('phone', models.CharField(blank=True, help_text='用户的手机号码', max_length=20, null=True, verbose_name='手机号码')), - ('avatar', models.ImageField(blank=True, help_text='用户头像图片', null=True, upload_to='avatars/', verbose_name='头像')), - ('is_verified', models.BooleanField(default=False, help_text='用户邮箱是否已验证', verbose_name='已验证')), - ('last_login_ip', models.GenericIPAddressField(blank=True, help_text='用户最后一次登录的IP地址', null=True, verbose_name='最后登录IP')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='用户账号创建时间', verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='用户信息最后更新时间', verbose_name='更新时间')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': '用户', - 'verbose_name_plural': '用户', - 'ordering': ['-created_at'], - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='UserProfile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nickname', models.CharField(blank=True, help_text='用户昵称', max_length=50, verbose_name='昵称')), - ('gender', models.CharField(blank=True, choices=[('male', '男'), ('female', '女'), ('other', '其他')], help_text='用户性别', max_length=10, verbose_name='性别')), - ('birthday', models.DateField(blank=True, help_text='用户生日', null=True, verbose_name='生日')), - ('location', models.CharField(blank=True, help_text='用户所在地', max_length=100, verbose_name='所在地')), - ('bio', models.TextField(blank=True, help_text='用户个人简介', verbose_name='个人简介')), - ('email_notification', models.BooleanField(default=True, help_text='是否接收邮件通知', verbose_name='邮件通知')), - ('system_notification', models.BooleanField(default=True, help_text='是否接收系统通知', verbose_name='系统通知')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='资料创建时间', verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='资料最后更新时间', verbose_name='更新时间')), - ('user', models.OneToOneField(help_text='关联的用户', on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '用户资料', - 'verbose_name_plural': '用户资料', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='LoginLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ip_address', models.GenericIPAddressField(help_text='登录时的IP地址', verbose_name='IP地址')), - ('user_agent', models.TextField(blank=True, help_text='登录时的浏览器信息', verbose_name='用户代理')), - ('login_type', models.CharField(choices=[('web', '网页登录'), ('api', 'API登录'), ('other', '其他')], default='web', help_text='用户的登录方式', max_length=20, verbose_name='登录方式')), - ('status', models.CharField(choices=[('success', '成功'), ('failed', '失败')], help_text='登录是否成功', max_length=20, verbose_name='登录状态')), - ('failure_reason', models.CharField(blank=True, help_text='登录失败的原因', max_length=200, verbose_name='失败原因')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='登录发生的时间', verbose_name='登录时间')), - ('user', models.ForeignKey(help_text='登录的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='login_logs', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '登录日志', - 'verbose_name_plural': '登录日志', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['user'], name='accounts_lo_user_id_20fa14_idx'), models.Index(fields=['ip_address'], name='accounts_lo_ip_addr_51fef5_idx'), models.Index(fields=['status'], name='accounts_lo_status_0f4a4d_idx'), models.Index(fields=['created_at'], name='accounts_lo_created_6efabd_idx')], - }, - ), - migrations.AddIndex( - model_name='user', - index=models.Index(fields=['email'], name='accounts_us_email_74c8d6_idx'), - ), - migrations.AddIndex( - model_name='user', - index=models.Index(fields=['phone'], name='accounts_us_phone_f54457_idx'), - ), - migrations.AddIndex( - model_name='user', - index=models.Index(fields=['is_active'], name='accounts_us_is_acti_a5841d_idx'), - ), - ] diff --git a/apps/accounts/migrations/0002_groupprofile_model.py b/apps/accounts/migrations/0002_groupprofile_model.py deleted file mode 100644 index da29160..0000000 --- a/apps/accounts/migrations/0002_groupprofile_model.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-02 16:03 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='GroupProfile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_default', models.BooleanField(default=False, help_text='默认组不可删除,系统预设角色', verbose_name='默认组')), - ('description', models.TextField(blank=True, help_text='用户组的功能描述', verbose_name='描述')), - ('sort_order', models.IntegerField(default=0, help_text='数值越小排序越靠前', verbose_name='排序')), - ('group', models.OneToOneField(help_text='关联的Django用户组', on_delete=django.db.models.deletion.CASCADE, related_name='profile', to='auth.group', verbose_name='用户组')), - ], - options={ - 'verbose_name': '用户组配置', - 'verbose_name_plural': '用户组配置', - 'ordering': ['sort_order', 'group__name'], - }, - ), - ] diff --git a/apps/accounts/migrations/0003_default_groups.py b/apps/accounts/migrations/0003_default_groups.py deleted file mode 100644 index 3dc8d54..0000000 --- a/apps/accounts/migrations/0003_default_groups.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.db import migrations - - -DEFAULT_GROUPS = [ - { - 'name': '超管', - 'description': '系统超级管理员,拥有所有权限。对应Django的is_superuser属性。', - 'is_default': True, - 'sort_order': 0, - }, - { - 'name': '主机提供商', - 'description': '主机提供商,可管理分配给自己的主机、产品、开户申请和工单。', - 'is_default': True, - 'sort_order': 1, - 'rename_from': '提供商', - }, - { - 'name': '云电脑审批', - 'description': '云电脑审批人员,可管理开户申请和工单系统。', - 'is_default': True, - 'sort_order': 2, - }, - { - 'name': '工单技术客服', - 'description': '工单技术客服,仅可管理工单系统。', - 'is_default': True, - 'sort_order': 3, - }, - { - 'name': '普通用户', - 'description': '普通用户,可使用已授权的产品和服务。', - 'is_default': True, - 'sort_order': 4, - }, -] - - -def create_default_groups(apps, schema_editor): - Group = apps.get_model('auth', 'Group') - GroupProfile = apps.get_model('accounts', 'GroupProfile') - - for group_data in DEFAULT_GROUPS: - rename_from = group_data.pop('rename_from', None) - - if rename_from: - existing = Group.objects.filter(name=rename_from).first() - if existing: - existing.name = group_data['name'] - existing.save() - group = existing - else: - group, _ = Group.objects.get_or_create( - name=group_data['name'] - ) - else: - group, _ = Group.objects.get_or_create( - name=group_data['name'] - ) - - GroupProfile.objects.get_or_create( - group=group, - defaults={ - 'is_default': group_data['is_default'], - 'description': group_data['description'], - 'sort_order': group_data['sort_order'], - } - ) - - -def reverse_default_groups(apps, schema_editor): - Group = apps.get_model('auth', 'Group') - GroupProfile = apps.get_model('accounts', 'GroupProfile') - - for group_data in DEFAULT_GROUPS: - group = Group.objects.filter(name=group_data['name']).first() - if group: - GroupProfile.objects.filter(group=group).delete() - - provider_group = Group.objects.filter(name='主机提供商').first() - if provider_group: - provider_group.name = '提供商' - provider_group.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_groupprofile_model'), - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.RunPython( - create_default_groups, - reverse_default_groups, - ), - ] diff --git a/apps/accounts/migrations/0004_normal_user_group.py b/apps/accounts/migrations/0004_normal_user_group.py deleted file mode 100644 index 0482936..0000000 --- a/apps/accounts/migrations/0004_normal_user_group.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import migrations - - -def add_normal_user_group(apps, schema_editor): - Group = apps.get_model('auth', 'Group') - GroupProfile = apps.get_model('accounts', 'GroupProfile') - - group, _ = Group.objects.get_or_create(name='普通用户') - GroupProfile.objects.get_or_create( - group=group, - defaults={ - 'is_default': True, - 'description': '普通用户,可使用已授权的产品和服务。', - 'sort_order': 4, - } - ) - - -def remove_normal_user_group(apps, schema_editor): - Group = apps.get_model('auth', 'Group') - GroupProfile = apps.get_model('accounts', 'GroupProfile') - - group = Group.objects.filter(name='普通用户').first() - if group: - GroupProfile.objects.filter(group=group).delete() - group.delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0003_default_groups'), - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.RunPython( - add_normal_user_group, - remove_normal_user_group, - ), - ] diff --git a/apps/accounts/migrations/0005_auto_staff.py b/apps/accounts/migrations/0005_auto_staff.py deleted file mode 100644 index 5971110..0000000 --- a/apps/accounts/migrations/0005_auto_staff.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.db import migrations, models - - -AUTO_STAFF_GROUPS = ['超管', '主机提供商', '云电脑审批', '工单技术客服'] - - -def set_auto_staff(apps, schema_editor): - GroupProfile = apps.get_model('accounts', 'GroupProfile') - GroupProfile.objects.filter( - group__name__in=AUTO_STAFF_GROUPS - ).update(auto_staff=True) - - User = apps.get_model('accounts', 'User') - for user in User.objects.filter(is_superuser=True, is_staff=False): - user.is_staff = True - user.save(update_fields=['is_staff']) - - for profile in GroupProfile.objects.filter(auto_staff=True): - for user in profile.group.user_set.filter(is_staff=False): - user.is_staff = True - user.save(update_fields=['is_staff']) - - -def unset_auto_staff(apps, schema_editor): - GroupProfile = apps.get_model('accounts', 'GroupProfile') - GroupProfile.objects.filter(auto_staff=True).update(auto_staff=False) - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0004_normal_user_group'), - ] - - operations = [ - migrations.AddField( - model_name='groupprofile', - name='auto_staff', - field=models.BooleanField( - default=False, - help_text='勾选后,属于该组的用户将自动获得员工身份(is_staff)', - verbose_name='自动员工', - ), - ), - migrations.RunPython(set_auto_staff, unset_auto_staff), - ] diff --git a/apps/accounts/migrations/0006_registrationlink.py b/apps/accounts/migrations/0006_registrationlink.py deleted file mode 100644 index 24dfc56..0000000 --- a/apps/accounts/migrations/0006_registrationlink.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-04 11:17 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('accounts', '0005_auto_staff'), - ] - - operations = [ - migrations.CreateModel( - name='RegistrationLink', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(default=uuid.uuid4, help_text='注册链接的唯一标识', max_length=64, unique=True, verbose_name='令牌')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('used', models.BooleanField(default=False, help_text='此注册链接是否已被使用', verbose_name='已使用')), - ('used_at', models.DateTimeField(blank=True, null=True, verbose_name='使用时间')), - ('expires_at', models.DateTimeField(blank=True, help_text='留空表示永不过期', null=True, verbose_name='过期时间')), - ('note', models.CharField(blank=True, help_text='管理员备注,仅后台可见', max_length=200, verbose_name='备注')), - ('created_by', models.ForeignKey(help_text='创建此注册链接的管理员', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_registration_links', to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ('group', models.ForeignKey(help_text='通过此链接注册的用户将自动加入该用户组', on_delete=django.db.models.deletion.CASCADE, related_name='registration_links', to='auth.group', verbose_name='注册后用户组')), - ('used_by', models.ForeignKey(blank=True, help_text='使用此链接注册的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='used_registration_link', to=settings.AUTH_USER_MODEL, verbose_name='使用者')), - ], - options={ - 'verbose_name': '注册链接', - 'verbose_name_plural': '注册链接', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/apps/accounts/migrations/0007_add_registrationlink_max_uses.py b/apps/accounts/migrations/0007_add_registrationlink_max_uses.py deleted file mode 100644 index a4c7c0f..0000000 --- a/apps/accounts/migrations/0007_add_registrationlink_max_uses.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -def fix_existing_used_links(apps, schema_editor): - RegistrationLink = apps.get_model('accounts', 'RegistrationLink') - RegistrationLink.objects.filter(used=True).update(used_count=1) - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0006_registrationlink"), - ] - - operations = [ - migrations.AddField( - model_name="registrationlink", - name="max_uses", - field=models.IntegerField( - default=1, - help_text="设置为0表示不限制使用次数", - verbose_name="最大使用次数", - ), - ), - migrations.AddField( - model_name="registrationlink", - name="used_count", - field=models.IntegerField(default=0, verbose_name="已使用次数"), - ), - migrations.AlterField( - model_name="registrationlink", - name="used_at", - field=models.DateTimeField( - blank=True, null=True, verbose_name="最后使用时间" - ), - ), - migrations.AlterField( - model_name="registrationlink", - name="used_by", - field=models.ForeignKey( - blank=True, - help_text="最后使用此链接注册的用户", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="used_registration_link", - to=settings.AUTH_USER_MODEL, - verbose_name="最后使用者", - ), - ), - migrations.RunPython(fix_existing_used_links, migrations.RunPython.noop), - ] diff --git a/apps/accounts/migrations/0008_user_site_groups.py b/apps/accounts/migrations/0008_user_site_groups.py deleted file mode 100644 index 67775ad..0000000 --- a/apps/accounts/migrations/0008_user_site_groups.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 13:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0014_sitegroup_sitegrouphostname_and_more"), - ("accounts", "0007_add_registrationlink_max_uses"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="site_groups", - field=models.ManyToManyField( - blank=True, - help_text="用户所属的站点组,决定用户可见的数据范围", - related_name="members", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ] diff --git a/apps/accounts/migrations/0009_useremail.py b/apps/accounts/migrations/0009_useremail.py deleted file mode 100644 index a68c121..0000000 --- a/apps/accounts/migrations/0009_useremail.py +++ /dev/null @@ -1,83 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 06:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0008_user_site_groups"), - ] - - operations = [ - migrations.CreateModel( - name="UserEmail", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "email", - models.EmailField( - help_text="绑定的邮箱地址", - max_length=254, - verbose_name="邮箱地址", - ), - ), - ( - "is_primary", - models.BooleanField( - default=False, - help_text="是否为用户的主邮箱", - verbose_name="主邮箱", - ), - ), - ( - "is_verified", - models.BooleanField( - default=False, - help_text="邮箱是否已通过验证码验证", - verbose_name="已验证", - ), - ), - ( - "created_at", - models.DateTimeField( - auto_now_add=True, - help_text="邮箱绑定的时间", - verbose_name="绑定时间", - ), - ), - ( - "user", - models.ForeignKey( - help_text="关联的用户", - on_delete=django.db.models.deletion.CASCADE, - related_name="emails", - to=settings.AUTH_USER_MODEL, - verbose_name="用户", - ), - ), - ], - options={ - "verbose_name": "用户邮箱", - "verbose_name_plural": "用户邮箱", - "indexes": [ - models.Index(fields=["email"], name="accounts_us_email_37c5c6_idx"), - models.Index( - fields=["user", "is_primary"], - name="accounts_us_user_id_664fe0_idx", - ), - ], - "unique_together": {("email",)}, - }, - ), - ] diff --git a/apps/accounts/migrations/0010_userbanhistory_userban.py b/apps/accounts/migrations/0010_userbanhistory_userban.py deleted file mode 100644 index 46c7d3a..0000000 --- a/apps/accounts/migrations/0010_userbanhistory_userban.py +++ /dev/null @@ -1,127 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 14:04 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0009_useremail"), - ] - - operations = [ - migrations.CreateModel( - name="UserBanHistory", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("reason", models.TextField(blank=True, verbose_name="封禁理由")), - ("banned_at", models.DateTimeField(verbose_name="封禁时间")), - ( - "unbanned_at", - models.DateTimeField(auto_now_add=True, verbose_name="解封时间"), - ), - ( - "banned_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="issued_ban_history", - to=settings.AUTH_USER_MODEL, - verbose_name="封禁操作者", - ), - ), - ( - "unbanned_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="revoked_ban_history", - to=settings.AUTH_USER_MODEL, - verbose_name="解封操作者", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="ban_history", - to=settings.AUTH_USER_MODEL, - verbose_name="用户", - ), - ), - ], - options={ - "verbose_name": "封禁历史", - "verbose_name_plural": "封禁历史", - "ordering": ["-unbanned_at"], - }, - ), - migrations.CreateModel( - name="UserBan", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "reason", - models.TextField( - blank=True, - help_text="封禁该用户的理由,将展示给用户", - verbose_name="封禁理由", - ), - ), - ( - "created_at", - models.DateTimeField( - auto_now_add=True, - help_text="封禁操作的时间", - verbose_name="封禁时间", - ), - ), - ( - "banned_by", - models.ForeignKey( - blank=True, - help_text="执行封禁操作的管理员", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="issued_bans", - to=settings.AUTH_USER_MODEL, - verbose_name="封禁操作者", - ), - ), - ( - "user", - models.OneToOneField( - help_text="被封禁的用户", - on_delete=django.db.models.deletion.CASCADE, - related_name="active_ban", - to=settings.AUTH_USER_MODEL, - verbose_name="用户", - ), - ), - ], - options={ - "verbose_name": "用户封禁", - "verbose_name_plural": "用户封禁", - }, - ), - ] diff --git a/apps/accounts/migrations/__init__.py b/apps/accounts/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/accounts/models.py b/apps/accounts/models.py deleted file mode 100755 index 00854ce..0000000 --- a/apps/accounts/models.py +++ /dev/null @@ -1,550 +0,0 @@ -""" -用户管理模型 -""" - -import uuid as _uuid - -from django.db import models -from django.contrib.auth.models import AbstractUser, Group -from django.utils.translation import gettext_lazy as _ -from django.utils import timezone - - -class User(AbstractUser): - """ - 自定义用户模型 - - 扩展Django默认用户模型,添加额外字段 - """ - - # 基本信息 - phone = models.CharField( - max_length=20, - blank=True, - null=True, - verbose_name=_("手机号码"), - help_text=_("用户的手机号码"), - ) - avatar = models.ImageField( - upload_to="avatars/", - blank=True, - null=True, - verbose_name=_("头像"), - help_text=_("用户头像图片"), - ) - - # 状态信息 - is_verified = models.BooleanField( - default=False, verbose_name=_("已验证"), help_text=_("用户邮箱是否已验证") - ) - last_login_ip = models.GenericIPAddressField( - blank=True, - null=True, - verbose_name=_("最后登录IP"), - help_text=_("用户最后一次登录的IP地址"), - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, verbose_name=_("创建时间"), help_text=_("用户账号创建时间") - ) - updated_at = models.DateTimeField( - auto_now=True, verbose_name=_("更新时间"), help_text=_("用户信息最后更新时间") - ) - - site_groups = models.ManyToManyField( - "dashboard.SiteGroup", - blank=True, - related_name="members", - verbose_name=_("所属站点组"), - help_text=_("用户所属的站点组,决定用户可见的数据范围"), - ) - - class Meta: - verbose_name = _("用户") - verbose_name_plural = verbose_name - ordering = ["-created_at"] - indexes = [ - models.Index(fields=["email"]), - models.Index(fields=["phone"]), - models.Index(fields=["is_active"]), - ] - - def __str__(self): - """返回用户名""" - return self.username - - def get_full_name(self): - """获取用户全名""" - full_name = super().get_full_name() - return full_name if full_name else self.username - - def is_site_group_admin(self, site_group=None): - if self.is_superuser: - return True - if site_group is None: - return False - return site_group.admins.filter(pk=self.pk).exists() - - def get_adminable_site_groups(self): - if self.is_superuser: - from apps.dashboard.models import SiteGroup - - return SiteGroup.objects.all() - return self.admin_site_groups.all() - - def sync_staff_status(self): - if self.is_superuser: - if not self.is_staff: - self.is_staff = True - self.save(update_fields=["is_staff"]) - return - - has_auto_staff = self.groups.filter(profile__auto_staff=True).exists() - - if has_auto_staff != self.is_staff: - self.is_staff = has_auto_staff - self.save(update_fields=["is_staff"]) - - def update_last_login(self, request): - """ - 更新最后登录信息 - - Args: - request: Django请求对象 - """ - from utils.helpers import get_client_ip - - self.last_login = timezone.now() - self.last_login_ip = get_client_ip(request) - self.save(update_fields=["last_login", "last_login_ip"]) - - -class UserEmail(models.Model): - """ - 用户多邮箱绑定模型 - - 一个用户可以有一个主邮箱和多个子邮箱。 - 用于账户合并检测和封禁污染追踪。 - """ - - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name="emails", - verbose_name=_("用户"), - help_text=_("关联的用户"), - ) - email = models.EmailField( - verbose_name=_("邮箱地址"), - help_text=_("绑定的邮箱地址"), - ) - is_primary = models.BooleanField( - default=False, - verbose_name=_("主邮箱"), - help_text=_("是否为用户的主邮箱"), - ) - is_verified = models.BooleanField( - default=False, - verbose_name=_("已验证"), - help_text=_("邮箱是否已通过验证码验证"), - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_("绑定时间"), - help_text=_("邮箱绑定的时间"), - ) - - class Meta: - verbose_name = _("用户邮箱") - verbose_name_plural = verbose_name - unique_together = [("email",)] - indexes = [ - models.Index(fields=["email"]), - models.Index(fields=["user", "is_primary"]), - ] - - def __str__(self): - primary_tag = " [主]" if self.is_primary else "" - return f"{self.email}{primary_tag}" - - def save(self, *args, **kwargs): - # 如果设置为主邮箱,确保同用户其他邮箱取消主邮箱标记 - if self.is_primary: - UserEmail.objects.filter(user=self.user, is_primary=True).exclude( - pk=self.pk - ).update(is_primary=False) - super().save(*args, **kwargs) - - -class UserBan(models.Model): - """ - 用户封禁记录 - - 自定义封禁系统,替代 Django 的 is_active 字段。 - 支持封禁理由、封禁者记录,以及封禁历史。 - 一个用户同一时间只能有一条活跃封禁记录。 - """ - - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - related_name="active_ban", - verbose_name=_("用户"), - help_text=_("被封禁的用户"), - ) - reason = models.TextField( - blank=True, - verbose_name=_("封禁理由"), - help_text=_("封禁该用户的理由,将展示给用户"), - ) - banned_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="issued_bans", - verbose_name=_("封禁操作者"), - help_text=_("执行封禁操作的管理员"), - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_("封禁时间"), - help_text=_("封禁操作的时间"), - ) - - class Meta: - verbose_name = _("用户封禁") - verbose_name_plural = verbose_name - - def __str__(self): - return f"封禁: {self.user.username} ({self.created_at:%Y-%m-%d %H:%M})" - - -class UserBanHistory(models.Model): - """ - 封禁历史记录 - - 解封时将活跃封禁记录归档到此表,保留完整封禁历史。 - """ - - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name="ban_history", - verbose_name=_("用户"), - ) - reason = models.TextField( - blank=True, - verbose_name=_("封禁理由"), - ) - banned_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="issued_ban_history", - verbose_name=_("封禁操作者"), - ) - unbanned_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="revoked_ban_history", - verbose_name=_("解封操作者"), - ) - banned_at = models.DateTimeField( - verbose_name=_("封禁时间"), - ) - unbanned_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_("解封时间"), - ) - - class Meta: - verbose_name = _("封禁历史") - verbose_name_plural = verbose_name - ordering = ["-unbanned_at"] - - def __str__(self): - return f"封禁历史: {self.user.username} ({self.banned_at:%Y-%m-%d} → {self.unbanned_at:%Y-%m-%d})" - - -class UserProfile(models.Model): - """ - 用户资料模型 - - 存储用户的详细资料信息 - """ - - # 关联用户 - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - related_name="profile", - verbose_name=_("用户"), - help_text=_("关联的用户"), - ) - - # 个人信息 - nickname = models.CharField( - max_length=50, blank=True, verbose_name=_("昵称"), help_text=_("用户昵称") - ) - gender = models.CharField( - max_length=10, - choices=[ - ("male", _("男")), - ("female", _("女")), - ("other", _("其他")), - ], - blank=True, - verbose_name=_("性别"), - help_text=_("用户性别"), - ) - birthday = models.DateField( - blank=True, null=True, verbose_name=_("生日"), help_text=_("用户生日") - ) - location = models.CharField( - max_length=100, blank=True, verbose_name=_("所在地"), help_text=_("用户所在地") - ) - bio = models.TextField( - blank=True, verbose_name=_("个人简介"), help_text=_("用户个人简介") - ) - - # 通知设置 - email_notification = models.BooleanField( - default=True, verbose_name=_("邮件通知"), help_text=_("是否接收邮件通知") - ) - system_notification = models.BooleanField( - default=True, verbose_name=_("系统通知"), help_text=_("是否接收系统通知") - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, verbose_name=_("创建时间"), help_text=_("资料创建时间") - ) - updated_at = models.DateTimeField( - auto_now=True, verbose_name=_("更新时间"), help_text=_("资料最后更新时间") - ) - - class Meta: - verbose_name = _("用户资料") - verbose_name_plural = verbose_name - ordering = ["-created_at"] - - def __str__(self): - """返回用户昵称或用户名""" - return self.nickname or self.user.username - - -class LoginLog(models.Model): - """ - 登录日志模型 - - 记录用户的登录历史 - """ - - # 关联用户 - user = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="login_logs", - verbose_name=_("用户"), - help_text=_("登录的用户"), - ) - - # 登录信息 - ip_address = models.GenericIPAddressField( - verbose_name=_("IP地址"), help_text=_("登录时的IP地址") - ) - user_agent = models.TextField( - blank=True, verbose_name=_("用户代理"), help_text=_("登录时的浏览器信息") - ) - login_type = models.CharField( - max_length=20, - choices=[ - ("web", _("网页登录")), - ("api", _("API登录")), - ("other", _("其他")), - ], - default="web", - verbose_name=_("登录方式"), - help_text=_("用户的登录方式"), - ) - status = models.CharField( - max_length=20, - choices=[ - ("success", _("成功")), - ("failed", _("失败")), - ], - verbose_name=_("登录状态"), - help_text=_("登录是否成功"), - ) - failure_reason = models.CharField( - max_length=200, - blank=True, - verbose_name=_("失败原因"), - help_text=_("登录失败的原因"), - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, verbose_name=_("登录时间"), help_text=_("登录发生的时间") - ) - - class Meta: - verbose_name = _("登录日志") - verbose_name_plural = verbose_name - ordering = ["-created_at"] - indexes = [ - models.Index(fields=["user"]), - models.Index(fields=["ip_address"]), - models.Index(fields=["status"]), - models.Index(fields=["created_at"]), - ] - - def __str__(self): - """返回登录信息""" - return ( - f'{self.user.username if self.user else "未知用户"}' f" - {self.ip_address}" - ) - - -class GroupProfile(models.Model): - group = models.OneToOneField( - Group, - on_delete=models.CASCADE, - related_name="profile", - verbose_name=_("用户组"), - help_text=_("关联的Django用户组"), - ) - is_default = models.BooleanField( - default=False, - verbose_name=_("默认组"), - help_text=_("默认组不可删除,系统预设角色"), - ) - description = models.TextField( - blank=True, verbose_name=_("描述"), help_text=_("用户组的功能描述") - ) - auto_staff = models.BooleanField( - default=False, - verbose_name=_("自动员工"), - help_text=_("勾选后,属于该组的用户将自动获得员工身份(is_staff)"), - ) - sort_order = models.IntegerField( - default=0, verbose_name=_("排序"), help_text=_("数值越小排序越靠前") - ) - - class Meta: - verbose_name = _("用户组配置") - verbose_name_plural = verbose_name - ordering = ["sort_order", "group__name"] - - def __str__(self): - default_tag = " [默认]" if self.is_default else "" - return f"{self.group.name}{default_tag}" - - -class RegistrationLink(models.Model): - token = models.CharField( - max_length=64, - unique=True, - default=_uuid.uuid4, - verbose_name=_("令牌"), - help_text=_("注册链接的唯一标识"), - ) - group = models.ForeignKey( - Group, - on_delete=models.CASCADE, - related_name="registration_links", - verbose_name=_("注册后用户组"), - help_text=_("通过此链接注册的用户将自动加入该用户组"), - ) - created_by = models.ForeignKey( - "User", - on_delete=models.SET_NULL, - null=True, - related_name="created_registration_links", - verbose_name=_("创建者"), - help_text=_("创建此注册链接的管理员"), - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("创建时间")) - max_uses = models.IntegerField( - default=1, - verbose_name=_("最大使用次数"), - help_text=_("设置为0表示不限制使用次数"), - ) - used_count = models.IntegerField(default=0, verbose_name=_("已使用次数")) - used = models.BooleanField( - default=False, verbose_name=_("已使用"), help_text=_("此注册链接是否已被使用") - ) - used_by = models.ForeignKey( - "User", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="used_registration_link", - verbose_name=_("最后使用者"), - help_text=_("最后使用此链接注册的用户"), - ) - used_at = models.DateTimeField( - null=True, blank=True, verbose_name=_("最后使用时间") - ) - expires_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_("过期时间"), - help_text=_("留空表示永不过期"), - ) - note = models.CharField( - max_length=200, - blank=True, - verbose_name=_("备注"), - help_text=_("管理员备注,仅后台可见"), - ) - - class Meta: - verbose_name = _("注册链接") - verbose_name_plural = verbose_name - ordering = ["-created_at"] - - def __str__(self): - if self.max_uses == 0: - usage = f"已用{self.used_count}次/不限" - else: - usage = f"已用{self.used_count}/{self.max_uses}次" - return f"注册链接({self.group.name}) - {usage}" - - @property - def is_expired(self): - if self.expires_at is None: - return False - return timezone.now() > self.expires_at - - @property - def is_exhausted(self): - if self.max_uses == 0: - return False - return self.used_count >= self.max_uses - - @property - def is_valid(self): - return not self.is_exhausted and not self.is_expired - - def increment_usage(self, user): - from django.db.models import F - - RegistrationLink.objects.filter(pk=self.pk).update( - used_count=F("used_count") + 1, - used_by=user, - used_at=timezone.now(), - ) - self.refresh_from_db() - if self.max_uses > 0 and self.used_count >= self.max_uses: - RegistrationLink.objects.filter(pk=self.pk).update(used=True) - self.used = True - else: - RegistrationLink.objects.filter(pk=self.pk).update(used=False) - self.used = False diff --git a/apps/accounts/provider_decorators.py b/apps/accounts/provider_decorators.py deleted file mode 100644 index e4c92f4..0000000 --- a/apps/accounts/provider_decorators.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -后台认证装饰器 - -集中管理后台用户的身份验证逻辑。 -供整个项目的视图和中间件使用。 - -- admin_required: 要求用户为超级管理员或提供商(统一后台入口) -- superadmin_required: 要求用户为超级管理员(仅超管可访问的功能) -- provider_required: 要求用户为提供商(已废弃,保留兼容) -""" - -from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.auth.decorators import login_required -from django.http import HttpResponseForbidden -from django.shortcuts import redirect -from functools import wraps - - -PROVIDER_GROUP_NAME = '主机提供商' - - -def is_provider(user): - """ - 检查用户是否属于提供商组 - - 条件: - - 用户已认证 - - 用户属于"提供商"组 - - 用户为 staff 成员 - - 注意:超级管理员不属于提供商组,即使其权限更高。 - 此逻辑与 Admin 后台的数据隔离保持一致。 - - Args: - user: 用户对象 - - Returns: - bool: 如果用户满足提供商条件,返回 True - """ - if not user.is_authenticated: - return False - if user.is_superuser: - return False - if not user.is_staff: - return False - return user.groups.filter(name=PROVIDER_GROUP_NAME).exists() - - -def provider_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, - login_url=None): - """ - 装饰器:要求当前用户为提供商 - - 验证逻辑: - - 未登录用户将被重定向到登录页(带 next 参数) - - 已登录但非提供商用户将收到 403 Forbidden - - 用法: - @provider_required - def my_view(request): - ... - - # 或用于类视图 - @method_decorator(provider_required, name='dispatch') - class MyView(TemplateView): - ... - """ - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - path = request.get_full_path() - return redirect_to_login( - path, - login_url=login_url, - redirect_field_name=redirect_field_name, - ) - if not is_provider(request.user): - return HttpResponseForbidden( - '您没有提供商权限,无法访问此页面。' - ) - return func(request, *args, **kwargs) - return wrapper - - if view_func: - return decorator(view_func) - return decorator - - -def superadmin_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, - login_url=None): - """ - 装饰器:要求当前用户为超级管理员 - - 验证逻辑: - - 未登录用户将被重定向到登录页(带 next 参数) - - 已登录但非超级管理员将收到 403 Forbidden - - 用法: - @superadmin_required - def my_view(request): - ... - """ - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - path = request.get_full_path() - return redirect_to_login( - path, - login_url=login_url, - redirect_field_name=redirect_field_name, - ) - if not request.user.is_superuser: - return HttpResponseForbidden( - '您没有超级管理员权限,无法访问此页面。' - ) - return func(request, *args, **kwargs) - return wrapper - - if view_func: - return decorator(view_func) - return decorator - - -def is_site_group_admin(user, site_group=None): - if not user.is_authenticated: - return False - if user.is_superuser: - return True - if site_group is None: - return False - return site_group.admins.filter(pk=user.pk).exists() - - -def site_group_admin_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, - login_url=None): - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - path = request.get_full_path() - return redirect_to_login( - path, - login_url=login_url, - redirect_field_name=redirect_field_name, - ) - site_group = getattr(request, 'site_group', None) - if not (request.user.is_superuser or is_site_group_admin(request.user, site_group)): - return HttpResponseForbidden( - '您没有站点组管理员权限,无法访问此页面。' - ) - return func(request, *args, **kwargs) - return wrapper - - if view_func: - return decorator(view_func) - return decorator - - -def admin_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, - login_url=None): - """ - 装饰器:要求当前用户为后台用户(超级管理员或提供商) - - 统一后台入口装饰器,允许超级管理员和提供商访问 /admin/ 路由。 - 视图内部通过 request.user.is_superuser 判断角色做数据隔离。 - - 验证逻辑: - - 未登录用户将被重定向到登录页(带 next 参数) - - 已登录但非后台用户将收到 403 Forbidden - - 用法: - @admin_required - def my_view(request): - if request.user.is_superuser: - qs = Model.objects.all() - else: - qs = Model.objects.filter(created_by=request.user) - ... - """ - def decorator(func): - @wraps(func) - def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - path = request.get_full_path() - return redirect_to_login( - path, - login_url=login_url, - redirect_field_name=redirect_field_name, - ) - user = request.user - site_group = getattr(request, 'site_group', None) - if not (user.is_staff or is_provider(user) or is_site_group_admin(user, site_group)): - return HttpResponseForbidden( - '您没有后台访问权限。' - ) - return func(request, *args, **kwargs) - return wrapper - - if view_func: - return decorator(view_func) - return decorator diff --git a/apps/accounts/rate_limit.py b/apps/accounts/rate_limit.py deleted file mode 100755 index e6c9549..0000000 --- a/apps/accounts/rate_limit.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -速率限制工具模块 -""" -from functools import wraps -from django.core.cache import cache -from django.http import JsonResponse -from django.conf import settings -from utils.helpers import get_client_ip -import time - - -def rate_limit(key_func, rate='5/m'): - """ - 速率限制装饰器(固定窗口计数器实现) - - Args: - key_func: 生成限流键的函数,接收request参数 - rate: 速率限制规则,格式如 '5/m', '10/h', '100/d' - """ - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - limit, period = rate.lower().split('/') - limit = int(limit) - - period_map = { - 's': 1, - 'm': 60, - 'h': 3600, - 'd': 86400 - } - period_seconds = period_map.get(period, 60) - - client_key = key_func(request) - window = int(time.time() // period_seconds) - cache_key = f'rate_limit:{view_func.__name__}:{client_key}:{window}' - - current_count = cache.get(cache_key, 0) - - if current_count >= limit: - remaining_time = period_seconds - (time.time() % period_seconds) - return JsonResponse({ - 'status': 'error', - 'message': f'请求过于频繁,请在 {int(remaining_time)} 秒后重试', - 'retry_after': int(remaining_time) - }, status=429) - - cache.set(cache_key, current_count + 1, timeout=period_seconds + 1) - - return view_func(request, *args, **kwargs) - return wrapper - return decorator - - -def get_rate_limit_key(request, prefix=''): - """生成基于IP的速率限制键""" - ip = get_client_ip(request) - return f"{prefix}{ip}" - - -# 预定义的速率限制装饰器 -def login_rate_limit(view_func): - """登录接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'login:'), - rate='5/m' # 每分钟最多5次 - )(view_func) - - -def register_rate_limit(view_func): - """注册接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'register:'), - rate='3/m' # 每分钟最多3次 - )(view_func) - - -def email_code_rate_limit(view_func): - """邮箱验证码接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'email_code:'), - rate='2/m' # 每分钟最多2次 - )(view_func) - - -def avatar_upload_rate_limit(view_func): - """头像上传接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'avatar_upload:'), - rate='5/h' # 每小时最多5次 - )(view_func) - - -def general_api_rate_limit(view_func): - """通用API接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'api:'), - rate='10/m' # 每分钟最多10次 - )(view_func) - - -def file_upload_rate_limit(view_func): - """文件上传接口速率限制""" - return rate_limit( - key_func=lambda r: get_rate_limit_key(r, 'file_upload:'), - rate='10/m' # 每分钟最多10次 - )(view_func) \ No newline at end of file diff --git a/apps/accounts/signals.py b/apps/accounts/signals.py deleted file mode 100755 index ab5a4fa..0000000 --- a/apps/accounts/signals.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -用户管理信号处理器 -""" -from django.db.models.signals import post_save, m2m_changed -from django.dispatch import receiver -from django.contrib.auth import get_user_model - -User = get_user_model() - - -@receiver(post_save, sender=User) -def create_user_log(sender, instance, created, **kwargs): - """ - 用户创建/更新时记录操作日志 - - Args: - sender: 发送信号的模型类 - instance: 用户实例 - created: 是否是新创建的用户 - **kwargs: 其他参数 - """ - pass - - -@receiver(m2m_changed, sender=User.groups.through) -def sync_staff_on_group_change(sender, instance, action, **kwargs): - if action in ('post_add', 'post_remove', 'post_clear'): - instance.sync_staff_status() diff --git a/apps/accounts/tasks.py b/apps/accounts/tasks.py deleted file mode 100644 index d5a069f..0000000 --- a/apps/accounts/tasks.py +++ /dev/null @@ -1,277 +0,0 @@ -from celery import shared_task -import logging - -logger = logging.getLogger(__name__) - - -@shared_task( - bind=True, - max_retries=3, - default_retry_delay=30, - acks_late=True, -) -def send_email_task( - self, - to_emails, - subject, - text_body, - html_body=None, - from_email=None, - site_group_id=None, -): - """ - 异步发送邮件任务 - - Args: - to_emails: 收件人列表 - subject: 邮件主题 - text_body: 纯文本内容 - html_body: HTML 内容(可选) - from_email: 发件人(可选,默认使用系统配置) - site_group_id: 站点组ID(可选,用于使用站点组SMTP配置) - """ - from apps.accounts.email_service import EmailService - from utils.site_group import get_effective_config - from apps.dashboard.models import SiteGroup - - try: - site_group = None - if site_group_id: - try: - site_group = SiteGroup.objects.get(pk=site_group_id) - except SiteGroup.DoesNotExist: - pass - ec = get_effective_config(site_group) - email_service = EmailService.from_effective_config(ec) - email_service.send_email( - to_emails=to_emails, - subject=subject, - text_body=text_body, - html_body=html_body, - from_email=from_email, - ) - except Exception as exc: - logger.error(f'异步邮件发送失败: {exc}', exc_info=True) - raise self.retry(exc=exc) - - -@shared_task( - bind=True, - max_retries=3, - default_retry_delay=30, - acks_late=True, -) -def send_ticket_email_task( - self, - subject, - template_name, - context, - recipient_list, -): - """ - 异步发送工单邮件任务 - - Args: - subject: 邮件主题 - template_name: 邮件模板名称 - context: 模板上下文 - recipient_list: 收件人列表 - """ - from django.template.loader import render_to_string - from django.utils.html import strip_tags - from apps.accounts.email_service import EmailService - from apps.dashboard.models import SystemConfig - - if not recipient_list: - return - - try: - html_message = render_to_string(template_name, context) - plain_message = strip_tags(html_message) - - config = SystemConfig.get_config() - from_email = ( - config.smtp_from_email - if config and config.smtp_from_email - else 'noreply@2c2a.com' - ) - from_name = config.smtp_from_name if config else None - - if config: - try: - email_service = EmailService.from_system_config(config) - email_service.send_email( - to_emails=recipient_list, - subject=subject, - text_body=plain_message, - html_body=html_message, - from_email=from_email, - ) - return - except Exception: - pass - - # 回退到 Django 的 send_mail - from django.core.mail import send_mail - if from_name: - from_email = f'{from_name} <{from_email}>' - send_mail( - subject=subject, - message=plain_message, - from_email=from_email, - recipient_list=recipient_list, - html_message=html_message, - fail_silently=True, - ) - except Exception as exc: - logger.error(f'异步工单邮件发送失败: {exc}', exc_info=True) - raise self.retry(exc=exc) - - -@shared_task(bind=True, max_retries=1, acks_late=True) -def send_test_email_task(self, async_task_id): - """ - 异步发送测试邮件,使用 AsyncTask 追踪状态并记录调试日志 - - Args: - async_task_id: AsyncTask 记录的 PK - """ - from apps.tasks.models import AsyncTask - from apps.accounts.email_service import EmailService - from apps.dashboard.models import SystemConfig - - try: - task_record = AsyncTask.objects.get(pk=async_task_id) - except AsyncTask.DoesNotExist: - logger.error(f'AsyncTask {async_task_id} 不存在') - return - - def _log(msg): - """追加调试日志到 AsyncTask.result""" - task_record.refresh_from_db() - result = task_record.result or {} - logs = result.get('logs', []) - from django.utils import timezone - logs.append( - f'[{timezone.now().strftime("%H:%M:%S")}] {msg}' - ) - result['logs'] = logs - task_record.result = result - task_record.save(update_fields=['result']) - - task_record.start_execution() - _log('任务已启动') - - try: - _log('正在读取系统 SMTP 配置...') - config = SystemConfig.get_config() - - if not config or not config.smtp_host: - _log('错误: SMTP 未配置') - task_record.complete_failure('SMTP 未配置(smtp_host 为空)') - return - - _log( - f'SMTP 配置: {config.smtp_host}:{config.smtp_port}, ' - f'加密={config.smtp_encryption}, ' - f'from={config.smtp_from_email}' - ) - - email_service = EmailService.from_system_config(config) - test_email = ( - task_record.result.get('test_email', '') - if task_record.result else '' - ) - - _log(f'收件人: {test_email}') - - subject = '2c2a 测试邮件' - from django.utils import timezone - text_body = ( - f'这是一封测试邮件,用于验证邮件配置是否正确。' - f'测试时间: {timezone.now().strftime("%Y-%m-%d %H:%M:%S")}' - ) - html_body = f''' - - - - - {subject} - - - -
-
-

2c2a 验证码服务

-
-
-

您好!

-
-

这是一封测试邮件,用于验证邮件配置是否正确。

-
-

系统配置的SMTP服务器可以正常发送邮件。

-

测试时间: {timezone.now().strftime("%Y-%m-%d %H:%M:%S")}

-
- -
- - - ''' - - _log('正在连接 SMTP 服务器...') - task_record.progress = 30 - task_record.save(update_fields=['progress']) - - email_service.send_email( - to_emails=[test_email], - subject=subject, - text_body=text_body, - html_body=html_body, - ) - - _log('邮件发送成功') - task_record.progress = 100 - task_record.save(update_fields=['progress']) - - task_record.complete_success( - result_data={'test_email': test_email} - ) - except Exception as exc: - logger.error(f'测试邮件发送失败: {exc}', exc_info=True) - _log(f'异常: {exc}') - task_record.complete_failure(str(exc)) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py deleted file mode 100755 index 2624f20..0000000 --- a/apps/accounts/urls.py +++ /dev/null @@ -1,79 +0,0 @@ -from django.urls import path -from django.views.decorators.cache import never_cache -from . import views - -app_name = 'accounts' - -urlpatterns = [ - path('register/', views.RegisterView.as_view(), name='register'), - path( - 'register//', - views.RegisterByLinkView.as_view(), - name='register_by_link', - ), - path( - 'login/', - never_cache(views.LoginView.as_view()), - name='login', - ), - path('profile/', views.ProfileView.as_view(), name='profile'), - path('migrate/', views.migrate_view, name='migrate'), - path('banned/', views.banned_view, name='banned'), - path('logout/', views.logout_view, name='logout'), - path( - 'email/send-code/', - views.send_register_email_code, - name='send_register_email_code', - ), - path( - 'forgot-password/', - views.ForgotPasswordView.as_view(), - name='forgot_password', - ), - path( - 'email/send-forgot-password-code/', - views.send_forgot_password_email_code, - name='send_forgot_password_email_code', - ), - path( - 'api/profile/avatar/', - views.upload_avatar, - name='upload_avatar', - ), - path( - 'api/password/change/', - views.password_change_api, - name='password_change_api', - ), - # 邮箱绑定 API - path( - 'api/emails/', - views.email_list_api, - name='email_list', - ), - path( - 'api/emails/bind/', - views.email_bind_api, - name='email_bind', - ), - path( - 'api/emails/send-bind-code/', - views.send_bind_email_code, - name='send_bind_email_code', - ), - path( - 'api/emails//set-primary/', - views.email_set_primary_api, - name='email_set_primary', - ), - path( - 'api/emails//unbind/', - views.email_unbind_api, - name='email_unbind', - ), - path( - 'api/emails/merge-confirm/', - views.email_merge_confirm_api, - name='email_merge_confirm', - ), -] diff --git a/apps/accounts/urls_admin.py b/apps/accounts/urls_admin.py deleted file mode 100644 index a662b8c..0000000 --- a/apps/accounts/urls_admin.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -超管后台 URL 配置 - -所有 URL 以 /admin/ 为前缀,命名空间为 'admin'。 -各子模块通过 include 引入,实现模块化路由。 -此模块完全替代 Django Admin。 -""" - -from django.urls import path, include - -from apps.accounts.views_admin import admin_dashboard -from plugins.dynamic_urls import get_plugin_admin_urls - -app_name = 'admin' - -urlpatterns = [ - # 仪表盘 - path('', admin_dashboard, name='dashboard'), - - # 用户与权限 - path('users/', include('apps.accounts.urls_admin_users')), - path('groups/', include('apps.accounts.urls_admin_groups')), - path('reglinks/', include('apps.accounts.urls_admin_reglinks')), - - # 提供商分配 - path('providers/', include('apps.accounts.urls_admin_providers')), - - # 主机与产品 - path('hosts/', include('apps.hosts.urls_admin')), - - # 运营管理 - path('operations/', include('apps.operations.urls_admin')), - - # 工单系统 - path('tickets/', include('apps.tickets.urls_admin')), - - # 插件配置(动态加载) - path('plugins/', include('plugins.urls_admin')), - path('plugins/', include(get_plugin_admin_urls())), - - # 审计日志 - path('audit/', include('apps.audit.urls_admin')), - - # 仪表盘组件配置 - path('dashboard/', include('apps.dashboard.urls_admin')), - - # 主题配置 - path('themes/', include('apps.themes.urls_admin')), -] diff --git a/apps/accounts/urls_admin_groups.py b/apps/accounts/urls_admin_groups.py deleted file mode 100644 index 1710088..0000000 --- a/apps/accounts/urls_admin_groups.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from . import views_admin_groups as views - -app_name = 'admin_groups' - -urlpatterns = [ - path('', views.group_list, name='group_list'), - path('create/', views.group_create, name='group_create'), - path('/edit/', views.group_update, name='group_edit'), - path('/delete/', views.group_delete, name='group_delete'), -] diff --git a/apps/accounts/urls_admin_providers.py b/apps/accounts/urls_admin_providers.py deleted file mode 100644 index 5dccdda..0000000 --- a/apps/accounts/urls_admin_providers.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.urls import path - -from .views_superadmin import ( - superadmin_host_list, - superadmin_host_provider_assign, - superadmin_hostgroup_list, - superadmin_hostgroup_provider_assign, -) - -app_name = 'admin_providers' - -urlpatterns = [ - path( - 'hosts/', - superadmin_host_list, - name='provider_host_list', - ), - path( - 'hosts//providers/', - superadmin_host_provider_assign, - name='provider_host_assign', - ), - path( - 'host-groups/', - superadmin_hostgroup_list, - name='provider_hostgroup_list', - ), - path( - 'host-groups//providers/', - superadmin_hostgroup_provider_assign, - name='provider_hostgroup_assign', - ), -] diff --git a/apps/accounts/urls_admin_reglinks.py b/apps/accounts/urls_admin_reglinks.py deleted file mode 100644 index 902f4e7..0000000 --- a/apps/accounts/urls_admin_reglinks.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from . import views_admin_reglinks as views - -app_name = 'admin_reglinks' - -urlpatterns = [ - path('', views.reglink_list, name='reglink_list'), - path('create/', views.reglink_create, name='reglink_create'), - path('/delete/', views.reglink_delete, name='reglink_delete'), -] diff --git a/apps/accounts/urls_admin_users.py b/apps/accounts/urls_admin_users.py deleted file mode 100644 index 0e33668..0000000 --- a/apps/accounts/urls_admin_users.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -超管后台 - 用户管理 URL 配置 - -命名空间: admin_users (通过 admin: 命名空间访问) -""" - -from django.urls import path - -from . import views_admin_users as views - -app_name = 'admin_users' - -urlpatterns = [ - # 用户列表 - path('', views.user_list, name='user_list'), - - # 创建用户 - path('create/', views.user_create, name='user_create'), - - # 编辑用户 - path('/edit/', views.user_update, name='user_edit'), - - # 删除用户 - path('/delete/', views.user_delete, name='user_delete'), - - # 切换激活状态 - path('/toggle-active/', views.user_toggle_active, name='user_toggle_active'), - - # 重置密码 - path('/reset-password/', views.user_reset_password, name='user_reset_password'), -] diff --git a/apps/accounts/urls_provider.py b/apps/accounts/urls_provider.py deleted file mode 100644 index dd6769a..0000000 --- a/apps/accounts/urls_provider.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -提供商后台 URL 配置 - -所有 URL 以 /provider/ 为前缀,命名空间为 'provider'。 -各子模块通过 include 引入,实现模块化路由。 -""" - -from django.urls import path, include - -from apps.accounts.views_provider import provider_dashboard -from apps.accounts.views_superadmin import ( - superadmin_host_list, - superadmin_host_provider_assign, - superadmin_hostgroup_list, - superadmin_hostgroup_provider_assign, -) -from plugins.dynamic_urls import get_plugin_provider_urls - -app_name = 'provider' - -urlpatterns = [ - # 仪表盘 - path('', provider_dashboard, name='dashboard'), - - # 主机管理子模块 - path('hosts/', include('apps.hosts.urls_provider')), - - # 运营管理子模块 - path('operations/', include('apps.operations.urls_provider')), - - # 工单管理子模块 - path('tickets/', include('apps.tickets.urls_provider')), - - # 插件配置子模块(动态加载) - path('plugins/', include('plugins.urls_provider')), - path('plugins/', include(get_plugin_provider_urls())), - - # 提供商 API - path('api/', include('apps.provider_backend.api_urls')), - - # 超级管理员 - 提供商分配 - path('superadmin/hosts/', superadmin_host_list, name='superadmin_host_list'), - path('superadmin/hosts//providers/', superadmin_host_provider_assign, name='superadmin_host_provider_assign'), - path('superadmin/host-groups/', superadmin_hostgroup_list, name='superadmin_hostgroup_list'), - path('superadmin/host-groups//providers/', superadmin_hostgroup_provider_assign, name='superadmin_hostgroup_provider_assign'), -] diff --git a/apps/accounts/user_service.py b/apps/accounts/user_service.py deleted file mode 100644 index 11941ca..0000000 --- a/apps/accounts/user_service.py +++ /dev/null @@ -1,371 +0,0 @@ -""" -用户服务模块 - -集中处理用户迁移、封禁同步、账户合并、邮箱绑定等核心业务逻辑。 -""" - -import logging - -from django.db import transaction -from django.contrib.auth import get_user_model - -from .models import UserEmail, UserBan, UserBanHistory - -User = get_user_model() -logger = logging.getLogger(__name__) - - -# ── 用户迁移 ────────────────────────────────────────── - - -def check_user_migration(user, site_group): - """ - 检查用户是否需要迁移到指定站点组。 - - Returns: - dict: { - 'needs_migration': bool, - 'already_member': bool, - 'user_banned': bool, - } - """ - if site_group is None: - return {"needs_migration": False, "already_member": True, "user_banned": False} - - # 超级管理员不需要迁移,相当于所有站点的管理员 - if user.is_superuser: - return {"needs_migration": False, "already_member": True, "user_banned": False} - - is_member = user.site_groups.filter(pk=site_group.pk).exists() - if is_member: - return {"needs_migration": False, "already_member": True, "user_banned": False} - - if UserBan.objects.filter(user=user).exists(): - return {"needs_migration": False, "already_member": False, "user_banned": True} - - return {"needs_migration": True, "already_member": False, "user_banned": False} - - -def migrate_user_to_site_group(user, site_group): - """ - 将用户迁移到指定站点组。 - - 仅添加 site_groups 关联,不改变用户数据。 - 迁移前需检查邮箱后缀合规性。 - - Returns: - dict: {'success': bool, 'reason': str} - """ - if site_group is None: - return {"success": False, "reason": "无效的站点组"} - - if UserBan.objects.filter(user=user).exists(): - return {"success": False, "reason": "用户已被封禁,无法迁移"} - - if user.site_groups.filter(pk=site_group.pk).exists(): - return {"success": False, "reason": "用户已在该站点组中"} - - # 检查邮箱后缀合规性 - from utils.site_group import get_effective_config - - ec = get_effective_config(site_group) - suffix_data = ec.get_email_suffix_lists() - has_whitelist = bool(suffix_data["whitelist"]) - has_blacklist = bool(suffix_data["blacklist"]) - - if has_whitelist or has_blacklist: - # 站点组配置了邮箱后缀限制,需要检查合规性 - user_emails = UserEmail.objects.filter(user=user) - email_list = list(user_emails.values_list("email", flat=True)) - # 兼容旧用户:如果没有 UserEmail 记录,回退到 User.email - if not email_list and user.email: - email_list = [user.email] - - has_compliant_email = any(ec.is_email_suffix_allowed(e) for e in email_list) - - if not has_compliant_email: - return { - "success": False, - "reason": "email_not_compliant", - "message": "您的邮箱不满足该站点的邮箱后缀要求,请先绑定符合条件的邮箱", - } - - user.site_groups.add(site_group) - logger.info( - f"用户 {user.username}(id={user.pk}) 已迁移到站点组 " - f"{site_group.name}(id={site_group.pk})" - ) - return {"success": True, "reason": "迁移成功"} - - -# ── 封禁同步 ────────────────────────────────────────── - - -def ban_user(user, reason="", banned_by=None): - """ - 全局封禁用户。 - - 使用自定义 UserBan 模型替代 Django 的 is_active 字段。 - 封禁是全局的,影响用户在所有站点组的访问。 - 封禁污染通过 bind_email 触发:当用户绑定被封禁用户的邮箱时, - 当前用户也会被封禁。 - """ - ban, created = UserBan.objects.get_or_create( - user=user, - defaults={"reason": reason, "banned_by": banned_by}, - ) - if not created: - # 已有封禁记录,更新理由 - if reason: - ban.reason = reason - if banned_by: - ban.banned_by = banned_by - ban.save() - - if created: - logger.info( - f"用户 {user.username}(id={user.pk}) 已被封禁。" - f"原因: {reason},操作者: {banned_by}" - ) - return ban - - -def unban_user(user, unbanned_by=None): - """ - 解封用户。 - - 将活跃封禁记录归档到 UserBanHistory,然后删除。 - """ - ban = UserBan.objects.filter(user=user).first() - if not ban: - return - - # 归档到历史记录 - UserBanHistory.objects.create( - user=user, - reason=ban.reason, - banned_by=ban.banned_by, - unbanned_by=unbanned_by, - banned_at=ban.created_at, - ) - ban.delete() - logger.info( - f"用户 {user.username}(id={user.pk}) 已解封。操作者: {unbanned_by}" - ) - - -def check_ban_status(email): - """ - 检查邮箱关联的账户是否被封禁。 - - 用于注册/忘记密码时检查,防止通过被封禁邮箱绕过。 - - Returns: - dict: {'is_banned': bool, 'user': User or None} - """ - # 先检查 UserEmail 表 - user_email = UserEmail.objects.filter(email=email).first() - if user_email and UserBan.objects.filter(user=user_email.user).exists(): - return {"is_banned": True, "user": user_email.user} - - # 再检查 User.email 字段(兼容旧数据) - user = User.objects.filter(email=email).first() - if user and UserBan.objects.filter(user=user).exists(): - return {"is_banned": True, "user": user} - - return {"is_banned": False, "user": user} - - -# ── 账户合并 ────────────────────────────────────────── - - -def merge_accounts(source_user, target_user, keep_newer=True): - """ - 合并两个账户。 - - 将 source_user 的数据迁移到 target_user,然后删除 source_user。 - 默认保留较新的账户(keep_newer=True),用户可选保留较旧的。 - - Args: - source_user: 被合并的账户(将被删除) - target_user: 保留的账户 - keep_newer: True=新到旧(source=旧,target=新), False=旧到新(source=新,target=旧) - - Returns: - dict: {'success': bool, 'reason': str} - """ - if source_user.pk == target_user.pk: - return {"success": False, "reason": "不能合并同一账户"} - - if not keep_newer: - # 旧到新:source 是较新的,target 是较旧的 - source_user, target_user = target_user, source_user - - with transaction.atomic(): - # 迁移 site_groups - for sg in source_user.site_groups.all(): - target_user.site_groups.add(sg) - - # 迁移 UserEmail - UserEmail.objects.filter(user=source_user).exclude( - email__in=UserEmail.objects.filter(user=target_user).values_list( - "email", flat=True - ) - ).update(user=target_user) - - # 迁移 groups - for group in source_user.groups.all(): - target_user.groups.add(group) - - # 迁移 UserProfile(如果 target 没有) - if not hasattr(target_user, "profile") and hasattr(source_user, "profile"): - source_user.profile.user = target_user - source_user.profile.save() - - # 如果 source 的主邮箱不在 target 的邮箱列表中,添加为子邮箱 - source_primary = UserEmail.objects.filter( - user=source_user, is_primary=True - ).first() - if source_primary: - if not UserEmail.objects.filter( - user=target_user, email=source_primary.email - ).exists(): - source_primary.user = target_user - source_primary.is_primary = False - source_primary.save() - - # 如果 target 没有任何邮箱,把 source 的邮箱都迁移过来 - if not UserEmail.objects.filter(user=target_user).exists(): - UserEmail.objects.filter(user=source_user).update(user=target_user) - - # 同步封禁状态 - source_banned = UserBan.objects.filter(user=source_user).first() - if source_banned and not UserBan.objects.filter(user=target_user).exists(): - UserBan.objects.create( - user=target_user, - reason=source_banned.reason, - banned_by=source_banned.banned_by, - ) - - # 删除被合并的账户 - source_user.delete() - - logger.info( - f"账户合并: 用户 {source_user.username}(id={source_user.pk}) " - f"已合并到 {target_user.username}(id={target_user.pk})" - ) - - return {"success": True, "reason": "合并成功", "kept_user": target_user} - - -# ── 邮箱绑定 ────────────────────────────────────────── - - -def bind_email(user, email, is_primary=False): - """ - 绑定邮箱到用户账户。 - - 如果该邮箱已被其他账户使用: - - 如果那个账户被封禁,同步封禁到当前用户(封禁污染) - - 如果那个账户正常,触发账户合并 - - Args: - user: 当前用户 - email: 要绑定的邮箱 - is_primary: 是否设为主邮箱 - - Returns: - dict: { - 'success': bool, - 'action': 'bound'|'banned'|'merge_required', - 'reason': str, - 'merge_info': dict or None, - } - """ - # 检查邮箱是否已被绑定 - existing = UserEmail.objects.filter(email=email).first() - if existing: - if existing.user.pk == user.pk: - return { - "success": False, - "action": "bound", - "reason": "该邮箱已绑定到当前账户", - "merge_info": None, - } - - other_user = existing.user - - # 封禁污染:如果邮箱关联的账户被封禁,同步封禁当前用户 - if UserBan.objects.filter(user=other_user).exists(): - ban_user( - user, reason=f"绑定了被封禁账户 {other_user.username} 的邮箱 {email}" - ) - return { - "success": False, - "action": "banned", - "reason": f"该邮箱关联的账户已被封禁,您的账户也已被同步封禁", - "merge_info": None, - } - - # 账户合并:需要用户确认 - return { - "success": False, - "action": "merge_required", - "reason": f"该邮箱已被账户 {other_user.username} 使用,需要进行账户合并", - "merge_info": { - "other_user_id": other_user.pk, - "other_username": other_user.username, - "other_created_at": str(other_user.created_at), - "current_user_id": user.pk, - "current_username": user.username, - "current_created_at": str(user.created_at), - }, - } - - # 正常绑定 - UserEmail.objects.create( - user=user, - email=email, - is_primary=is_primary, - is_verified=False, - ) - - # 如果设为主邮箱,同步更新 User.email - if is_primary: - user.email = email - user.save(update_fields=["email"]) - - return { - "success": True, - "action": "bound", - "reason": "邮箱绑定成功", - "merge_info": None, - } - - -def set_primary_email(user, email): - """设置主邮箱""" - ue = UserEmail.objects.filter(user=user, email=email).first() - if not ue: - return {"success": False, "reason": "该邮箱未绑定到当前账户"} - - ue.is_primary = True - ue.save() - - user.email = email - user.save(update_fields=["email"]) - - return {"success": True, "reason": "主邮箱设置成功"} - - -def unbind_email(user, email): - """解绑邮箱(不能解绑主邮箱)""" - ue = UserEmail.objects.filter(user=user, email=email).first() - if not ue: - return {"success": False, "reason": "该邮箱未绑定到当前账户"} - - if ue.is_primary: - return {"success": False, "reason": "不能解绑主邮箱,请先设置其他邮箱为主邮箱"} - - ue.delete() - return {"success": True, "reason": "邮箱解绑成功"} diff --git a/apps/accounts/views.py b/apps/accounts/views.py deleted file mode 100755 index adedd7e..0000000 --- a/apps/accounts/views.py +++ /dev/null @@ -1,1231 +0,0 @@ -""" -用户管理视图 -""" - -from django.shortcuts import redirect, render -from django.contrib.auth import login, logout -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.views.generic import CreateView, UpdateView, TemplateView -from django.urls import reverse_lazy -from django.utils.decorators import method_decorator -from django.views.decorators.http import require_http_methods -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_protect, csrf_exempt -from django.core.cache import cache -from django.utils.http import url_has_allowed_host_and_scheme -from PIL import Image -import os - -from .models import User, RegistrationLink -from .forms import UserRegistrationForm, UserUpdateForm, UserLoginForm -from . import rate_limit -from apps.themes.models import ThemeConfig, PageContent - - -def get_theme_context(): - """获取主题上下文,避免重复代码""" - theme_config = ThemeConfig.get_config() - return { - "theme_config": theme_config, - "theme_css_url": f"css/themes/{theme_config.active_theme}.css", - "custom_css_vars": theme_config.generate_css_variables(), - "page_contents": PageContent.get_all_enabled(), - } - - -def get_captcha_context(scene, request=None): - from utils.site_group import get_effective_config - - site_group = getattr(request, "site_group", None) if request else None - ec = get_effective_config(site_group) - captcha_provider, captcha_type = ec.get_captcha_config(scene=scene) - ctx = { - "CAPTCHA_PROVIDER": captcha_provider, - "CAPTCHA_TYPE": captcha_type, - } - if scene in ("register", "forgot_password"): - _, email_type = ec.get_captcha_config(scene="email") - ctx["CAPTCHA_TYPE_EMAIL"] = email_type - return ctx - - -@method_decorator(rate_limit.register_rate_limit, name="dispatch") -class RegisterView(CreateView): - """用户注册视图""" - - model = User - form_class = UserRegistrationForm - template_name = "accounts/register.html" - success_url = reverse_lazy("accounts:login") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update(get_captcha_context("register", self.request)) - context.update(get_theme_context()) - return context - - def form_valid(self, form): - """表单验证成功后的处理""" - request = self.request - email = form.cleaned_data.get("email") - email_code = request.POST.get("email_code") - if not (email and email_code): - form.add_error(None, "邮箱验证码缺失") - return self.form_invalid(form) - - # 检查该邮箱是否关联被封禁的账户 - from .user_service import check_ban_status - - ban = check_ban_status(email) - if ban["is_banned"]: - form.add_error("email", "该邮箱关联的账户已被封禁,无法注册") - return self.form_invalid(form) - - import hmac - - cache_key = f"register_email_code:{email}" - expected = cache.get(cache_key) - if expected is None: - form.add_error(None, "邮箱验证码已过期或不存在") - return self.form_invalid(form) - if not hmac.compare_digest(str(expected), str(email_code)): - form.add_error(None, "邮箱验证码错误") - return self.form_invalid(form) - - cache.delete(cache_key) - - response = super().form_valid(form) - - # 注册成功后创建 UserEmail 记录 - user = self.object - if user: - from .models import UserEmail - - UserEmail.objects.get_or_create( - email=email, - defaults={ - "user": user, - "is_primary": True, - "is_verified": True, - }, - ) - # 如果通过子站点注册,自动加入该站点组 - site_group = getattr(request, "site_group", None) - if site_group: - user.site_groups.add(site_group) - - messages.success(self.request, "注册成功!请登录您的账户。") - return response - - def form_invalid(self, form): - """表单验证失败后的处理""" - messages.error(self.request, "注册失败,请检查表单中的错误。") - return super().form_invalid(form) - - -@method_decorator(rate_limit.login_rate_limit, name="dispatch") -class LoginView(TemplateView): - """用户登录视图""" - - template_name = "accounts/login.html" - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context["form"] = UserLoginForm() - context.update(get_captcha_context("login", self.request)) - context["is_demo_mode"] = getattr(self.request, "is_demo_mode", False) - context["next"] = self.request.POST.get("next") or self.request.GET.get( - "next", "" - ) - context.update(get_theme_context()) - return context - - def post(self, request, *args, **kwargs): - """处理POST请求""" - # 处理迁移确认 - if request.POST.get("action") == "migrate_confirm": - return self._handle_migration_confirm(request) - - form = UserLoginForm(request.POST) - - if form.is_valid(): - from .captcha_service import validate_captcha - - is_valid, error_msg = validate_captcha(request, scene="login") - - if not is_valid: - form.add_error(None, error_msg) - context = self.get_context_data(**kwargs) - context["form"] = form - return self.render_to_response(context) - - username = form.cleaned_data["username"] - password = form.cleaned_data["password"] - remember = form.cleaned_data.get("remember", False) - - from django.contrib.auth import authenticate - - # 先检查封禁用户:使用自定义 UserBan 模型 - from .models import User as UserModel - from .models import UserBan - - try: - candidate = UserModel.objects.get(username=username) - if candidate.check_password(password) and UserBan.objects.filter(user=candidate).exists(): - login(request, candidate) - request.session["is_banned"] = True - return redirect("accounts:banned") - except UserModel.DoesNotExist: - pass - - user = authenticate(request, username=username, password=password) - - if user is not None: - # 检查是否需要迁移到当前站点组 - site_group = getattr(request, "site_group", None) - if site_group: - from .user_service import check_user_migration - - migration = check_user_migration(user, site_group) - if migration["user_banned"]: - login(request, user) - request.session["is_banned"] = True - return redirect("accounts:banned") - if migration["needs_migration"]: - # 将用户信息暂存到 session,重定向到迁移页 - login(request, user) - request.session["pending_migration_sg_id"] = site_group.pk - return redirect("accounts:migrate") - - # 更新最后登录IP - from django.utils import timezone - - user.last_login = timezone.now() - user.last_login_ip = self.get_client_ip(request) - user.save(update_fields=["last_login", "last_login_ip"]) - - # 登录用户 - if not hasattr(request, "user") or not request.user.is_authenticated: - login(request, user) - - # 设置会话过期时间 - if not remember: - request.session.set_expiry(0) - else: - request.session.set_expiry(60 * 60 * 24 * 7) - - messages.success(request, f"欢迎回来,{user.username}!") - next_url = request.POST.get("next") or request.GET.get("next") - if next_url and url_has_allowed_host_and_scheme( - next_url, - allowed_hosts=request.get_host(), - ): - return redirect(next_url) - if user.is_staff or user.is_superuser: - return redirect("/admin/") - return redirect("dashboard:index") - else: - messages.error(request, "用户名或密码错误") - - context = self.get_context_data(**kwargs) - context["form"] = form - return self.render_to_response(context) - - def _handle_migration_confirm(self, request): - """处理用户迁移确认(重定向到迁移页)""" - return redirect("accounts:migrate") - - def get_client_ip(self, request): - from utils.helpers import get_client_ip as _get_client_ip - - return _get_client_ip(request) - - -@method_decorator(login_required, name="dispatch") -class ProfileView(UpdateView): - """用户资料视图""" - - model = User - form_class = UserUpdateForm - template_name = "accounts/profile.html" - success_url = reverse_lazy("accounts:profile") - - def get_object(self, queryset=None): - """获取当前用户对象""" - return self.request.user - - def form_valid(self, form): - """表单验证成功后的处理""" - messages.success(self.request, "个人资料更新成功!") - return super().form_valid(form) - - def form_invalid(self, form): - """表单验证失败后的处理""" - messages.error(self.request, "个人资料更新失败,请检查表单中的错误。") - return super().form_invalid(form) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["is_demo_mode"] = getattr(self.request, "is_demo_mode", False) - return context - - def post(self, request, *args, **kwargs): - """处理POST请求,包括资料更新和密码修改""" - # 检查是否是密码修改请求 - current_password = request.POST.get("current_password") - new_password = request.POST.get("new_password") - confirm_password = request.POST.get("confirm_password") - - # 检查是否是密码修改请求 - if current_password or new_password or confirm_password: - # 检查是否在DEMO模式下 - if hasattr(request, "is_demo_mode") and request.is_demo_mode: - from django.contrib import messages - - messages.error(request, "DEMO模式下不允许修改密码") - # 返回GET请求以显示表单和错误消息 - return super().get(request, *args, **kwargs) - - # 验证密码字段 - if not current_password: - return JsonResponse({"status": "error", "message": "请输入当前密码"}) - if not new_password: - return JsonResponse({"status": "error", "message": "请输入新密码"}) - if new_password != confirm_password: - return JsonResponse( - {"status": "error", "message": "两次输入的新密码不一致"} - ) - - # 验证当前密码是否正确 - user = request.user - if not user.check_password(current_password): - return JsonResponse({"status": "error", "message": "当前密码错误"}) - - from django.contrib.auth.password_validation import validate_password - from django.core.exceptions import ValidationError as ValError - - try: - validate_password(new_password, user=user) - except ValError as e: - return JsonResponse({"status": "error", "message": e.messages[0]}) - - user.set_password(new_password) - user.save() - - return JsonResponse( - {"status": "success", "message": "密码修改成功,请重新登录"} - ) - - # 否则是资料更新请求 - return super().post(request, *args, **kwargs) - - -@login_required -def logout_view(request): - """用户登出视图""" - logout(request) - messages.success(request, "您已成功登出") - return redirect("accounts:login") - - -@login_required -@require_http_methods(["POST"]) -@rate_limit.general_api_rate_limit -def password_change_api(request): - """密码更改API端点""" - if hasattr(request, "is_demo_mode") and request.is_demo_mode: - return JsonResponse({"status": "error", "message": "DEMO模式下不允许修改密码"}) - - current_password = request.POST.get("current_password") - new_password = request.POST.get("new_password") - confirm_password = request.POST.get("confirm_password") - - # 验证密码字段 - if not current_password: - return JsonResponse({"status": "error", "message": "请输入当前密码"}) - if not new_password: - return JsonResponse({"status": "error", "message": "请输入新密码"}) - if new_password != confirm_password: - return JsonResponse({"status": "error", "message": "两次输入的新密码不一致"}) - - # 验证当前密码是否正确 - user = request.user - if not user.check_password(current_password): - return JsonResponse({"status": "error", "message": "当前密码错误"}) - - from django.contrib.auth.password_validation import validate_password - from django.core.exceptions import ValidationError as ValError - - try: - validate_password(new_password, user=user) - except ValError as e: - return JsonResponse({"status": "error", "message": e.messages[0]}) - - user.set_password(new_password) - user.save() - - return JsonResponse({"status": "success", "message": "密码修改成功,请重新登录"}) - - -import secrets as _secrets - - -def _gen_code(length=6): - return "".join([_secrets.choice("0123456789") for _ in range(length)]) - - -@require_http_methods(["POST"]) -@csrf_protect -@rate_limit.email_code_rate_limit -def send_register_email_code(request): - """Send a one-time code to the supplied email for registration.""" - reglink_token = request.POST.get("reglink_token", "").strip() - - if reglink_token: - try: - reglink = RegistrationLink.objects.get(token=reglink_token) - if not reglink.is_valid: - return JsonResponse( - {"status": "error", "message": "邀请链接无效或已失效"}, status=400 - ) - except RegistrationLink.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "邀请链接不存在"}, status=400 - ) - else: - from utils.site_group import get_effective_config - - site_group = getattr(request, "site_group", None) - ec = get_effective_config(site_group) - if not ec.enable_registration: - return JsonResponse( - {"status": "error", "message": "注册功能已被管理员禁用"}, status=400 - ) - - email = request.POST.get("email") - - # Validate email - if not email: - return JsonResponse({"status": "error", "message": "缺少email"}, status=400) - - # 验证邮箱后缀 - from utils.site_group import get_effective_config - - site_group = getattr(request, "site_group", None) - ec = get_effective_config(site_group) - - if not ec.is_email_suffix_allowed(email): - email_suffix = "@" + email.split("@")[1] if "@" in email else "" - suffix_data = ec.get_email_suffix_lists() - if suffix_data["whitelist"]: - msg = f"邮箱后缀 {email_suffix} 不在允许的列表中" - else: - msg = f"邮箱后缀 {email_suffix} 已被禁止使用" - return JsonResponse({"status": "error", "message": msg}, status=400) - - # 验证邮箱格式 - from django.core.validators import validate_email - from django.core.exceptions import ValidationError - - try: - validate_email(email) - except ValidationError: - return JsonResponse( - {"status": "error", "message": "请输入有效的邮箱地址"}, status=400 - ) - - from .captcha_service import validate_captcha - - is_valid, error_msg = validate_captcha(request, scene="email") - - if not is_valid: - return JsonResponse({"status": "error", "message": error_msg}, status=400) - - code = _gen_code(6) - cache_key = f"register_email_code:{email}" - cache.set(cache_key, code, timeout=10 * 60) - - subject = "2c2a 注册验证码" - message_body = f"您的注册验证码是: {code},有效期10分钟。" - html_body = f""" - - - - - {subject} - - - -
-
-

2c2a 验证码服务

-
-
-

您好!

-

感谢您注册2c2a账户。

-

您的验证码是:

-
{code}
-

此验证码将在10分钟后失效,请及时使用。

-

如果您没有进行相关操作,请忽略此邮件。

-
- -
- - - """ - - from .email_service import EmailService - - try: - EmailService.send_email_async( - to_emails=[email], - subject=subject, - text_body=message_body, - html_body=html_body, - ) - except Exception as e: - import logging as _logging - - _logging.getLogger(__name__).error( - f"发送注册验证码邮件任务派发失败: {str(e)}", exc_info=True - ) - return JsonResponse( - {"status": "error", "message": "SMTP配置不完整"}, status=500 - ) - - return JsonResponse({"status": "ok"}) - - -@method_decorator(rate_limit.register_rate_limit, name="dispatch") -class RegisterByLinkView(CreateView): - model = User - form_class = UserRegistrationForm - template_name = "accounts/register.html" - success_url = reverse_lazy("accounts:login") - - def dispatch(self, request, *args, **kwargs): - token = kwargs.get("token") - try: - self.reglink = RegistrationLink.objects.select_related("group").get( - token=token - ) - except RegistrationLink.DoesNotExist: - messages.error(request, "注册链接不存在") - return redirect("accounts:register") - - if self.reglink.is_exhausted: - messages.error(request, "此注册链接可用次数已用完") - return redirect("accounts:register") - - if self.reglink.is_expired: - messages.error(request, "此注册链接已过期") - return redirect("accounts:register") - - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["reglink"] = self.reglink - context["target_group"] = self.reglink.group - context.update(get_captcha_context("email", self.request)) - context.update(get_theme_context()) - return context - - def form_valid(self, form): - import hmac - - email = form.cleaned_data.get("email") - email_code = self.request.POST.get("email_code") - if not (email and email_code): - form.add_error(None, "邮箱验证码缺失") - return self.form_invalid(form) - - cache_key = f"register_email_code:{email}" - expected = cache.get(cache_key) - if expected is None: - form.add_error(None, "邮箱验证码已过期或不存在") - return self.form_invalid(form) - if not hmac.compare_digest(str(expected), str(email_code)): - form.add_error(None, "邮箱验证码错误") - return self.form_invalid(form) - - cache.delete(cache_key) - - user = form.save() - - user.groups.set([self.reglink.group]) - user.sync_staff_status() - - self.reglink.increment_usage(user) - - messages.success( - self.request, f"注册成功!您已加入「{self.reglink.group.name}」组,请登录。" - ) - return redirect(self.success_url) - - def form_invalid(self, form): - messages.error(self.request, "注册失败,请检查表单中的错误。") - return super().form_invalid(form) - - -@login_required -@require_http_methods(["POST"]) -@rate_limit.avatar_upload_rate_limit -def upload_avatar(request): - """上传头像""" - if request.method == "POST" and request.FILES.get("avatar"): - avatar_file = request.FILES["avatar"] - user = request.user - - # 验证文件扩展名 - allowed_extensions = [".jpg", ".jpeg", ".png", ".gif"] - ext = os.path.splitext(avatar_file.name)[1].lower() - if ext not in allowed_extensions: - return JsonResponse({"status": "error", "message": "不支持的图片格式"}) - - # 验证文件大小 (5MB) - if avatar_file.size > 5 * 1024 * 1024: - return JsonResponse({"status": "error", "message": "图片大小不能超过5MB"}) - - try: - # 验证文件确实是图像文件,并检查是否包含恶意内容 - image = Image.open(avatar_file) - image.verify() # 验证图像完整性 - - # 重新打开文件,因为verify()会将指针移到末尾 - avatar_file.seek(0) - - # 再次打开图像用于尺寸检查 - image = Image.open(avatar_file) - - # 检查图像尺寸是否合理(防止像素炸弹) - max_width, max_height = 5000, 5000 # 最大允许尺寸 - if image.width > max_width or image.height > max_height: - return JsonResponse({"status": "error", "message": "图片尺寸过大"}) - - # 限制最小图像尺寸 - min_width, min_height = 10, 10 - if image.width < min_width or image.height < min_height: - return JsonResponse({"status": "error", "message": "图片尺寸过小"}) - - except Exception: - return JsonResponse( - {"status": "error", "message": "上传的文件不是有效的图片"} - ) - - # 重置文件指针以供保存 - avatar_file.seek(0) - - # 保存头像 - user.avatar = avatar_file - user.save() - - return JsonResponse({"status": "success", "message": "头像上传成功"}) - - return JsonResponse({"status": "error", "message": "没有上传文件"}) - - -@method_decorator(rate_limit.register_rate_limit, name="dispatch") -class ForgotPasswordView(TemplateView): - """忘记密码视图""" - - template_name = "accounts/forgot_password.html" - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context.update(get_captcha_context("email", self.request)) - context.update(get_theme_context()) - return context - - def post(self, request, *args, **kwargs): - """处理POST请求""" - email = request.POST.get("email") - email_code = request.POST.get("email_code") - new_password1 = request.POST.get("new_password1") - new_password2 = request.POST.get("new_password2") - - # 验证输入 - if not (email and email_code and new_password1 and new_password2): - messages.error(request, "请填写所有必需字段") - return self.render_to_response(self.get_context_data()) - - # 1. 行为验证码 - from .captcha_service import validate_captcha - - is_valid, error_msg = validate_captcha(request, scene="email") - if not is_valid: - messages.error(request, error_msg) - return self.render_to_response(self.get_context_data()) - - # 2. 邮箱验证码 - import hmac - - cache_key = f"forgot_password_email_code:{email}" - expected = cache.get(cache_key) - if expected is None: - messages.error(request, "邮箱验证码已过期或不存在") - return self.render_to_response(self.get_context_data()) - if not hmac.compare_digest(str(expected), str(email_code)): - messages.error(request, "邮箱验证码错误") - return self.render_to_response(self.get_context_data()) - - # 3. 用户存在性检查 - user_exists = User.objects.filter(email=email).exists() - if not user_exists: - messages.success(request, "如果该邮箱已注册,密码重置邮件已发送") - return redirect("accounts:login") - - # 4. 封禁账户检查 - from .user_service import check_ban_status - - ban = check_ban_status(email) - if ban["is_banned"]: - messages.error(request, "该邮箱关联的账户已被封禁,无法重置密码") - return self.render_to_response(self.get_context_data()) - - # 5. 密码重置 - if new_password1 != new_password2: - messages.error(request, "两次输入的密码不一致") - return self.render_to_response(self.get_context_data()) - - from django.contrib.auth.password_validation import validate_password - from django.core.exceptions import ValidationError as ValError - - try: - validate_password(new_password1) - except ValError as e: - messages.error(request, e.messages[0]) - return self.render_to_response(self.get_context_data()) - - user = User.objects.get(email=email) - user.set_password(new_password1) - user.save() - - # 清除验证码缓存 - cache.delete(cache_key) - - messages.success(request, "密码重置成功,请使用新密码登录") - return redirect("accounts:login") - - -@require_http_methods(["POST"]) -@csrf_protect -@rate_limit.email_code_rate_limit -def send_forgot_password_email_code(request): - """Send a one-time code to the supplied email for password reset.""" - email = request.POST.get("email") - - if not email: - return JsonResponse({"status": "error", "message": "缺少email"}, status=400) - - from .captcha_service import validate_captcha - - is_valid, error_msg = validate_captcha(request, scene="email") - - if not is_valid: - return JsonResponse({"status": "error", "message": error_msg}, status=400) - - user_exists = User.objects.filter(email=email).exists() - - if not user_exists: - return JsonResponse({"status": "ok"}) - - code = _gen_code(6) - cache_key = f"forgot_password_email_code:{email}" - cache.set(cache_key, code, timeout=10 * 60) - - import os - - if os.environ.get("2C2A_DEMO", "").lower() == "1": - import logging as _logging - - _logging.getLogger(__name__).info( - f"DEMO模式: 模拟发送忘记密码验证码邮件至 {email}" - ) - return JsonResponse({"status": "ok"}) - - subject = "2c2a 重置密码验证码" - message_body = f"您的重置密码验证码是: {code},有效期10分钟。" - html_body = f""" - - - - - {subject} - - - -
-
-

2c2a 验证码服务

-
-
-

您好!

-

您正在重置2c2a账户的密码。

-

您的验证码是:

-
{code}
-

此验证码将在10分钟后失效,请及时使用。

-

如果您没有进行相关操作,请忽略此邮件。

-
- -
- - - """ - - from .email_service import EmailService - - try: - EmailService.send_email_async( - to_emails=[email], - subject=subject, - text_body=message_body, - html_body=html_body, - ) - except Exception as e: - import logging as _logging - - _logging.getLogger(__name__).error( - f"发送忘记密码验证码邮件任务派发失败: {str(e)}", exc_info=True - ) - return JsonResponse( - {"status": "error", "message": "SMTP配置不完整"}, status=500 - ) - - return JsonResponse({"status": "ok"}) - - -# ── 邮箱绑定 API ───────────────────────────────────── - - -@login_required -@require_http_methods(["GET"]) -def email_list_api(request): - """获取当前用户的所有绑定邮箱""" - from .models import UserEmail - - emails = UserEmail.objects.filter(user=request.user).order_by( - "-is_primary", "created_at" - ) - data = [ - { - "id": ue.pk, - "email": ue.email, - "is_primary": ue.is_primary, - "is_verified": ue.is_verified, - "created_at": ue.created_at.isoformat(), - } - for ue in emails - ] - return JsonResponse({"status": "ok", "emails": data}) - - -@login_required -@require_http_methods(["POST"]) -@csrf_protect -@rate_limit.email_code_rate_limit -def send_bind_email_code(request): - """发送邮箱绑定验证码""" - email = request.POST.get("email") - if not email: - return JsonResponse( - {"status": "error", "message": "缺少email"}, - status=400, - ) - - from django.core.validators import validate_email - from django.core.exceptions import ValidationError - - try: - validate_email(email) - except ValidationError: - return JsonResponse( - {"status": "error", "message": "请输入有效的邮箱地址"}, - status=400, - ) - - from utils.site_group import get_effective_config - - site_group = getattr(request, "site_group", None) - ec = get_effective_config(site_group) - if not ec.is_email_suffix_allowed(email): - return JsonResponse( - {"status": "error", "message": "该邮箱后缀不在允许的列表中"}, - status=400, - ) - - from .models import UserEmail - - if UserEmail.objects.filter(user=request.user, email=email).exists(): - return JsonResponse( - {"status": "error", "message": "该邮箱已绑定到当前账户"}, - status=400, - ) - - code = _gen_code(6) - cache_key = f"bind_email_code:{email}" - cache.set(cache_key, code, timeout=10 * 60) - - subject = "2c2a 邮箱绑定验证码" - message_body = f"您的邮箱绑定验证码是: {code},有效期10分钟。" - html_body = f""" - - - - - {subject} - - - -
-
-

2c2a 验证码服务

-
-
-

您好!

-

您正在绑定邮箱到2c2a账户。

-

您的验证码是:

-
{code}
-

此验证码将在10分钟后失效,请及时使用。

-

如果您没有进行相关操作,请忽略此邮件。

-
- -
- - - """ - - from .email_service import EmailService - - sg_id = site_group.pk if site_group else None - try: - EmailService.send_email_async( - to_emails=[email], - subject=subject, - text_body=message_body, - html_body=html_body, - site_group_id=sg_id, - ) - except Exception as e: - import logging as _logging - - _logging.getLogger(__name__).error( - f"发送绑定验证码邮件任务派发失败: {str(e)}", - exc_info=True, - ) - return JsonResponse( - {"status": "error", "message": "SMTP配置不完整"}, - status=500, - ) - - return JsonResponse({"status": "ok"}) - - -@login_required -@require_http_methods(["POST"]) -@csrf_protect -def email_bind_api(request): - """绑定邮箱""" - email = request.POST.get("email") - email_code = request.POST.get("email_code") - - if not (email and email_code): - return JsonResponse( - {"status": "error", "message": "缺少必要参数"}, - status=400, - ) - - import hmac - - cache_key = f"bind_email_code:{email}" - expected = cache.get(cache_key) - if expected is None: - return JsonResponse( - {"status": "error", "message": "验证码已过期或不存在"}, - status=400, - ) - if not hmac.compare_digest(str(expected), str(email_code)): - return JsonResponse( - {"status": "error", "message": "验证码错误"}, - status=400, - ) - cache.delete(cache_key) - - from .user_service import bind_email - - result = bind_email(request.user, email) - - if result["action"] == "banned": - return JsonResponse( - { - "status": "error", - "message": result["reason"], - "action": "banned", - }, - status=403, - ) - elif result["action"] == "merge_required": - return JsonResponse( - { - "status": "error", - "message": result["reason"], - "action": "merge_required", - "merge_info": result["merge_info"], - }, - status=409, - ) - elif result["success"]: - return JsonResponse({"status": "ok", "message": result["reason"]}) - else: - return JsonResponse( - {"status": "error", "message": result["reason"]}, - status=400, - ) - - -@login_required -@require_http_methods(["POST"]) -@csrf_protect -def email_set_primary_api(request, email_id): - """设置主邮箱""" - from .models import UserEmail - - try: - ue = UserEmail.objects.get(pk=email_id, user=request.user) - except UserEmail.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "邮箱记录不存在"}, - status=404, - ) - - if not ue.is_verified: - return JsonResponse( - {"status": "error", "message": "邮箱未验证,无法设为主邮箱"}, - status=400, - ) - - from .user_service import set_primary_email - - result = set_primary_email(request.user, ue.email) - if result["success"]: - return JsonResponse({"status": "ok", "message": result["reason"]}) - return JsonResponse( - {"status": "error", "message": result["reason"]}, - status=400, - ) - - -@login_required -@require_http_methods(["POST"]) -@csrf_protect -def email_unbind_api(request, email_id): - """解绑邮箱""" - from .models import UserEmail - - try: - ue = UserEmail.objects.get(pk=email_id, user=request.user) - except UserEmail.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "邮箱记录不存在"}, - status=404, - ) - - from .user_service import unbind_email - - result = unbind_email(request.user, ue.email) - if result["success"]: - return JsonResponse({"status": "ok", "message": result["reason"]}) - return JsonResponse( - {"status": "error", "message": result["reason"]}, - status=400, - ) - - -@login_required -@require_http_methods(["POST"]) -@csrf_protect -def email_merge_confirm_api(request): - """确认账户合并""" - other_user_id = request.POST.get("other_user_id") - keep_newer = request.POST.get("keep_newer", "true").lower() == "true" - - if not other_user_id: - return JsonResponse( - {"status": "error", "message": "缺少必要参数"}, - status=400, - ) - - try: - other_user = User.objects.get(pk=other_user_id) - except User.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "目标用户不存在"}, - status=404, - ) - - from .user_service import merge_accounts - - result = merge_accounts( - source_user=other_user, - target_user=request.user, - keep_newer=keep_newer, - ) - - if result["success"]: - return JsonResponse({"status": "ok", "message": result["reason"]}) - return JsonResponse( - {"status": "error", "message": result["reason"]}, - status=400, - ) - - -# ── 站点迁移 ────────────────────────────────────────── - - -@login_required -def migrate_view(request): - """站点迁移页面""" - sg_id = request.session.get("pending_migration_sg_id") - if not sg_id: - return redirect("dashboard:index") - - from apps.dashboard.models import SiteGroup - - try: - site_group = SiteGroup.objects.get(pk=sg_id, is_active=True) - except SiteGroup.DoesNotExist: - request.session.pop("pending_migration_sg_id", None) - messages.error(request, "站点组不存在") - return redirect("dashboard:index") - - if request.method == "POST": - action = request.POST.get("action") - - if action == "confirm": - from .user_service import migrate_user_to_site_group - - result = migrate_user_to_site_group(request.user, site_group) - - if result["success"]: - request.session.pop("pending_migration_sg_id", None) - messages.success(request, f"已成功迁移到站点组「{site_group.name}」") - if request.user.is_staff or request.user.is_superuser: - return redirect("/admin/") - return redirect("dashboard:index") - elif result["reason"] == "email_not_compliant": - messages.warning(request, result["message"]) - else: - messages.error(request, result.get("reason", "迁移失败")) - return redirect("dashboard:index") - - elif action == "skip": - # 不迁移则退出登录 - request.session.pop("pending_migration_sg_id", None) - logout(request) - messages.info(request, "您已选择不迁移,已退出登录") - return redirect("accounts:login") - - # GET 或迁移失败后重新渲染 - # 只检查邮箱合规性,不执行迁移 - from utils.site_group import get_effective_config - from .models import UserEmail - - ec = get_effective_config(site_group) - suffix_data = ec.get_email_suffix_lists() - has_whitelist = bool(suffix_data["whitelist"]) - has_blacklist = bool(suffix_data["blacklist"]) - email_not_compliant = False - - if has_whitelist or has_blacklist: - user_emails = UserEmail.objects.filter(user=request.user) - email_list = list(user_emails.values_list("email", flat=True)) - if not email_list and request.user.email: - email_list = [request.user.email] - email_not_compliant = not any(ec.is_email_suffix_allowed(e) for e in email_list) - - context = { - "site_group_name": site_group.name, - "username": request.user.username, - "email_not_compliant": email_not_compliant, - } - context.update(get_theme_context()) - return render(request, "accounts/migrate.html", context) - - -def banned_view(request): - """封禁用户提示页面""" - if not request.user.is_authenticated: - return redirect("accounts:login") - - from .models import UserBan - - ban = UserBan.objects.filter(user=request.user).first() - if not ban: - request.session.pop("is_banned", None) - return redirect("dashboard:index") - - from apps.tickets.models import TicketCategory - - categories = TicketCategory.objects.filter( - is_active=True, allow_banned_users=True - ).order_by("display_order") - - context = { - "username": request.user.username, - "categories": categories, - "ban_reason": ban.reason, - "ban_time": ban.created_at, - } - context.update(get_theme_context()) - return render(request, "accounts/banned.html", context) diff --git a/apps/accounts/views_admin.py b/apps/accounts/views_admin.py deleted file mode 100644 index 3d09ea1..0000000 --- a/apps/accounts/views_admin.py +++ /dev/null @@ -1,380 +0,0 @@ -""" -超管后台视图 - -包含超管仪表盘视图,所有视图均使用 @admin_required 装饰器保护。 -超管可查看系统全局数据;站点组管理员可查看当前站点组数据; -提供商仅可查看自己相关的统计数据。 -""" - -from django.db.models import Q -from django.shortcuts import render -from django.contrib.auth import get_user_model - -from apps.accounts.provider_decorators import admin_required -from utils.provider import get_provider_hosts, get_provider_products - -User = get_user_model() - - -@admin_required -def admin_dashboard(request): - """ - 超管指挥中心视图 - - 渲染 admin_base/dashboard.html,传递任务导向的上下文数据。 - 设计理念:不是数据库 CRUD 界面,而是智能指挥中心。 - """ - from apps.hosts.models import Host, HostGroup - from apps.operations.models import ( - AccountOpeningRequest, - CloudComputerUser, - Product, - ProductGroup, - ProductInvitationToken, - ProductAccessGrant, - RdpDomainRoute, - ) - from apps.tickets.models import Ticket, TicketCategory - from apps.audit.models import AuditLog - - is_superuser = request.user.is_superuser - site_group = getattr(request, "site_group", None) - is_site_admin = ( - not is_superuser and site_group and request.user.is_site_group_admin(site_group) - ) - - provider_hosts = get_provider_hosts(request.user) - provider_products = get_provider_products(request.user) - - # === 基础统计(数据隔离) === - if is_superuser: - total_users = User.objects.count() - total_hosts = Host.objects.count() - total_hostgroups = HostGroup.objects.count() - total_products = Product.objects.count() - total_productgroups = ProductGroup.objects.count() - pending_requests = AccountOpeningRequest.objects.filter( - status="pending" - ).count() - total_cloud_users = CloudComputerUser.objects.filter(status="active").count() - active_tokens = ProductInvitationToken.objects.filter(is_active=True).count() - active_grants = ProductAccessGrant.objects.filter(is_revoked=False).count() - open_tickets = Ticket.objects.filter( - status__in=["pending", "processing", "waiting_feedback"] - ).count() - total_categories = TicketCategory.objects.count() - total_routes = RdpDomainRoute.objects.count() - total_audit_logs = AuditLog.objects.count() - elif is_site_admin: - sg = site_group - total_users = ( - User.objects.filter( - Q(provider_hosts__site_group=sg) | Q(created_products__site_group=sg) - ) - .distinct() - .count() - ) - total_hosts = Host.objects.filter(site_group=sg).count() - total_hostgroups = HostGroup.objects.filter(site_group=sg).count() - total_products = Product.objects.filter(site_group=sg).count() - total_productgroups = ProductGroup.objects.filter(site_group=sg).count() - pending_requests = AccountOpeningRequest.objects.filter( - status="pending", - target_product__in=Product.objects.filter(site_group=sg), - ).count() - total_cloud_users = CloudComputerUser.objects.filter( - status="active", - product__in=Product.objects.filter(site_group=sg), - ).count() - active_tokens = ProductInvitationToken.objects.filter( - is_active=True, - product__in=Product.objects.filter(site_group=sg), - ).count() - active_grants = ProductAccessGrant.objects.filter( - is_revoked=False, - product__in=Product.objects.filter(site_group=sg), - ).count() - open_tickets = ( - Ticket.objects.filter( - status__in=["pending", "processing", "waiting_feedback"], - ) - .filter(Q(related_cloud_computer__product__site_group=sg)) - .count() - ) - total_categories = TicketCategory.objects.count() - total_routes = RdpDomainRoute.objects.filter( - product__in=Product.objects.filter(site_group=sg), - ).count() - total_audit_logs = AuditLog.objects.filter( - Q(host__site_group=sg) | Q(host__isnull=True) - ).count() - else: - total_users = ( - User.objects.filter( - Q(cloud_users__product__in=provider_products) - | Q(provider_hosts__in=provider_hosts) - ) - .distinct() - .count() - ) - total_hosts = provider_hosts.count() - total_hostgroups = HostGroup.objects.filter(created_by=request.user).count() - total_products = provider_products.count() - total_productgroups = ProductGroup.objects.filter( - created_by=request.user - ).count() - pending_requests = AccountOpeningRequest.objects.filter( - status="pending", - target_product__in=provider_products, - ).count() - total_cloud_users = CloudComputerUser.objects.filter( - status="active", - product__in=provider_products, - ).count() - active_tokens = ProductInvitationToken.objects.filter( - is_active=True, - created_by=request.user, - ).count() - active_grants = ProductAccessGrant.objects.filter( - is_revoked=False, - product__in=provider_products, - ).count() - open_tickets = Ticket.objects.filter( - status__in=["pending", "processing", "waiting_feedback"], - related_cloud_computer__product__in=provider_products, - ).count() - total_categories = TicketCategory.objects.filter( - created_by=request.user - ).count() - total_routes = RdpDomainRoute.objects.filter( - product__in=provider_products, - ).count() - total_audit_logs = AuditLog.objects.filter(host__in=provider_hosts).count() - - # === 需要关注的事项 === - if is_superuser: - hosts_without_providers = Host.objects.filter(providers__isnull=True).count() - offline_hosts = Host.objects.filter(status="offline").count() - elif is_site_admin: - sg = site_group - sg_hosts = Host.objects.filter(site_group=sg) - hosts_without_providers = sg_hosts.filter(providers__isnull=True).count() - offline_hosts = sg_hosts.filter(status="offline").count() - else: - hosts_without_providers = provider_hosts.filter(providers__isnull=True).count() - offline_hosts = provider_hosts.filter(status="offline").count() - - attention_items = [] - if hosts_without_providers > 0: - attention_items.append( - { - "icon": "dns", - "description": f"{hosts_without_providers} 台主机未分配提供商", - "action_label": "分配", - "action_url": "admin:admin_providers:provider_host_list", - "severity": "warning", - } - ) - if pending_requests > 0: - attention_items.append( - { - "icon": "person_add", - "description": f"{pending_requests} 条开户申请待审批", - "action_label": "审批", - "action_url": "admin:admin_operations:request_list", - "severity": "warning", - } - ) - if open_tickets > 0: - attention_items.append( - { - "icon": "confirmation_number", - "description": f"{open_tickets} 个工单待处理", - "action_label": "处理", - "action_url": "admin:admin_tickets:ticket_list", - "severity": "warning", - } - ) - if offline_hosts > 0: - attention_items.append( - { - "icon": "cloud_off", - "description": f"{offline_hosts} 台主机离线", - "action_label": "查看", - "action_url": "admin:admin_hosts:host_list", - "severity": "error", - } - ) - - # === 快捷操作 === - quick_actions = [ - { - "label": "添加主机", - "icon": "dns", - "url": "admin:admin_hosts:host_create", - "variant": "filled", - }, - { - "label": "入驻提供商", - "icon": "person_add", - "url": "admin:admin_users:user_create", - "variant": "filled", - }, - { - "label": "审批申请", - "icon": "how_to_reg", - "url": "admin:admin_operations:request_list", - "variant": "filled", - "badge": pending_requests if pending_requests > 0 else None, - }, - { - "label": "查看工单", - "icon": "confirmation_number", - "url": "admin:admin_tickets:ticket_list", - "variant": "filled", - "badge": open_tickets if open_tickets > 0 else None, - }, - ] - - # === 系统健康状态 === - if is_superuser: - online_hosts = Host.objects.filter(status="online").count() - active_tunnels = Host.objects.filter(tunnel_status="online").count() - inactive_tunnels = ( - Host.objects.exclude(tunnel_status="no_tunnel") - .exclude(tunnel_status="online") - .count() - ) - elif is_site_admin: - sg = site_group - sg_hosts = Host.objects.filter(site_group=sg) - online_hosts = sg_hosts.filter(status="online").count() - active_tunnels = sg_hosts.filter(tunnel_status="online").count() - inactive_tunnels = ( - sg_hosts.exclude(tunnel_status="no_tunnel") - .exclude(tunnel_status="online") - .count() - ) - else: - online_hosts = provider_hosts.filter(status="online").count() - active_tunnels = provider_hosts.filter(tunnel_status="online").count() - inactive_tunnels = ( - provider_hosts.exclude(tunnel_status="no_tunnel") - .exclude(tunnel_status="online") - .count() - ) - - system_health = { - "online_hosts": online_hosts, - "offline_hosts": offline_hosts, - "total_hosts": total_hosts, - "active_tunnels": active_tunnels, - "inactive_tunnels": inactive_tunnels, - "active_users": total_cloud_users, - "active_products": total_products, - } - - # === 最近动态 === - if is_superuser: - recent_logs = AuditLog.objects.select_related("user", "host").order_by( - "-timestamp" - )[:10] - elif is_site_admin: - sg = site_group - recent_logs = ( - AuditLog.objects.filter(Q(host__site_group=sg) | Q(host__isnull=True)) - .select_related("user", "host") - .order_by("-timestamp")[:10] - ) - else: - recent_logs = ( - AuditLog.objects.filter(host__in=provider_hosts) - .select_related("user", "host") - .order_by("-timestamp")[:10] - ) - - # 为审计日志构建可读描述 - action_display_map = dict(AuditLog.ACTION_CHOICES) - recent_activities = [] - for log in recent_logs: - action_text = action_display_map.get(log.action, log.action) - description = action_text - if log.host: - description = f"{action_text} - {log.host.name}" - recent_activities.append( - { - "timestamp": log.timestamp, - "icon": _get_action_icon(log.action), - "description": description, - "actor": log.user.username if log.user else "系统", - "success": log.success, - } - ) - - context = { - "stats": { - "total_users": total_users, - "total_hosts": total_hosts, - "total_hostgroups": total_hostgroups, - "total_products": total_products, - "total_productgroups": total_productgroups, - "pending_requests": pending_requests, - "total_cloud_users": total_cloud_users, - "active_tokens": active_tokens, - "active_grants": active_grants, - "open_tickets": open_tickets, - "total_categories": total_categories, - "total_routes": total_routes, - "total_audit_logs": total_audit_logs, - }, - "attention_items": attention_items, - "quick_actions": quick_actions, - "recent_activities": recent_activities, - "system_health": system_health, - "page_title": "超管指挥中心", - "active_nav": "dashboard", - } - - return render(request, "admin_base/dashboard.html", context) - - -def _get_action_icon(action): - """根据审计操作类型返回对应的 Material Icon 名称""" - icon_map = { - "create_user": "person_add", - "delete_user": "person_remove", - "reset_password": "key", - "connect_host": "lan", - "modify_host": "edit", - "view_password": "visibility", - "approve_request": "check_circle", - "reject_request": "cancel", - "bootstrap_host": "rocket_launch", - "issue_cert": "verified", - "revoke_cert": "gpp_bad", - "create_host": "add_circle", - "delete_host": "remove_circle", - "update_host": "update", - "process_opening_request": "how_to_reg", - "batch_process_requests": "playlist_add_check", - "login": "login", - "logout": "logout", - "view_audit_log": "receipt_long", - "admin_action": "admin_panel_settings", - "tunnel_online": "link", - "tunnel_offline": "link_off", - "tunnel_heartbeat_timeout": "heart_broken", - "rdp_connect": "desktop_windows", - "rdp_disconnect": "desktop_access_disabled", - "remote_exec": "terminal", - "remote_exec_result": "terminal", - "domain_bind": "language", - "domain_unbind": "language", - "create_ticket": "add_task", - "update_ticket": "edit_note", - "assign_ticket": "assignment_ind", - "change_ticket_status": "swap_horiz", - "close_ticket": "task_alt", - "add_ticket_comment": "comment", - } - return icon_map.get(action, "circle") diff --git a/apps/accounts/views_admin_groups.py b/apps/accounts/views_admin_groups.py deleted file mode 100644 index 78ecc28..0000000 --- a/apps/accounts/views_admin_groups.py +++ /dev/null @@ -1,127 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth.models import Group - -from apps.accounts.provider_decorators import superadmin_required -from apps.accounts.models import GroupProfile - - -@superadmin_required -def group_list(request): - group_profiles = GroupProfile.objects.select_related('group').order_by( - 'sort_order', 'group__name' - ) - unprofiled_groups = Group.objects.filter(profile__isnull=True).order_by( - 'name' - ) - - context = { - 'group_profiles': group_profiles, - 'unprofiled_groups': unprofiled_groups, - 'active_nav': 'groups', - } - return render(request, 'admin_base/groups/group_list.html', context) - - -@superadmin_required -def group_create(request): - if request.method == 'POST': - name = request.POST.get('name', '').strip() - description = request.POST.get('description', '').strip() - sort_order = request.POST.get('sort_order', 0) - auto_staff = request.POST.get('auto_staff') == 'on' - - if not name: - messages.error(request, '用户组名称不能为空') - return redirect('admin:admin_groups:group_create') - - if Group.objects.filter(name=name).exists(): - messages.error(request, f'用户组「{name}」已存在') - return redirect('admin:admin_groups:group_create') - - group = Group.objects.create(name=name) - GroupProfile.objects.create( - group=group, - is_default=False, - description=description, - auto_staff=auto_staff, - sort_order=int(sort_order), - ) - messages.success(request, f'用户组「{name}」创建成功') - return redirect('admin:admin_groups:group_list') - - context = { - 'is_create': True, - 'active_nav': 'groups', - } - return render(request, 'admin_base/groups/group_form.html', context) - - -@superadmin_required -def group_update(request, pk): - group_profile = get_object_or_404(GroupProfile, pk=pk) - - if request.method == 'POST': - name = request.POST.get('name', '').strip() - description = request.POST.get('description', '').strip() - sort_order = request.POST.get('sort_order', 0) - auto_staff = request.POST.get('auto_staff') == 'on' - - if not name: - messages.error(request, '用户组名称不能为空') - return redirect( - 'admin:admin_groups:group_edit', pk=group_profile.pk - ) - - if ( - Group.objects.filter(name=name) - .exclude(pk=group_profile.group.pk) - .exists() - ): - messages.error(request, f'用户组「{name}」已存在') - return redirect( - 'admin:admin_groups:group_edit', pk=group_profile.pk - ) - - group_profile.group.name = name - group_profile.group.save() - group_profile.description = description - group_profile.sort_order = int(sort_order) - group_profile.auto_staff = auto_staff - group_profile.save() - - for user in group_profile.group.user_set.all(): - user.sync_staff_status() - - messages.success(request, f'用户组「{name}」更新成功') - return redirect('admin:admin_groups:group_list') - - context = { - 'group_profile': group_profile, - 'is_create': False, - 'active_nav': 'groups', - } - return render(request, 'admin_base/groups/group_form.html', context) - - -@superadmin_required -def group_delete(request, pk): - group_profile = get_object_or_404(GroupProfile, pk=pk) - - if group_profile.is_default: - messages.error(request, '默认用户组不可删除') - return redirect('admin:admin_groups:group_list') - - if request.method == 'POST': - group_name = group_profile.group.name - group_profile.group.delete() - messages.success(request, f'用户组「{group_name}」已删除') - return redirect('admin:admin_groups:group_list') - - context = { - 'group_profile': group_profile, - 'active_nav': 'groups', - } - return render( - request, 'admin_base/groups/group_confirm_delete.html', context - ) diff --git a/apps/accounts/views_admin_reglinks.py b/apps/accounts/views_admin_reglinks.py deleted file mode 100644 index b1a4c4d..0000000 --- a/apps/accounts/views_admin_reglinks.py +++ /dev/null @@ -1,158 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth.models import Group -from django.core.paginator import Paginator -from django.db.models import Q -from django.utils import timezone - -from apps.accounts.provider_decorators import superadmin_required -from apps.accounts.models import RegistrationLink - - -@superadmin_required -def reglink_list(request): - queryset = RegistrationLink.objects.select_related( - 'group', 'created_by', 'used_by' - ).order_by('-created_at') - - status_filter = request.GET.get('status', '').strip() - if status_filter == 'unused': - queryset = queryset.filter(used=False) - elif status_filter == 'used': - queryset = queryset.filter(used=True) - elif status_filter == 'expired': - queryset = queryset.filter(used=False, expires_at__lt=timezone.now()) - - group_filter = request.GET.get('group', '').strip() - if group_filter: - queryset = queryset.filter(group_id=group_filter) - - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(note__icontains=search) | Q(token__icontains=search) - ) - - all_groups = Group.objects.select_related('profile').order_by('name') - - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'all_groups': all_groups, - 'status_filter': status_filter, - 'group_filter': group_filter, - 'search': search, - 'active_nav': 'reglinks', - } - return render(request, 'admin_base/reglinks/reglink_list.html', context) - - -@superadmin_required -def reglink_create(request): - all_groups = Group.objects.select_related('profile').order_by('name') - - if request.method == 'POST': - group_id = request.POST.get('group', '').strip() - expires_at_str = request.POST.get('expires_at', '').strip() - max_uses_str = request.POST.get('max_uses', '').strip() - note = request.POST.get('note', '').strip() - - if not group_id: - messages.error(request, '请选择注册后的用户组') - context = { - 'all_groups': all_groups, - 'active_nav': 'reglinks', - } - return render(request, 'admin_base/reglinks/reglink_form.html', context) - - try: - group = Group.objects.get(pk=group_id) - except Group.DoesNotExist: - messages.error(request, '所选用户组不存在') - context = { - 'all_groups': all_groups, - 'active_nav': 'reglinks', - } - return render(request, 'admin_base/reglinks/reglink_form.html', context) - - max_uses = 1 - if max_uses_str: - try: - max_uses = int(max_uses_str) - if max_uses < 0: - raise ValueError - except (ValueError, TypeError): - messages.error(request, '最大使用次数必须为非负整数') - context = { - 'all_groups': all_groups, - 'active_nav': 'reglinks', - } - return render( - request, 'admin_base/reglinks/reglink_form.html', context - ) - - expires_at = None - if expires_at_str: - try: - expires_at = timezone.datetime.fromisoformat(expires_at_str) - if timezone.is_naive(expires_at): - expires_at = timezone.make_aware(expires_at) - except (ValueError, TypeError): - messages.error(request, '过期时间格式不正确') - context = { - 'all_groups': all_groups, - 'active_nav': 'reglinks', - } - return render( - request, 'admin_base/reglinks/reglink_form.html', context - ) - - reglink = RegistrationLink.objects.create( - group=group, - created_by=request.user, - max_uses=max_uses, - expires_at=expires_at, - note=note, - ) - - messages.success(request, f'注册链接创建成功,用户将加入「{group.name}」组') - return redirect('admin:admin_reglinks:reglink_list') - - context = { - 'all_groups': all_groups, - 'active_nav': 'reglinks', - } - return render(request, 'admin_base/reglinks/reglink_form.html', context) - - -@superadmin_required -def reglink_delete(request, pk): - reglink = get_object_or_404(RegistrationLink, pk=pk) - - if reglink.is_exhausted: - messages.error(request, '已用完的注册链接不可删除') - return redirect('admin:admin_reglinks:reglink_list') - - if request.method == 'POST': - reglink.delete() - messages.success(request, '注册链接已删除') - return redirect('admin:admin_reglinks:reglink_list') - - context = { - 'reglink': reglink, - 'active_nav': 'reglinks', - } - return render(request, 'admin_base/reglinks/reglink_confirm_delete.html', context) - - -@superadmin_required -def reglink_copy_url(request, pk): - reglink = get_object_or_404(RegistrationLink, pk=pk) - from django.urls import reverse - url = request.build_absolute_uri( - reverse('accounts:register_by_link', kwargs={'token': reglink.token}) - ) - return {'url': url} diff --git a/apps/accounts/views_admin_users.py b/apps/accounts/views_admin_users.py deleted file mode 100644 index 6ebdfdc..0000000 --- a/apps/accounts/views_admin_users.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -超管后台 - 用户管理视图 - -所有视图均使用 @superadmin_required 装饰器保护。 -超管后台无数据隔离,可查看系统全局数据。 -""" - -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.core.paginator import Paginator -from django.db.models import Q - -from apps.accounts.provider_decorators import superadmin_required -from apps.accounts.models import UserBan -from apps.accounts.user_service import ban_user, unban_user -from .forms_admin import ( - AdminUserCreateForm, - AdminUserUpdateForm, - AdminPasswordResetForm, -) - -User = get_user_model() - - -@superadmin_required -def user_list(request): - """ - 用户列表视图 - - 支持按用户名/邮箱/姓名搜索,按 is_staff/is_active 筛选。 - 显示用户组信息,分页展示。 - """ - queryset = User.objects.prefetch_related( - 'groups' - ).select_related('active_ban').order_by('-created_at') - - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(username__icontains=search) - | Q(email__icontains=search) - | Q(first_name__icontains=search) - | Q(last_name__icontains=search) - ) - - staff_filter = request.GET.get('is_staff', '').strip() - if staff_filter == '1': - queryset = queryset.filter(is_staff=True) - elif staff_filter == '0': - queryset = queryset.filter(is_staff=False) - - active_filter = request.GET.get('is_active', '').strip() - if active_filter == '1': - queryset = queryset.filter(is_active=True) - elif active_filter == '0': - queryset = queryset.filter(is_active=False) - - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'search': search, - 'staff_filter': staff_filter, - 'active_filter': active_filter, - 'active_nav': 'users', - } - - return render(request, 'admin_base/users/user_list.html', context) - - -@superadmin_required -def user_create(request): - """创建用户视图""" - if request.method == 'POST': - form = AdminUserCreateForm(request.POST) - if form.is_valid(): - user = form.save() - messages.success(request, f'用户「{user.username}」创建成功') - return redirect('admin:admin_users:user_list') - else: - form = AdminUserCreateForm() - - context = { - 'form': form, - 'is_create': True, - 'active_nav': 'users', - } - - return render(request, 'admin_base/users/user_form.html', context) - - -@superadmin_required -def user_update(request, pk): - """编辑用户视图""" - user = get_object_or_404(User, pk=pk) - - if request.method == 'POST': - form = AdminUserUpdateForm(request.POST, instance=user) - if form.is_valid(): - form.save() - messages.success( - request, f'用户「{user.username}」更新成功' - ) - return redirect('admin:admin_users:user_list') - else: - form = AdminUserUpdateForm(instance=user) - - context = { - 'form': form, - 'target_user': user, - 'is_create': False, - 'active_nav': 'users', - } - - return render(request, 'admin_base/users/user_form.html', context) - - -@superadmin_required -def user_delete(request, pk): - """删除用户视图(含自删除保护)""" - user = get_object_or_404(User, pk=pk) - - if user.pk == request.user.pk: - messages.error(request, '不能删除自己的账号') - return redirect('admin:admin_users:user_list') - - if request.method == 'POST': - username = user.username - user.delete() - messages.success(request, f'用户「{username}」已删除') - return redirect('admin:admin_users:user_list') - - context = { - 'target_user': user, - 'active_nav': 'users', - } - - return render( - request, 'admin_base/users/user_confirm_delete.html', context - ) - - -@superadmin_required -def user_toggle_active(request, pk): - """切换用户封禁状态(POST 操作,完成后重定向回列表)""" - user = get_object_or_404(User, pk=pk) - - if user.pk == request.user.pk: - messages.error(request, '不能封禁自己的账号') - return redirect('admin:admin_users:user_list') - - if request.method == 'POST': - is_banned = UserBan.objects.filter(user=user).exists() - if is_banned: - # 解封 - unban_user(user, unbanned_by=request.user) - messages.success(request, f'用户「{user.username}」已解封') - else: - # 封禁 - reason = request.POST.get('ban_reason', '').strip() - ban_user(user, reason=reason, banned_by=request.user) - messages.success(request, f'用户「{user.username}」已封禁') - - return redirect('admin:admin_users:user_list') - - -@superadmin_required -def user_reset_password(request, pk): - """重置用户密码视图""" - user = get_object_or_404(User, pk=pk) - - if request.method == 'POST': - form = AdminPasswordResetForm(request.POST) - if form.is_valid(): - user.set_password(form.cleaned_data['new_password1']) - user.save() - messages.success( - request, f'用户「{user.username}」密码已重置' - ) - return redirect('admin:admin_users:user_list') - else: - form = AdminPasswordResetForm() - - context = { - 'form': form, - 'target_user': user, - 'active_nav': 'users', - } - - return render( - request, 'admin_base/users/user_reset_password.html', context - ) diff --git a/apps/accounts/views_provider.py b/apps/accounts/views_provider.py deleted file mode 100644 index df18768..0000000 --- a/apps/accounts/views_provider.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -提供商后台视图 - -包含仪表盘视图,所有视图均使用 @provider_required 装饰器保护。 -统计数据通过 utils.provider 中的函数获取,确保数据隔离的一致性。 -""" - -from django.shortcuts import render - -from apps.accounts.provider_decorators import provider_required -from utils.provider import get_provider_hosts, get_provider_products - - -@provider_required -def provider_dashboard(request): - """ - 提供商仪表盘视图 - - 渲染 provider/dashboard.html,传递统计数据到模板。 - 所有统计均按 request.user 进行数据隔离。 - """ - user = request.user - - from apps.operations.models import AccountOpeningRequest, CloudComputerUser - - host_count = get_provider_hosts(user).count() - product_count = get_provider_products(user).count() - pending_request_count = AccountOpeningRequest.objects.filter( - target_product__created_by=user, status='pending' - ).count() - active_user_count = CloudComputerUser.objects.filter( - product__created_by=user, status='active' - ).count() - - context = { - 'host_count': host_count, - 'product_count': product_count, - 'pending_request_count': pending_request_count, - 'active_user_count': active_user_count, - 'stats': { - 'host_count': host_count, - 'product_count': product_count, - 'pending_request_count': pending_request_count, - 'active_user_count': active_user_count, - }, - 'page_title': '仪表盘', - 'active_nav': 'dashboard', - } - - return render(request, 'admin_base/provider/dashboard.html', context) diff --git a/apps/accounts/views_superadmin.py b/apps/accounts/views_superadmin.py deleted file mode 100644 index eb81c16..0000000 --- a/apps/accounts/views_superadmin.py +++ /dev/null @@ -1,175 +0,0 @@ -""" -超级管理员视图 - -用于超级管理员分配提供商给主机和主机组。 -所有视图均使用 @superadmin_required 装饰器保护。 -""" - -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.core.paginator import Paginator - -from apps.accounts.provider_decorators import superadmin_required -from apps.accounts.forms_superadmin import ( - HostProviderAssignForm, - HostGroupProviderAssignForm, -) -from apps.hosts.models import Host, HostGroup -from apps.hosts.views_admin import _get_permission_context - - -@superadmin_required -def superadmin_host_list(request): - """ - 超级管理员 - 主机列表视图 - - 显示所有主机及其已分配的提供商,支持搜索和筛选。 - """ - hosts = Host.objects.select_related('created_by').prefetch_related( - 'providers' - ).order_by('-created_at') - - # 搜索 - search = request.GET.get('search', '').strip() - if search: - hosts = hosts.filter( - name__icontains=search - ) | hosts.filter( - hostname__icontains=search - ) - # 重新排序,因为 OR 查询会丢失排序 - hosts = hosts.order_by('-created_at') - - # 状态筛选 - status_filter = request.GET.get('status', '').strip() - if status_filter: - hosts = hosts.filter(status=status_filter) - - # 连接类型筛选 - connection_type_filter = request.GET.get('connection_type', '').strip() - if connection_type_filter: - hosts = hosts.filter(connection_type=connection_type_filter) - - # 分页 - paginator = Paginator(hosts, 20) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'hosts': page_obj, - 'search': search, - 'status_filter': status_filter, - 'connection_type_filter': connection_type_filter, - 'status_choices': Host.STATUS_CHOICES, - 'connection_type_choices': Host.CONNECTION_TYPE_CHOICES, - 'active_nav': 'provider_hosts', - } - - return render(request, 'admin_base/providers/host_list.html', context) - - -@superadmin_required -def superadmin_host_provider_assign(request, pk): - """ - 超级管理员 - 分配提供商给主机 - - 允许超级管理员为主机分配或移除提供商。 - """ - host = get_object_or_404(Host, pk=pk) - - if request.method == 'POST': - form = HostProviderAssignForm(request.POST, host=host) - if form.is_valid(): - providers = form.cleaned_data['providers'] - host.providers.set(providers) - messages.success( - request, - f'已成功更新主机「{host.name}」的提供商分配,' - f'当前分配 {providers.count()} 个提供商。' - ) - return redirect('admin:admin_providers:provider_host_list') - else: - form = HostProviderAssignForm(host=host) - - context = { - 'host': host, - 'form': form, - 'current_providers': host.providers.all(), - 'active_nav': 'provider_hosts', - } - context.update(_get_permission_context(form, host)) - - return render( - request, 'admin_base/providers/host_provider_assign.html', context - ) - - -@superadmin_required -def superadmin_hostgroup_list(request): - """ - 超级管理员 - 主机组列表视图 - - 显示所有主机组及其已分配的提供商,支持搜索。 - """ - hostgroups = HostGroup.objects.select_related( - 'created_by' - ).prefetch_related('providers', 'hosts').order_by('-created_at') - - # 搜索 - search = request.GET.get('search', '').strip() - if search: - hostgroups = hostgroups.filter(name__icontains=search) - - # 分页 - paginator = Paginator(hostgroups, 20) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'hostgroups': page_obj, - 'search': search, - 'active_nav': 'provider_hosts', - } - - return render( - request, 'admin_base/providers/hostgroup_list.html', context - ) - - -@superadmin_required -def superadmin_hostgroup_provider_assign(request, pk): - """ - 超级管理员 - 分配提供商给主机组 - - 允许超级管理员为主机组分配或移除提供商。 - """ - hostgroup = get_object_or_404(HostGroup, pk=pk) - - if request.method == 'POST': - form = HostGroupProviderAssignForm(request.POST, hostgroup=hostgroup) - if form.is_valid(): - providers = form.cleaned_data['providers'] - hostgroup.providers.set(providers) - messages.success( - request, - f'已成功更新主机组「{hostgroup.name}」的提供商分配,' - f'当前分配 {providers.count()} 个提供商。' - ) - return redirect('admin:admin_providers:provider_hostgroup_list') - else: - form = HostGroupProviderAssignForm(hostgroup=hostgroup) - - context = { - 'hostgroup': hostgroup, - 'form': form, - 'current_providers': hostgroup.providers.all(), - 'active_nav': 'provider_hosts', - } - - return render( - request, - 'admin_base/providers/hostgroup_provider_assign.html', - context, - ) diff --git a/apps/audit/admin.py b/apps/audit/admin.py deleted file mode 100644 index adb01a3..0000000 --- a/apps/audit/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.audit.views_admin) diff --git a/apps/audit/apps.py b/apps/audit/apps.py deleted file mode 100755 index 64c17b9..0000000 --- a/apps/audit/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.apps import AppConfig - - -class AuditConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.audit' - verbose_name = '审计日志系统' - - def ready(self): - # 导入信号处理器 - import apps.audit.signals \ No newline at end of file diff --git a/apps/audit/decorators.py b/apps/audit/decorators.py deleted file mode 100755 index 56f6399..0000000 --- a/apps/audit/decorators.py +++ /dev/null @@ -1,281 +0,0 @@ -from functools import wraps -from .models import AuditLog, SensitiveOperation, SecurityEvent -from django.contrib.auth.models import User -from apps.hosts.models import Host -from utils.helpers import get_client_ip -import json -import logging -from django.http import JsonResponse -from django.core.exceptions import PermissionDenied - -logger = logging.getLogger(__name__) - - -def audit_log(action, host_param=None, details_extractor=None, related_object_param=None): - """ - 审计日志装饰器 - :param action: 操作类型 - :param host_param: 从参数中提取主机对象的参数名 - :param details_extractor: 从参数中提取详细信息的函数 - :param related_object_param: 从参数中提取关联对象的参数名(用于通用外键) - """ - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - response = None - success = True - error_msg = None - - try: - response = view_func(request, *args, **kwargs) - except Exception as e: - success = False - error_msg = str(e) - raise - finally: - # 记录审计日志 - try: - user = request.user if request.user.is_authenticated else None - - # 获取主机对象 - host = None - if host_param and kwargs.get(host_param): - host_id = kwargs[host_param] - if isinstance(host_id, Host): - host = host_id - elif isinstance(host_id, int): - host = Host.objects.filter(id=host_id).first() - - # 获取关联对象(用于通用外键) - content_object = None - if related_object_param and kwargs.get(related_object_param): - obj_id = kwargs[related_object_param] - obj_type = None - if isinstance(obj_id, str) and '.' in obj_id: - app_label, model = obj_id.split('.') - from django.apps import apps - obj_type = apps.get_model(app_label, model) - # 这里可以根据实际需要扩展对象类型识别逻辑 - - # 提取操作详情 - details = {} - if details_extractor: - details = details_extractor(request, *args, **kwargs) - else: - # 默认提取一些基本信息 - details = { - 'method': request.method, - 'path': request.path, - 'user_agent': request.META.get('HTTP_USER_AGENT', ''), - } - - AuditLog.objects.create( - user=user, - host=host, - action=action, - ip_address=get_client_ip(request), - success=success, - details=details, - result=error_msg, - content_object=content_object - ) - except Exception as log_error: - # 审计日志记录失败不应该影响主业务 - logger.error(f"Audit logging failed: {log_error}", exc_info=True) - - return response - return wrapper - return decorator - - -def log_sensitive_operation(operation_type, justification_required=True, response_on_missing_justification=None): - """ - 敏感操作日志装饰器 - :param operation_type: 操作类型 - :param justification_required: 是否需要提供操作理由 - :param response_on_missing_justification: 缺少理由时的响应 - """ - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - if justification_required: - justification = ( - request.POST.get('justification') or - request.GET.get('justification') or - request.META.get('HTTP_X_JUSTIFICATION') or - getattr(request, 'data', {}).get('justification') # 对于DRF - ) - if not justification: - if response_on_missing_justification: - return response_on_missing_justification - else: - raise PermissionDenied("此操作需要提供操作理由") - - response = None - error_occurred = False - try: - response = view_func(request, *args, **kwargs) - except Exception as e: - error_occurred = True - raise - finally: - try: - # 记录敏感操作 - SensitiveOperation.objects.create( - operation_type=operation_type, - user=request.user if request.user.is_authenticated else None, - target=str(args) + str(kwargs), - ip_address=get_client_ip(request), - justification=justification or "N/A", - result=str(response) if response and hasattr(response, '__str__') else "Completed" if not error_occurred else "Failed" - ) - except Exception as log_error: - logger.error(f"Sensitive operation logging failed: {log_error}", exc_info=True) - - return response - return wrapper - return decorator - - -def security_event_logger(event_type, severity='medium', auto_resolve_threshold=5): - """ - 安全事件记录装饰器 - :param event_type: 事件类型 - :param severity: 严重程度 - :param auto_resolve_threshold: 自动解决阈值(相同IP同类型事件数量) - """ - def decorator(view_func): - @wraps(view_func) - def wrapper(request, *args, **kwargs): - ip_address = get_client_ip(request) - - # 检查是否有未解决的同类事件 - recent_events = SecurityEvent.objects.filter( - event_type=event_type, - ip_address=ip_address, - resolved=False - ).order_by('-timestamp') - - # 如果超过阈值,自动标记为已解决 - if recent_events.count() >= auto_resolve_threshold: - recent_events.update(resolved=True, resolved_at=timezone.now()) - - try: - response = view_func(request, *args, **kwargs) - except Exception as e: - # 记录安全事件 - SecurityEvent.objects.create( - event_type=event_type, - severity=severity, - user=request.user if request.user.is_authenticated else None, - ip_address=ip_address, - description=str(e) if str(e) != '' else f"Security event occurred during {view_func.__name__}", - ) - raise - - # 对于某些类型的事件,即使成功也要记录 - if event_type in ['failed_login']: # 这种情况不太可能,但我们保留这个逻辑 - pass # 不记录成功的登录为安全事件 - - return response - return wrapper - return decorator - - -def log_user_session_activity(view_func): - """ - 记录用户会话活动的装饰器 - """ - @wraps(view_func) - def wrapper(request, *args, **kwargs): - from .models import SessionActivity - from django.contrib.sessions.models import Session - - session_key = request.session.session_key - user = request.user if request.user.is_authenticated else None - - if user and session_key: - # 检查是否已有对应的会话活动记录 - session_activity, created = SessionActivity.objects.get_or_create( - session_key=session_key, - user=user, - is_active=True, - defaults={ - 'ip_address': get_client_ip(request), - 'user_agent': request.META.get('HTTP_USER_AGENT', '')[:500], # 限制长度 - } - ) - - response = view_func(request, *args, **kwargs) - return response - return wrapper - - -# 辅助函数:批量记录审计日志 -def bulk_audit_log(entries): - """ - 批量记录审计日志 - :param entries: 日志条目列表,每个条目是一个字典 - """ - audit_logs = [] - for entry in entries: - audit_logs.append(AuditLog( - user=entry.get('user'), - host=entry.get('host'), - action=entry['action'], - ip_address=entry.get('ip_address'), - success=entry.get('success', True), - details=entry.get('details', {}), - result=entry.get('result') - )) - - AuditLog.objects.bulk_create(audit_logs) - - -# Django信号处理器辅助函数 -def log_model_change(sender, instance, created, **kwargs): - """ - 通用模型变更日志记录函数 - 可以作为Django信号的处理器使用 - """ - from django.contrib.contenttypes.models import ContentType - - user = getattr(instance, '_audit_user', None) # 从实例获取操作用户(需要在视图中设置) - action = 'create' if created else 'update' - ip_address = getattr(instance, '_audit_ip', None) # 从实例获取IP地址 - - AuditLog.objects.create( - user=user, - action=action, - ip_address=ip_address, - details={ - 'model': sender._meta.label, - 'pk': instance.pk, - 'fields_changed': getattr(instance, '_fields_changed', []) - }, - content_type=ContentType.objects.get_for_model(sender), - object_id=instance.pk - ) - - -def log_model_deletion(sender, instance, **kwargs): - """ - 通用模型删除日志记录函数 - 可以作为Django信号的处理器使用 - """ - from django.contrib.contenttypes.models import ContentType - - user = getattr(instance, '_audit_user', None) # 从实例获取操作用户 - ip_address = getattr(instance, '_audit_ip', None) # 从实例获取IP地址 - - AuditLog.objects.create( - user=user, - action='delete', - ip_address=ip_address, - details={ - 'model': sender._meta.label, - 'pk': instance.pk, - }, - content_type=ContentType.objects.get_for_model(sender), - object_id=instance.pk - ) \ No newline at end of file diff --git a/apps/audit/forms_admin.py b/apps/audit/forms_admin.py deleted file mode 100644 index ef90566..0000000 --- a/apps/audit/forms_admin.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -审计日志超级管理员表单 - -审计日志为只读模型,无需创建/编辑表单。 -此文件保留用于未来可能的筛选表单扩展。 -""" - -# AuditLog 为只读模型,不需要表单 diff --git a/apps/audit/migrations/0001_initial.py b/apps/audit/migrations/0001_initial.py deleted file mode 100755 index 6885d1a..0000000 --- a/apps/audit/migrations/0001_initial.py +++ /dev/null @@ -1,104 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 11:58 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('hosts', '0005_host_connection_type_alter_host_port'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='SessionActivity', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('session_key', models.CharField(max_length=40, verbose_name='会话密钥')), - ('ip_address', models.GenericIPAddressField(verbose_name='IP地址')), - ('user_agent', models.TextField(verbose_name='用户代理')), - ('login_time', models.DateTimeField(auto_now_add=True, verbose_name='登录时间')), - ('logout_time', models.DateTimeField(blank=True, null=True, verbose_name='登出时间')), - ('is_active', models.BooleanField(default=True, verbose_name='是否活跃')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '会话活动', - 'verbose_name_plural': '会话活动', - 'db_table': 'session_activity', - 'ordering': ['-login_time'], - }, - ), - migrations.CreateModel( - name='SensitiveOperation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('operation_type', models.CharField(max_length=50, verbose_name='操作类型')), - ('target', models.CharField(max_length=255, verbose_name='操作目标')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='操作时间')), - ('ip_address', models.GenericIPAddressField(verbose_name='操作IP')), - ('justification', models.TextField(verbose_name='操作理由')), - ('approved_at', models.DateTimeField(null=True, verbose_name='批准时间')), - ('result', models.TextField(blank=True, null=True, verbose_name='操作结果')), - ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_sensitive_ops', to=settings.AUTH_USER_MODEL, verbose_name='批准人')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='操作用户')), - ], - options={ - 'verbose_name': '敏感操作', - 'verbose_name_plural': '敏感操作', - 'db_table': 'sensitive_operation', - 'ordering': ['-timestamp'], - }, - ), - migrations.CreateModel( - name='SecurityEvent', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('event_type', models.CharField(choices=[('unauthorized_access', '未授权访问'), ('failed_login', '登录失败'), ('suspicious_activity', '可疑活动'), ('data_exposure', '数据暴露风险'), ('privilege_escalation', '权限提升尝试'), ('cert_compromise', '证书泄露'), ('brute_force', '暴力破解')], max_length=50, verbose_name='事件类型')), - ('severity', models.CharField(choices=[('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '严重')], default='medium', max_length=10, verbose_name='严重程度')), - ('ip_address', models.GenericIPAddressField(verbose_name='事件IP')), - ('description', models.TextField(verbose_name='事件描述')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='发生时间')), - ('resolved', models.BooleanField(default=False, verbose_name='已解决')), - ('resolved_at', models.DateTimeField(null=True, verbose_name='解决时间')), - ('resolution_notes', models.TextField(blank=True, null=True, verbose_name='解决备注')), - ('resolved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_security_events', to=settings.AUTH_USER_MODEL, verbose_name='解决人')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='关联用户')), - ], - options={ - 'verbose_name': '安全事件', - 'verbose_name_plural': '安全事件', - 'db_table': 'security_event', - 'ordering': ['-timestamp'], - }, - ), - migrations.CreateModel( - name='AuditLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('action', models.CharField(choices=[('create_user', '创建用户'), ('delete_user', '删除用户'), ('reset_password', '重置密码'), ('connect_host', '连接主机'), ('modify_host', '修改主机'), ('view_password', '查看密码'), ('approve_request', '审批请求'), ('reject_request', '拒绝请求'), ('bootstrap_host', '初始化主机'), ('issue_cert', '签发证书'), ('revoke_cert', '吊销证书'), ('create_host', '创建主机'), ('delete_host', '删除主机'), ('update_host', '更新主机'), ('process_opening_request', '处理开户请求'), ('batch_process_requests', '批量处理请求'), ('login', '用户登录'), ('logout', '用户登出'), ('view_audit_log', '查看审计日志'), ('admin_action', '管理员操作')], max_length=50, verbose_name='操作类型')), - ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='操作IP地址')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='操作时间')), - ('success', models.BooleanField(default=True, verbose_name='操作成功')), - ('details', models.JSONField(default=dict, verbose_name='操作详情')), - ('result', models.TextField(blank=True, null=True, verbose_name='操作结果')), - ('object_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='关联对象ID')), - ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.contenttype', verbose_name='关联对象类型')), - ('host', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosts.host', verbose_name='操作主机')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='操作用户')), - ], - options={ - 'verbose_name': '审计日志', - 'verbose_name_plural': '审计日志', - 'db_table': 'audit_log', - 'ordering': ['-timestamp'], - 'indexes': [models.Index(fields=['user', 'timestamp'], name='audit_log_user_id_835db7_idx'), models.Index(fields=['host', 'timestamp'], name='audit_log_host_id_51b348_idx'), models.Index(fields=['action', 'timestamp'], name='audit_log_action_09d227_idx'), models.Index(fields=['timestamp'], name='audit_log_timesta_e8e14e_idx')], - }, - ), - ] diff --git a/apps/audit/migrations/0002_auditlog_tunnel_actions.py b/apps/audit/migrations/0002_auditlog_tunnel_actions.py deleted file mode 100644 index cfa7888..0000000 --- a/apps/audit/migrations/0002_auditlog_tunnel_actions.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('audit', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='auditlog', - name='action', - field=models.CharField( - choices=[ - ('create_user', '创建用户'), - ('delete_user', '删除用户'), - ('reset_password', '重置密码'), - ('connect_host', '连接主机'), - ('modify_host', '修改主机'), - ('view_password', '查看密码'), - ('approve_request', '审批请求'), - ('reject_request', '拒绝请求'), - ('bootstrap_host', '初始化主机'), - ('issue_cert', '签发证书'), - ('revoke_cert', '吊销证书'), - ('create_host', '创建主机'), - ('delete_host', '删除主机'), - ('update_host', '更新主机'), - ('process_opening_request', '处理开户请求'), - ('batch_process_requests', '批量处理请求'), - ('login', '用户登录'), - ('logout', '用户登出'), - ('view_audit_log', '查看审计日志'), - ('admin_action', '管理员操作'), - ('tunnel_online', '隧道上线'), - ('tunnel_offline', '隧道离线'), - ('tunnel_heartbeat_timeout', '隧道心跳超时'), - ('rdp_connect', 'RDP连接'), - ('rdp_disconnect', 'RDP断开'), - ('remote_exec', '远程执行命令'), - ('remote_exec_result', '远程执行结果'), - ('domain_bind', '域名绑定'), - ('domain_unbind', '域名解绑'), - ], - max_length=50, verbose_name='操作类型' - ), - ), - ] diff --git a/apps/audit/migrations/0003_alter_auditlog_action.py b/apps/audit/migrations/0003_alter_auditlog_action.py deleted file mode 100644 index d09dc53..0000000 --- a/apps/audit/migrations/0003_alter_auditlog_action.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-26 15:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('audit', '0002_auditlog_tunnel_actions'), - ] - - operations = [ - migrations.AlterField( - model_name='auditlog', - name='action', - field=models.CharField(choices=[('create_user', '创建用户'), ('delete_user', '删除用户'), ('reset_password', '重置密码'), ('connect_host', '连接主机'), ('modify_host', '修改主机'), ('view_password', '查看密码'), ('approve_request', '审批请求'), ('reject_request', '拒绝请求'), ('bootstrap_host', '初始化主机'), ('issue_cert', '签发证书'), ('revoke_cert', '吊销证书'), ('create_host', '创建主机'), ('delete_host', '删除主机'), ('update_host', '更新主机'), ('process_opening_request', '处理开户请求'), ('batch_process_requests', '批量处理请求'), ('login', '用户登录'), ('logout', '用户登出'), ('view_audit_log', '查看审计日志'), ('admin_action', '管理员操作'), ('tunnel_online', '隧道上线'), ('tunnel_offline', '隧道离线'), ('tunnel_heartbeat_timeout', '隧道心跳超时'), ('rdp_connect', 'RDP连接'), ('rdp_disconnect', 'RDP断开'), ('remote_exec', '远程执行命令'), ('remote_exec_result', '远程执行结果'), ('domain_bind', '域名绑定'), ('domain_unbind', '域名解绑'), ('create_ticket', '创建工单'), ('update_ticket', '更新工单'), ('assign_ticket', '分配工单'), ('change_ticket_status', '变更工单状态'), ('close_ticket', '关闭工单'), ('add_ticket_comment', '添加工单评论')], max_length=50, verbose_name='操作类型'), - ), - ] diff --git a/apps/audit/migrations/0004_auditlog_user_agent_alter_auditlog_action.py b/apps/audit/migrations/0004_auditlog_user_agent_alter_auditlog_action.py deleted file mode 100644 index e88ee86..0000000 --- a/apps/audit/migrations/0004_auditlog_user_agent_alter_auditlog_action.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-01 14:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('audit', '0003_alter_auditlog_action'), - ] - - operations = [ - migrations.AddField( - model_name='auditlog', - name='user_agent', - field=models.TextField(blank=True, verbose_name='用户代理'), - ), - migrations.AlterField( - model_name='auditlog', - name='action', - field=models.CharField(choices=[('create_user', '创建用户'), ('delete_user', '删除用户'), ('reset_password', '重置密码'), ('connect_host', '连接主机'), ('modify_host', '修改主机'), ('view_password', '查看密码'), ('approve_request', '审批请求'), ('reject_request', '拒绝请求'), ('bootstrap_host', '初始化主机'), ('issue_cert', '签发证书'), ('revoke_cert', '吊销证书'), ('create_host', '创建主机'), ('delete_host', '删除主机'), ('update_host', '更新主机'), ('process_opening_request', '处理开户请求'), ('batch_process_requests', '批量处理请求'), ('login', '用户登录'), ('logout', '用户登出'), ('view_audit_log', '查看审计日志'), ('admin_action', '管理员操作'), ('tunnel_online', '隧道上线'), ('tunnel_offline', '隧道离线'), ('tunnel_heartbeat_timeout', '隧道心跳超时'), ('rdp_connect', 'RDP连接'), ('rdp_disconnect', 'RDP断开'), ('remote_exec', '远程执行命令'), ('remote_exec_result', '远程执行结果'), ('domain_bind', '域名绑定'), ('domain_unbind', '域名解绑'), ('create_ticket', '创建工单'), ('update_ticket', '更新工单'), ('assign_ticket', '分配工单'), ('change_ticket_status', '变更工单状态'), ('close_ticket', '关闭工单'), ('add_ticket_comment', '添加工单评论'), ('dashboard_view', '访问仪表盘'), ('system_config_update', '更新系统配置')], max_length=50, verbose_name='操作类型'), - ), - ] diff --git a/apps/audit/migrations/__init__.py b/apps/audit/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/audit/models.py b/apps/audit/models.py deleted file mode 100755 index 9ddc99f..0000000 --- a/apps/audit/models.py +++ /dev/null @@ -1,222 +0,0 @@ -from django.db import models -from apps.hosts.models import Host -from apps.operations.models import AccountOpeningRequest, CloudComputerUser -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericForeignKey - - -class AuditLog(models.Model): - """审计日志模型""" - ACTION_CHOICES = [ - ('create_user', '创建用户'), - ('delete_user', '删除用户'), - ('reset_password', '重置密码'), - ('connect_host', '连接主机'), - ('modify_host', '修改主机'), - ('view_password', '查看密码'), - ('approve_request', '审批请求'), - ('reject_request', '拒绝请求'), - ('bootstrap_host', '初始化主机'), - ('issue_cert', '签发证书'), - ('revoke_cert', '吊销证书'), - ('create_host', '创建主机'), - ('delete_host', '删除主机'), - ('update_host', '更新主机'), - ('process_opening_request', '处理开户请求'), - ('batch_process_requests', '批量处理请求'), - ('login', '用户登录'), - ('logout', '用户登出'), - ('view_audit_log', '查看审计日志'), - ('admin_action', '管理员操作'), - ('tunnel_online', '隧道上线'), - ('tunnel_offline', '隧道离线'), - ('tunnel_heartbeat_timeout', '隧道心跳超时'), - ('rdp_connect', 'RDP连接'), - ('rdp_disconnect', 'RDP断开'), - ('remote_exec', '远程执行命令'), - ('remote_exec_result', '远程执行结果'), - ('domain_bind', '域名绑定'), - ('domain_unbind', '域名解绑'), - ('create_ticket', '创建工单'), - ('update_ticket', '更新工单'), - ('assign_ticket', '分配工单'), - ('change_ticket_status', '变更工单状态'), - ('close_ticket', '关闭工单'), - ('add_ticket_comment', '添加工单评论'), - ('dashboard_view', '访问仪表盘'), - ('system_config_update', '更新系统配置'), - ] - - user = models.ForeignKey( - 'accounts.User', - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name="操作用户" - ) - host = models.ForeignKey( - Host, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name="操作主机" - ) - action = models.CharField( - max_length=50, - choices=ACTION_CHOICES, - verbose_name="操作类型" - ) - ip_address = models.GenericIPAddressField( - null=True, - blank=True, - verbose_name="操作IP地址" - ) - user_agent = models.TextField( - blank=True, - verbose_name="用户代理" - ) - timestamp = models.DateTimeField(auto_now_add=True, verbose_name="操作时间") - success = models.BooleanField(default=True, verbose_name="操作成功") - details = models.JSONField(default=dict, verbose_name="操作详情") # 存储具体操作详情 - result = models.TextField(null=True, blank=True, verbose_name="操作结果") - - # 通用外键,用于关联各种模型 - content_type = models.ForeignKey( - ContentType, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name="关联对象类型" - ) - object_id = models.PositiveIntegerField( - null=True, - blank=True, - verbose_name="关联对象ID" - ) - content_object = GenericForeignKey('content_type', 'object_id') - - class Meta: - verbose_name = "审计日志" - verbose_name_plural = "审计日志" - db_table = "audit_log" - ordering = ['-timestamp'] - indexes = [ - models.Index(fields=['user', 'timestamp']), - models.Index(fields=['host', 'timestamp']), - models.Index(fields=['action', 'timestamp']), - models.Index(fields=['timestamp']), - ] - - def __str__(self): - user_str = self.user.username if self.user else "Anonymous" - host_str = f" on {self.host.hostname}" if self.host else "" - return f"[{self.timestamp}] {user_str}{host_str} - {self.action}" - - -class SensitiveOperation(models.Model): - """敏感操作记录""" - operation_type = models.CharField(max_length=50, verbose_name="操作类型") - user = models.ForeignKey('accounts.User', on_delete=models.CASCADE, verbose_name="操作用户") - target = models.CharField(max_length=255, verbose_name="操作目标") # 目标对象描述 - timestamp = models.DateTimeField(auto_now_add=True, verbose_name="操作时间") - ip_address = models.GenericIPAddressField(verbose_name="操作IP") - justification = models.TextField(verbose_name="操作理由") # 必须提供操作理由 - approved_by = models.ForeignKey( - 'accounts.User', - on_delete=models.SET_NULL, - null=True, - related_name='approved_sensitive_ops', - verbose_name="批准人" - ) - approved_at = models.DateTimeField(null=True, verbose_name="批准时间") - result = models.TextField(null=True, blank=True, verbose_name="操作结果") - - class Meta: - verbose_name = "敏感操作" - verbose_name_plural = "敏感操作" - db_table = "sensitive_operation" - ordering = ['-timestamp'] - - def __str__(self): - return f"[{self.timestamp}] {self.user.username} - {self.operation_type} on {self.target}" - - -class SecurityEvent(models.Model): - """安全事件模型""" - SEVERITY_CHOICES = [ - ('low', '低'), - ('medium', '中'), - ('high', '高'), - ('critical', '严重'), - ] - - EVENT_TYPE_CHOICES = [ - ('unauthorized_access', '未授权访问'), - ('failed_login', '登录失败'), - ('suspicious_activity', '可疑活动'), - ('data_exposure', '数据暴露风险'), - ('privilege_escalation', '权限提升尝试'), - ('cert_compromise', '证书泄露'), - ('brute_force', '暴力破解'), - ] - - event_type = models.CharField( - max_length=50, - choices=EVENT_TYPE_CHOICES, - verbose_name="事件类型" - ) - severity = models.CharField( - max_length=10, - choices=SEVERITY_CHOICES, - default='medium', - verbose_name="严重程度" - ) - user = models.ForeignKey( - 'accounts.User', - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name="关联用户" - ) - ip_address = models.GenericIPAddressField(verbose_name="事件IP") - description = models.TextField(verbose_name="事件描述") - timestamp = models.DateTimeField(auto_now_add=True, verbose_name="发生时间") - resolved = models.BooleanField(default=False, verbose_name="已解决") - resolved_by = models.ForeignKey( - 'accounts.User', - on_delete=models.SET_NULL, - null=True, - related_name='resolved_security_events', - verbose_name="解决人" - ) - resolved_at = models.DateTimeField(null=True, verbose_name="解决时间") - resolution_notes = models.TextField(null=True, blank=True, verbose_name="解决备注") - - class Meta: - verbose_name = "安全事件" - verbose_name_plural = "安全事件" - db_table = "security_event" - ordering = ['-timestamp'] - - def __str__(self): - return f"[{self.severity.upper()}] {self.event_type} - {self.timestamp}" - - -class SessionActivity(models.Model): - """会话活动记录""" - user = models.ForeignKey('accounts.User', on_delete=models.CASCADE, verbose_name="用户") - session_key = models.CharField(max_length=40, verbose_name="会话密钥") - ip_address = models.GenericIPAddressField(verbose_name="IP地址") - user_agent = models.TextField(verbose_name="用户代理") - login_time = models.DateTimeField(auto_now_add=True, verbose_name="登录时间") - logout_time = models.DateTimeField(null=True, blank=True, verbose_name="登出时间") - is_active = models.BooleanField(default=True, verbose_name="是否活跃") - - class Meta: - verbose_name = "会话活动" - verbose_name_plural = "会话活动" - db_table = "session_activity" - ordering = ['-login_time'] - - def __str__(self): - return f"{self.user.username} - {self.session_key[:8]} - {self.login_time}" \ No newline at end of file diff --git a/apps/audit/signals.py b/apps/audit/signals.py deleted file mode 100755 index 47bce8c..0000000 --- a/apps/audit/signals.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -审计日志应用的信号处理器 -""" -from django.db.models.signals import post_save, pre_delete -from django.dispatch import receiver -from .models import AuditLog, SensitiveOperation, SecurityEvent, SessionActivity - - -# 可以在这里添加具体的信号处理器 -# 例如:在创建审计日志时触发某些操作 \ No newline at end of file diff --git a/apps/audit/tests/__init__.py b/apps/audit/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/audit/urls.py b/apps/audit/urls.py deleted file mode 100755 index cf73de1..0000000 --- a/apps/audit/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'audit' - -urlpatterns = [ - # 审计日志API - path('logs/', views.get_audit_logs, name='get_audit_logs'), - path('sensitive-ops/', views.get_sensitive_operations, name='get_sensitive_operations'), - path('security-events/', views.get_security_events, name='get_security_events'), - path('mark-event-resolved/', views.mark_security_event_resolved, name='mark_security_event_resolved'), - path('session-activity/', views.get_user_session_activity, name='get_user_session_activity'), - path('stats/', views.AuditManagementView.as_view(), name='audit_stats'), - path('export/', views.export_audit_logs, name='export_audit_logs'), -] \ No newline at end of file diff --git a/apps/audit/urls_admin.py b/apps/audit/urls_admin.py deleted file mode 100644 index e40167a..0000000 --- a/apps/audit/urls_admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path - -from .views_admin import auditlog_list, auditlog_detail - -app_name = 'admin_audit' - -urlpatterns = [ - path('', auditlog_list, name='auditlog_list'), - path('/', auditlog_detail, name='auditlog_detail'), -] diff --git a/apps/audit/views.py b/apps/audit/views.py deleted file mode 100755 index 710e78b..0000000 --- a/apps/audit/views.py +++ /dev/null @@ -1,568 +0,0 @@ -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.decorators import method_decorator -from django.views import View -from .models import AuditLog, SensitiveOperation, SecurityEvent, SessionActivity -from apps.hosts.models import Host -from django.contrib.auth.models import User -from django.shortcuts import get_object_or_404 -from django.core.paginator import Paginator -from django.utils import timezone -from datetime import datetime, timedelta -import json -import logging -import re - -logger = logging.getLogger(__name__) - -MAX_PAGE_SIZE = 100 -MAX_SEARCH_LENGTH = 200 -DATE_FORMAT = '%Y-%m-%d' - - -def _validate_int_param(value, default=1, min_val=1, max_val=None): - try: - result = int(value) - result = max(min_val, result) - if max_val: - result = min(max_val, result) - return result - except (ValueError, TypeError): - return default - - -def _validate_date_param(value): - if not value: - return None - try: - datetime.strptime(value, DATE_FORMAT) - return value - except ValueError: - return None - - -@require_http_methods(["GET"]) -@login_required -@permission_required('audit.view_auditlog', raise_exception=True) -def get_audit_logs(request): - """获取审计日志列表""" - try: - page = _validate_int_param(request.GET.get('page', 1), default=1, min_val=1, max_val=10000) - page_size = _validate_int_param(request.GET.get('page_size', 20), default=20, min_val=1, max_val=MAX_PAGE_SIZE) - action = request.GET.get('action', '')[:50] if request.GET.get('action') else None - user_id = _validate_int_param(request.GET.get('user_id'), default=None, min_val=1) if request.GET.get('user_id') else None - host_id = _validate_int_param(request.GET.get('host_id'), default=None, min_val=1) if request.GET.get('host_id') else None - start_date = _validate_date_param(request.GET.get('start_date')) - end_date = _validate_date_param(request.GET.get('end_date')) - success = request.GET.get('success') - search = request.GET.get('search', '')[:MAX_SEARCH_LENGTH] - - # 构建查询集 - queryset = AuditLog.objects.select_related('user', 'host').all() - - # 应用过滤器 - if action: - queryset = queryset.filter(action=action) - if user_id: - queryset = queryset.filter(user_id=user_id) - if host_id: - queryset = queryset.filter(host_id=host_id) - if success is not None: - queryset = queryset.filter(success=(success.lower() == 'true')) - if start_date: - queryset = queryset.filter(timestamp__gte=start_date) - if end_date: - queryset = queryset.filter(timestamp__lte=end_date) - if search: - # 搜索用户、主机或操作详情 - from django.db.models import Q - queryset = queryset.filter( - Q(user__username__icontains=search) | - Q(host__hostname__icontains=search) | - Q(details__icontains=search) | - Q(result__icontains=search) - ) - - # 按时间倒序排列 - queryset = queryset.order_by('-timestamp') - - # 分页 - paginator = Paginator(queryset, page_size) - logs_page = paginator.get_page(page) - - # 构造响应数据 - result = { - 'success': True, - 'data': { - 'logs': [ - { - 'id': log.id, - 'user': log.user.username if log.user else 'Anonymous', - 'user_id': log.user.id if log.user else None, - 'host': log.host.hostname if log.host else None, - 'host_id': log.host.id if log.host else None, - 'action': log.action, - 'ip_address': log.ip_address, - 'timestamp': log.timestamp.isoformat(), - 'success': log.success, - 'details': log.details, - 'result': log.result - } - for log in logs_page - ], - 'pagination': { - 'page': page, - 'page_size': page_size, - 'total_count': paginator.count, - 'total_pages': paginator.num_pages, - 'has_next': logs_page.has_next(), - 'has_previous': logs_page.has_previous() - } - } - } - - return JsonResponse(result) - - except ValueError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid parameter value' - }, status=400) - except Exception as e: - logger.error(f"Error getting audit logs: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve audit logs' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -@permission_required('audit.view_sensitiveoperation', raise_exception=True) -def get_sensitive_operations(request): - """获取敏感操作记录""" - try: - page = _validate_int_param(request.GET.get('page', 1), default=1, min_val=1, max_val=10000) - page_size = _validate_int_param(request.GET.get('page_size', 20), default=20, min_val=1, max_val=MAX_PAGE_SIZE) - user_id = _validate_int_param(request.GET.get('user_id'), default=None, min_val=1) if request.GET.get('user_id') else None - operation_type = request.GET.get('operation_type', '')[:50] if request.GET.get('operation_type') else None - start_date = _validate_date_param(request.GET.get('start_date')) - end_date = _validate_date_param(request.GET.get('end_date')) - - queryset = SensitiveOperation.objects.select_related('user', 'approved_by').all() - - if user_id: - queryset = queryset.filter(user_id=user_id) - if operation_type: - queryset = queryset.filter(operation_type=operation_type) - if start_date: - queryset = queryset.filter(timestamp__gte=start_date) - if end_date: - queryset = queryset.filter(timestamp__lte=end_date) - - queryset = queryset.order_by('-timestamp') - - # 分页 - paginator = Paginator(queryset, page_size) - ops_page = paginator.get_page(page) - - result = { - 'success': True, - 'data': { - 'operations': [ - { - 'id': op.id, - 'operation_type': op.operation_type, - 'user': op.user.username, - 'user_id': op.user.id, - 'target': op.target, - 'timestamp': op.timestamp.isoformat(), - 'ip_address': op.ip_address, - 'justification': op.justification, - 'approved_by': op.approved_by.username if op.approved_by else None, - 'approved_at': op.approved_at.isoformat() if op.approved_at else None, - 'result': op.result - } - for op in ops_page - ], - 'pagination': { - 'page': page, - 'page_size': page_size, - 'total_count': paginator.count, - 'total_pages': paginator.num_pages, - 'has_next': ops_page.has_next(), - 'has_previous': ops_page.has_previous() - } - } - } - - return JsonResponse(result) - - except ValueError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid parameter value' - }, status=400) - except Exception as e: - logger.error(f"Error getting sensitive operations: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve sensitive operations' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -@permission_required('audit.view_securityevent', raise_exception=True) -def get_security_events(request): - """获取安全事件记录""" - try: - page = _validate_int_param(request.GET.get('page', 1), default=1, min_val=1, max_val=10000) - page_size = _validate_int_param(request.GET.get('page_size', 20), default=20, min_val=1, max_val=MAX_PAGE_SIZE) - event_type = request.GET.get('event_type', '')[:50] if request.GET.get('event_type') else None - severity = request.GET.get('severity', '')[:20] if request.GET.get('severity') else None - resolved = request.GET.get('resolved') - user_id = _validate_int_param(request.GET.get('user_id'), default=None, min_val=1) if request.GET.get('user_id') else None - start_date = _validate_date_param(request.GET.get('start_date')) - end_date = _validate_date_param(request.GET.get('end_date')) - - queryset = SecurityEvent.objects.select_related('user', 'resolved_by').all() - - # 应用过滤器 - if event_type: - queryset = queryset.filter(event_type=event_type) - if severity: - queryset = queryset.filter(severity=severity) - if resolved is not None: - queryset = queryset.filter(resolved=(resolved.lower() == 'true')) - if user_id: - queryset = queryset.filter(user_id=user_id) - if start_date: - queryset = queryset.filter(timestamp__gte=start_date) - if end_date: - queryset = queryset.filter(timestamp__lte=end_date) - - queryset = queryset.order_by('-timestamp') - - # 分页 - paginator = Paginator(queryset, page_size) - events_page = paginator.get_page(page) - - result = { - 'success': True, - 'data': { - 'events': [ - { - 'id': event.id, - 'event_type': event.event_type, - 'severity': event.severity, - 'user': event.user.username if event.user else None, - 'user_id': event.user.id if event.user else None, - 'ip_address': event.ip_address, - 'description': event.description, - 'timestamp': event.timestamp.isoformat(), - 'resolved': event.resolved, - 'resolved_by': event.resolved_by.username if event.resolved_by else None, - 'resolved_at': event.resolved_at.isoformat() if event.resolved_at else None, - 'resolution_notes': event.resolution_notes - } - for event in events_page - ], - 'pagination': { - 'page': page, - 'page_size': page_size, - 'total_count': paginator.count, - 'total_pages': paginator.num_pages, - 'has_next': events_page.has_next(), - 'has_previous': events_page.has_previous() - } - } - } - - return JsonResponse(result) - - except ValueError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid parameter value' - }, status=400) - except Exception as e: - logger.error(f"Error getting security events: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve security events' - }, status=500) - - -@login_required -@permission_required('audit.change_securityevent', raise_exception=True) -@require_http_methods(["POST"]) -def mark_security_event_resolved(request): - """标记安全事件为已解决""" - try: - data = json.loads(request.body.decode('utf-8')) - event_id = data.get('event_id') - resolution_notes = data.get('resolution_notes', '') - - if not event_id: - return JsonResponse({ - 'success': False, - 'error': 'Event ID is required' - }, status=400) - - event = get_object_or_404(SecurityEvent, id=event_id) - - event.resolved = True - event.resolved_by = request.user if request.user.is_authenticated else None - event.resolved_at = timezone.now() - event.resolution_notes = resolution_notes[:1000] if resolution_notes else '' - event.save(update_fields=['resolved', 'resolved_by', 'resolved_at', 'resolution_notes']) - - return JsonResponse({ - 'success': True, - 'message': 'Security event marked as resolved' - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error marking security event as resolved: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to resolve security event' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -@permission_required('audit.view_sessionactivity', raise_exception=True) -def get_user_session_activity(request): - """获取用户会话活动记录""" - try: - user_id = _validate_int_param(request.GET.get('user_id'), default=None, min_val=1) if request.GET.get('user_id') else None - page = _validate_int_param(request.GET.get('page', 1), default=1, min_val=1, max_val=10000) - page_size = _validate_int_param(request.GET.get('page_size', 20), default=20, min_val=1, max_val=MAX_PAGE_SIZE) - - queryset = SessionActivity.objects.select_related('user').all() - - if user_id: - queryset = queryset.filter(user_id=user_id) - - queryset = queryset.order_by('-login_time') - - # 分页 - paginator = Paginator(queryset, page_size) - sessions_page = paginator.get_page(page) - - result = { - 'success': True, - 'data': { - 'sessions': [ - { - 'id': session.id, - 'user': session.user.username, - 'user_id': session.user.id, - 'session_key': session.session_key[:8] + '...', - 'ip_address': session.ip_address, - 'user_agent': session.user_agent[:100], # 限制长度 - 'login_time': session.login_time.isoformat(), - 'logout_time': session.logout_time.isoformat() if session.logout_time else None, - 'is_active': session.is_active - } - for session in sessions_page - ], - 'pagination': { - 'page': page, - 'page_size': page_size, - 'total_count': paginator.count, - 'total_pages': paginator.num_pages, - 'has_next': sessions_page.has_next(), - 'has_previous': sessions_page.has_previous() - } - } - } - - return JsonResponse(result) - - except ValueError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid parameter value' - }, status=400) - except Exception as e: - logger.error(f"Error getting user session activity: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve session activity' - }, status=500) - - -class AuditManagementView(View): - """审计管理视图 - 需要审计权限""" - - @method_decorator(permission_required('audit.view_auditlog')) - def get(self, request): - """获取审计统计信息""" - try: - # 获取最近24小时的数据 - last_24h = timezone.now() - timedelta(hours=24) - - # 统计数据 - stats = { - 'total_logs': AuditLog.objects.count(), - 'recent_logs': AuditLog.objects.filter(timestamp__gte=last_24h).count(), - 'total_sensitive_ops': SensitiveOperation.objects.count(), - 'recent_sensitive_ops': SensitiveOperation.objects.filter(timestamp__gte=last_24h).count(), - 'total_security_events': SecurityEvent.objects.count(), - 'unresolved_security_events': SecurityEvent.objects.filter(resolved=False).count(), - 'recent_security_events': SecurityEvent.objects.filter(timestamp__gte=last_24h).count(), - } - - # 按操作类型统计 - from django.db.models import Count - action_stats = AuditLog.objects.values('action').annotate(count=Count('id')).order_by('-count')[:10] - stats['top_actions'] = list(action_stats) - - # 按用户统计 - user_stats = AuditLog.objects.values('user__username').annotate(count=Count('id')).exclude(user__isnull=True).order_by('-count')[:10] - stats['top_users'] = list(user_stats) - - # 按主机统计 - host_stats = AuditLog.objects.values('host__hostname').annotate(count=Count('id')).exclude(host__isnull=True).order_by('-count')[:10] - stats['top_hosts'] = list(host_stats) - - return JsonResponse({ - 'success': True, - 'data': stats - }) - - except Exception as e: - logger.error(f"Error getting audit statistics: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve audit statistics' - }, status=500) - - @method_decorator(permission_required('audit.delete_auditlog')) - def delete(self, request): - """清理审计日志""" - try: - data = json.loads(request.body.decode('utf-8')) - days_to_keep = data.get('days_to_keep', 90) # 默认保留90天 - - cutoff_date = timezone.now() - timedelta(days=days_to_keep) - - # 删除旧的审计日志 - deleted_count, _ = AuditLog.objects.filter(timestamp__lt=cutoff_date).delete() - - # 删除旧的敏感操作记录 - deleted_sensitive, _ = SensitiveOperation.objects.filter(timestamp__lt=cutoff_date).delete() - - # 删除旧的安全事件记录(已解决的) - deleted_events, _ = SecurityEvent.objects.filter( - timestamp__lt=cutoff_date, - resolved=True - ).delete() - - return JsonResponse({ - 'success': True, - 'data': { - 'deleted_logs': deleted_count, - 'deleted_sensitive_ops': deleted_sensitive, - 'deleted_security_events': deleted_events, - 'days_kept': days_to_keep - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error cleaning audit logs: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to clean audit logs' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -@permission_required('audit.view_auditlog', raise_exception=True) -def export_audit_logs(request): - """导出审计日志(CSV格式)""" - try: - # 获取查询参数 - action = request.GET.get('action', '')[:50] if request.GET.get('action') else None - user_id = _validate_int_param(request.GET.get('user_id'), default=None, min_val=1) if request.GET.get('user_id') else None - host_id = _validate_int_param(request.GET.get('host_id'), default=None, min_val=1) if request.GET.get('host_id') else None - start_date = _validate_date_param(request.GET.get('start_date')) - end_date = _validate_date_param(request.GET.get('end_date')) - - # 构建查询集 - queryset = AuditLog.objects.select_related('user', 'host').all() - - # 应用过滤器 - if action: - queryset = queryset.filter(action=action) - if user_id: - queryset = queryset.filter(user_id=user_id) - if host_id: - queryset = queryset.filter(host_id=host_id) - if start_date: - queryset = queryset.filter(timestamp__gte=start_date) - if end_date: - queryset = queryset.filter(timestamp__lte=end_date) - - # 按时间倒序排列 - queryset = queryset.order_by('-timestamp') - - # 生成CSV内容 - import csv - import io - - output = io.StringIO() - writer = csv.writer(output) - - # 写入标题行 - writer.writerow([ - 'ID', 'User', 'Host', 'Action', 'IP Address', 'Timestamp', - 'Success', 'Details', 'Result' - ]) - - # 写入数据行 - for log in queryset: - writer.writerow([ - log.id, - log.user.username if log.user else 'Anonymous', - log.host.hostname if log.host else '', - log.action, - log.ip_address, - log.timestamp.strftime('%Y-%m-%d %H:%M:%S'), - log.success, - json.dumps(log.details, ensure_ascii=False) if log.details else '', - log.result or '' - ]) - - # 获取CSV内容 - csv_content = output.getvalue() - output.close() - - # 返回CSV文件 - from django.http import HttpResponse - response = HttpResponse(csv_content, content_type='text/csv') - response['Content-Disposition'] = f'attachment; filename=audit_logs_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv' - - return response - - except Exception as e: - logger.error(f"Error exporting audit logs: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to export audit logs' - }, status=500) \ No newline at end of file diff --git a/apps/audit/views_admin.py b/apps/audit/views_admin.py deleted file mode 100644 index 25be78e..0000000 --- a/apps/audit/views_admin.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -审计日志超级管理员视图 - -审计日志为只读,不支持创建/编辑/删除操作。 -""" - -from django.shortcuts import render, get_object_or_404 -from django.core.paginator import Paginator - -from apps.accounts.provider_decorators import superadmin_required -from .models import AuditLog - - -@superadmin_required -def auditlog_list(request): - """ - 审计日志列表视图(只读) - - 支持按用户、操作类型、时间范围筛选,支持搜索。 - """ - queryset = AuditLog.objects.select_related( - 'user', 'host' - ).order_by('-timestamp') - - # 搜索 - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - action__icontains=search - ) | queryset.filter( - user__username__icontains=search - ) | queryset.filter( - host__name__icontains=search - ) | queryset.filter( - ip_address__icontains=search - ) - - # 操作类型筛选 - action_filter = request.GET.get('action', '').strip() - if action_filter: - queryset = queryset.filter(action=action_filter) - - # 用户筛选 - user_filter = request.GET.get('user', '').strip() - if user_filter: - queryset = queryset.filter(user__username__icontains=user_filter) - - # 时间范围筛选 - timestamp_from = request.GET.get('timestamp_from', '').strip() - if timestamp_from: - queryset = queryset.filter(timestamp__gte=timestamp_from) - - timestamp_to = request.GET.get('timestamp_to', '').strip() - if timestamp_to: - queryset = queryset.filter(timestamp__lte=timestamp_to) - - # 分页 - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'search': search, - 'action_filter': action_filter, - 'user_filter': user_filter, - 'timestamp_from': timestamp_from, - 'timestamp_to': timestamp_to, - 'action_choices': AuditLog.ACTION_CHOICES, - 'active_nav': 'audit', - } - - return render(request, 'admin_base/audit/auditlog_list.html', context) - - -@superadmin_required -def auditlog_detail(request, pk): - """ - 审计日志详情视图(只读) - """ - log = get_object_or_404( - AuditLog.objects.select_related('user', 'host', 'content_type'), - pk=pk - ) - - context = { - 'log': log, - 'active_nav': 'audit', - } - - return render(request, 'admin_base/audit/auditlog_detail.html', context) diff --git a/apps/bootstrap/README.md b/apps/bootstrap/README.md deleted file mode 100644 index de237a3..0000000 --- a/apps/bootstrap/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# C端主机自动化初始化与安全认证系统 - -## 系统概述 - -本系统实现了符合共享技术契约的C端主机自动化初始化与安全认证功能,支持基于TOTP的双重验证机制,确保H端和C端之间的安全对接。 - -## 核心功能 - -### 1. 数据库设计 - -系统包含两个核心数据表: - -#### InitialToken(初始令牌表) -| 字段名 | 类型 | 说明 | -|--------|------|------| -| token | String (PK) | AccessToken | -| host | ForeignKey | 关联的主机 | -| expires_at | Datetime | AccessToken过期时间 | -| status | Enum | `ISSUED`(已签发), `TOTP_VERIFIED`(已验证), `CONSUMED`(已消耗) | -| created_at | Datetime | 创建时间 | - -#### ActiveSession(活动会话表) -| 字段名 | 类型 | 说明 | -|---------|------|------| -| session_token | String (PK) | 颁发给H端的临时凭证 | -| host | ForeignKey | 关联的主机 | -| bound_ip | String | **关键**:绑定的请求源IP | -| expires_at | Datetime | 24小时后的过期时间 | -| created_at | Datetime | 创建时间 | - -### 2. API接口 - -#### A. TOTP验证接口 -- **URL**: `POST /bootstrap/verify-totp/` -- **Request Body**: -```json -{ - "host_id": "Unique-Host-ID", - "totp_code": "123456" -} -``` - -#### B. Token交换接口 -- **URL**: `POST /bootstrap/exchange-token/` -- **Headers**: `Authorization: Bearer {AccessToken}` -- **Response**: -```json -{ - "success": true, - "session_token": "new-session-uuid", - "expires_in": 86400 -} -``` - -#### C. 会话吊销接口 -- **URL**: `DELETE /bootstrap/session/` -- **Headers**: `Authorization: Bearer {session_token}` - -### 3. 安全机制 - -#### 密钥派生算法 -严格按照共享技术契约实现: -1. 拼接字符串:`input_string = token + "|" + host_id + "|" + expires_at` -2. 哈希计算:`raw_hash = HMAC-SHA256(key="SHARED_STATIC_SALT", message=input_string)` -3. 截取与编码:取`raw_hash`的前20个字节,进行**Base32**编码 - -#### TOTP算法参数 -- **算法**: HMAC-SHA1 -- **时间步长**: 30秒 -- **位数**: 6位数字 -- **初始时间**: Unix Epoch (T0 = 0) - -## 管理界面 - -系统集成到Django Admin中,提供以下功能: -- 初始令牌管理 -- 活动会话监控 -- 一键生成令牌 -- 状态监控 - -## 自动化任务 - -- 定期清理过期的活动会话 -- 定期清理过期的初始令牌 - -## 配置要求 - -在`settings.py`中配置共享盐值: -```python -BOOTSTRAP_SHARED_SALT = os.environ.get('BOOTSTRAP_SHARED_SALT', 'MY_SECRET_2024') -``` - -## 使用流程 - -1. 管理员在Django Admin中生成初始令牌 -2. 系统生成包含C端URL、令牌、主机ID等信息的Base64配置字符串 -3. H端使用配置字符串进行初始化 -4. 用户在C端输入H端显示的TOTP码进行验证 -5. H端使用Access Token换取Session Token -6. 双方通过Session Token进行后续安全通信 \ No newline at end of file diff --git a/apps/bootstrap/admin.py b/apps/bootstrap/admin.py deleted file mode 100644 index 32abb64..0000000 --- a/apps/bootstrap/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 引导系统无需后台管理 diff --git a/apps/bootstrap/apps.py b/apps/bootstrap/apps.py deleted file mode 100755 index 9ef9793..0000000 --- a/apps/bootstrap/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.apps import AppConfig - - -class BootstrapConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.bootstrap' - verbose_name = '主机引导系统' - - def ready(self): - # 导入信号处理器 - import apps.bootstrap.signals \ No newline at end of file diff --git a/apps/bootstrap/management/commands/cleanup_expired_sessions.py b/apps/bootstrap/management/commands/cleanup_expired_sessions.py deleted file mode 100644 index 9b9c12c..0000000 --- a/apps/bootstrap/management/commands/cleanup_expired_sessions.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.core.management.base import BaseCommand -from django.utils import timezone -from apps.bootstrap.models import ActiveSession, InitialToken - - -class Command(BaseCommand): - help = '清理过期的活动会话和初始令牌' - - def add_arguments(self, parser): - parser.add_argument( - '--dry-run', - action='store_true', - help='仅显示将要删除的记录,不实际删除', - ) - - def handle(self, *args, **options): - dry_run = options['dry_run'] - now = timezone.now() - - expired_sessions = ActiveSession.objects.filter( - expires_at__lt=now, - ) - if expired_sessions.exists(): - count = expired_sessions.count() - self.stdout.write( - f'找到 {count} 个过期的会话' - ) - if not dry_run: - expired_sessions.delete() - self.stdout.write( - self.style.SUCCESS( - f'已删除 {count} 个过期的会话' - ) - ) - - expired_tokens = InitialToken.objects.filter( - expires_at__lt=now, - ) - if expired_tokens.exists(): - count = expired_tokens.count() - self.stdout.write( - f'找到 {count} 个过期的初始令牌' - ) - if not dry_run: - expired_tokens.delete() - self.stdout.write( - self.style.SUCCESS( - f'已删除 {count} 个过期的初始令牌' - ) - ) - - orphan_tokens = InitialToken.objects.filter( - host=None, - status='ISSUED', - expires_at__gt=now, - ) - if orphan_tokens.exists(): - count = orphan_tokens.count() - self.stdout.write( - f'找到 {count} 个未关联主机的初始令牌' - ) - if not dry_run: - orphan_tokens.delete() - self.stdout.write( - self.style.SUCCESS( - f'已删除 {count} 个未关联主机的初始令牌' - ) - ) - - if not any([expired_sessions.exists(), expired_tokens.exists(), orphan_tokens.exists()]): - self.stdout.write( - self.style.SUCCESS('没有需要清理的记录') - ) \ No newline at end of file diff --git a/apps/bootstrap/middleware.py b/apps/bootstrap/middleware.py deleted file mode 100644 index e99c6ff..0000000 --- a/apps/bootstrap/middleware.py +++ /dev/null @@ -1,122 +0,0 @@ -import logging -from django.http import JsonResponse -from .models import ActiveSession -from django.utils import timezone -from django.urls import resolve - -logger = logging.getLogger(__name__) - - -class SessionValidationMiddleware: - """会话验证中间件 - 根据规范实现""" - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # 记录请求信息用于调试 - client_ip = self.get_client_ip(request) - user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown') - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - - logger.debug(f"Session validation middleware processing request: path={request.path}, method={request.method}") - - # 检查是否需要验证会话的API端点 - # 仅对需要认证的API端点进行验证 - # 排除不需要SessionToken验证的特殊端点 - excluded_paths = [ - '/api/exchange_token', - '/api/exchange_token/', - '/bootstrap/exchange-token/', - '/api/get_session_token', - '/api/get_session_token/', - '/bootstrap/api/get_session_token', - '/bootstrap/api/get_session_token/', - '/api/check_totp_status', - '/api/check_totp_status/', - '/bootstrap/api/check_totp_status', - '/bootstrap/api/check_totp_status/', - '/bootstrap/sse/init-status', - '/bootstrap/sse/init-status/', - '/bootstrap/api/upload_host_cert', - '/bootstrap/api/upload_host_cert/', - ] - - if (request.path.startswith('/api/') or - request.path.startswith('/bootstrap/')) and \ - request.path not in excluded_paths: - - logger.debug(f"Checking session for protected endpoint: {request.path}") - - # 检查Authorization头部 - if auth_header.startswith('Bearer '): - session_token = auth_header.split(' ')[1] - logger.debug("Found Bearer token, validating session") - - # 验证会话有效性 - is_valid, result = self.check_session_validity(request, session_token) - - if not is_valid: - logger.warning(f"Session validation failed for request {request.path}: {result}") - return JsonResponse({ - 'success': False, - 'error': 'Access denied', - }, status=403) - else: - logger.debug("Session validation successful") - else: - logger.debug("No valid Bearer authorization header found") - else: - logger.debug(f"Skipping session validation for path: {request.path}") - - response = self.get_response(request) - return response - - def check_session_validity(self, request, session_token): - """检查会话有效性""" - try: - logger.debug(f"Looking up session token: {session_token[:8]}...") - session = ActiveSession.objects.get( - session_token=session_token, - expires_at__gt=timezone.now() - ) - - logger.debug(f"Found active session for host: {session.host.name} (ID: {session.host.id})") - - # 获取真实客户端IP - current_ip = self.get_client_ip(request) - bound_ip = session.bound_ip - - logger.debug(f"Comparing IPs - Request IP: {current_ip}, Bound IP: {bound_ip}") - - # IP校验 - if session.bound_ip != current_ip: - error_msg = f"IP address mismatch - request from {current_ip}, session bound to {bound_ip}" - logger.warning(error_msg) - logger.warning(f"Session details: token={session_token[:8]}..., host={session.host.name}, created={session.created_at}") - return False, error_msg - - logger.debug(f"IP validation passed: {current_ip}") - return True, session - - except ActiveSession.DoesNotExist: - error_msg = f"Invalid or expired session token: {session_token[:8]}..." - logger.warning(error_msg) - return False, error_msg - except Exception as e: - error_msg = f"Error during session validation: {str(e)}" - logger.error(error_msg, exc_info=True) - return False, error_msg - - def get_client_ip(self, request): - """获取客户端真实IP地址""" - from django.conf import settings - if getattr(settings, 'USE_X_FORWARDED_FOR', False): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0].strip() - logger.debug(f"Got IP from X-Forwarded-For: {ip}") - return ip - ip = request.META.get('REMOTE_ADDR', '127.0.0.1') - logger.debug(f"Got IP from REMOTE_ADDR: {ip}") - return ip \ No newline at end of file diff --git a/apps/bootstrap/migrations/0001_initial.py b/apps/bootstrap/migrations/0001_initial.py deleted file mode 100755 index e42f3d0..0000000 --- a/apps/bootstrap/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 11:58 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('hosts', '0005_host_connection_type_alter_host_port'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='BootstrapToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(max_length=255, unique=True, verbose_name='引导令牌')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('expires_at', models.DateTimeField(verbose_name='过期时间')), - ('is_used', models.BooleanField(default=False, verbose_name='是否已使用')), - ('used_at', models.DateTimeField(blank=True, null=True, verbose_name='使用时间')), - ('notes', models.TextField(blank=True, null=True, verbose_name='备注')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ('host', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联主机')), - ('used_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='used_bootstrap_tokens', to=settings.AUTH_USER_MODEL, verbose_name='使用者')), - ], - options={ - 'verbose_name': '引导令牌', - 'verbose_name_plural': '引导令牌', - 'db_table': 'bootstrap_token', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/apps/bootstrap/migrations/0002_bootstraptoken_is_paired_bootstraptoken_paired_at_and_more.py b/apps/bootstrap/migrations/0002_bootstraptoken_is_paired_bootstraptoken_paired_at_and_more.py deleted file mode 100644 index 971bd4f..0000000 --- a/apps/bootstrap/migrations/0002_bootstraptoken_is_paired_bootstraptoken_paired_at_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 08:54 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0005_host_connection_type_alter_host_port'), - ('bootstrap', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='bootstraptoken', - name='is_paired', - field=models.BooleanField(default=False, verbose_name='是否已配对'), - ), - migrations.AddField( - model_name='bootstraptoken', - name='paired_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='配对时间'), - ), - migrations.AddField( - model_name='bootstraptoken', - name='pairing_code', - field=models.CharField(blank=True, max_length=8, null=True, unique=True, verbose_name='配对码'), - ), - migrations.AddField( - model_name='bootstraptoken', - name='pairing_code_expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='配对码过期时间'), - ), - migrations.AlterField( - model_name='bootstraptoken', - name='host', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联主机'), - ), - ] diff --git a/apps/bootstrap/migrations/0003_bootstraptoken_totp_secret.py b/apps/bootstrap/migrations/0003_bootstraptoken_totp_secret.py deleted file mode 100644 index 3d4bf26..0000000 --- a/apps/bootstrap/migrations/0003_bootstraptoken_totp_secret.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 09:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0002_bootstraptoken_is_paired_bootstraptoken_paired_at_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='bootstraptoken', - name='totp_secret', - field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='TOTP密钥'), - ), - ] diff --git a/apps/bootstrap/migrations/0004_remove_bootstraptoken_pairing_code_and_more.py b/apps/bootstrap/migrations/0004_remove_bootstraptoken_pairing_code_and_more.py deleted file mode 100644 index 9a684cd..0000000 --- a/apps/bootstrap/migrations/0004_remove_bootstraptoken_pairing_code_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 11:48 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0005_host_connection_type_alter_host_port'), - ('bootstrap', '0003_bootstraptoken_totp_secret'), - ] - - operations = [ - migrations.RemoveField( - model_name='bootstraptoken', - name='pairing_code', - ), - migrations.RemoveField( - model_name='bootstraptoken', - name='pairing_code_expires_at', - ), - migrations.AlterField( - model_name='bootstraptoken', - name='host', - field=models.OneToOneField(default=1, on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联主机'), - preserve_default=False, - ), - migrations.CreateModel( - name='InitialToken', - fields=[ - ('token', models.CharField(max_length=255, primary_key=True, serialize=False, verbose_name='AccessToken')), - ('expires_at', models.DateTimeField(verbose_name='AccessToken过期时间')), - ('status', models.CharField(choices=[('ISSUED', '已签发'), ('TOTP_VERIFIED', '已验证'), ('CONSUMED', '已消耗')], default='ISSUED', max_length=20, verbose_name='状态')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联的主机')), - ], - options={ - 'verbose_name': '初始令牌', - 'verbose_name_plural': '初始令牌', - 'db_table': 'initial_token', - }, - ), - migrations.CreateModel( - name='ActiveSession', - fields=[ - ('session_token', models.CharField(max_length=255, primary_key=True, serialize=False, verbose_name='临时凭证')), - ('bound_ip', models.GenericIPAddressField(verbose_name='绑定的请求源IP')), - ('expires_at', models.DateTimeField(verbose_name='24小时后的过期时间')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联的主机')), - ], - options={ - 'verbose_name': '活动会话', - 'verbose_name_plural': '活动会话', - 'db_table': 'active_session', - }, - ), - ] diff --git a/apps/bootstrap/migrations/0005_delete_bootstraptoken.py b/apps/bootstrap/migrations/0005_delete_bootstraptoken.py deleted file mode 100644 index 4ccf250..0000000 --- a/apps/bootstrap/migrations/0005_delete_bootstraptoken.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 11:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0004_remove_bootstraptoken_pairing_code_and_more'), - ] - - operations = [ - migrations.DeleteModel( - name='BootstrapToken', - ), - ] diff --git a/apps/bootstrap/migrations/0006_alter_activesession_expires_at.py b/apps/bootstrap/migrations/0006_alter_activesession_expires_at.py deleted file mode 100644 index 83516e7..0000000 --- a/apps/bootstrap/migrations/0006_alter_activesession_expires_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-03 03:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0005_delete_bootstraptoken'), - ] - - operations = [ - migrations.AlterField( - model_name='activesession', - name='expires_at', - field=models.DateTimeField(verbose_name='会话过期时间'), - ), - ] diff --git a/apps/bootstrap/migrations/0007_update_initialtoken_for_pairing.py b/apps/bootstrap/migrations/0007_update_initialtoken_for_pairing.py deleted file mode 100644 index 4b426fb..0000000 --- a/apps/bootstrap/migrations/0007_update_initialtoken_for_pairing.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0006_alter_activesession_expires_at'), - ] - - operations = [ - migrations.AddField( - model_name='initialtoken', - name='pairing_code', - field=models.CharField(blank=True, max_length=6, null=True, verbose_name='配对码'), - ), - migrations.AddField( - model_name='initialtoken', - name='pairing_code_expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='配对码过期时间'), - ), - migrations.AlterField( - model_name='initialtoken', - name='status', - field=models.CharField(choices=[('ISSUED', '已签发'), ('PAIRED', '已配对'), ('CONSUMED', '已消耗')], default='ISSUED', max_length=20, verbose_name='状态'), - ), - migrations.AlterField( - model_name='activesession', - name='expires_at', - field=models.DateTimeField(verbose_name='会话过期时间'), - ), - ] \ No newline at end of file diff --git a/apps/bootstrap/migrations/0008_add_pairing_attempts.py b/apps/bootstrap/migrations/0008_add_pairing_attempts.py deleted file mode 100644 index dd3ddc4..0000000 --- a/apps/bootstrap/migrations/0008_add_pairing_attempts.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-10 04:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0007_update_initialtoken_for_pairing'), - ] - - operations = [ - migrations.AddField( - model_name='initialtoken', - name='pairing_attempts', - field=models.IntegerField(default=0, verbose_name='配对码验证尝试次数'), - ), - ] diff --git a/apps/bootstrap/migrations/0009_initialtoken_host_nullable.py b/apps/bootstrap/migrations/0009_initialtoken_host_nullable.py deleted file mode 100644 index ca11b15..0000000 --- a/apps/bootstrap/migrations/0009_initialtoken_host_nullable.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-23 09:29 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0011_host_auth_method_host_cert_key_path_and_more'), - ('bootstrap', '0008_add_pairing_attempts'), - ] - - operations = [ - migrations.AlterField( - model_name='initialtoken', - name='host', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联的主机'), - ), - ] diff --git a/apps/bootstrap/migrations/0010_add_cert_data.py b/apps/bootstrap/migrations/0010_add_cert_data.py deleted file mode 100644 index 27a0b8b..0000000 --- a/apps/bootstrap/migrations/0010_add_cert_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-23 14:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0009_initialtoken_host_nullable'), - ] - - operations = [ - migrations.AddField( - model_name='initialtoken', - name='cert_data', - field=models.JSONField(blank=True, default=None, null=True, verbose_name='暂存证书数据'), - ), - ] diff --git a/apps/bootstrap/migrations/0011_add_cert_provision_token.py b/apps/bootstrap/migrations/0011_add_cert_provision_token.py deleted file mode 100644 index e7e33e4..0000000 --- a/apps/bootstrap/migrations/0011_add_cert_provision_token.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-29 15:57 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0012_host_username_optional'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('bootstrap', '0010_add_cert_data'), - ] - - operations = [ - migrations.CreateModel( - name='CertProvisionToken', - fields=[ - ('token', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='配置令牌')), - ('server_host', models.CharField(max_length=255, verbose_name='服务器地址')), - ('expires_at', models.DateTimeField(verbose_name='过期时间')), - ('status', models.CharField(choices=[('ISSUED', '已签发'), ('HOSTNAME_UPLOADED', '主机名已上传'), ('CERT_ISSUED', '证书已签发'), ('HOST_CONFIGURED', '主机已配置'), ('CONSUMED', '已消耗')], default='ISSUED', max_length=20, verbose_name='状态')), - ('consumed_at', models.DateTimeField(blank=True, null=True, verbose_name='消耗时间')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ('host', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联的主机')), - ], - options={ - 'verbose_name': '证书配置令牌', - 'verbose_name_plural': '证书配置令牌', - 'db_table': 'cert_provision_token', - }, - ), - ] diff --git a/apps/bootstrap/migrations/0012_certprovisiontoken_cert_data_and_more.py b/apps/bootstrap/migrations/0012_certprovisiontoken_cert_data_and_more.py deleted file mode 100644 index 62d73eb..0000000 --- a/apps/bootstrap/migrations/0012_certprovisiontoken_cert_data_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-30 00:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0011_add_cert_provision_token'), - ] - - operations = [ - migrations.AddField( - model_name='certprovisiontoken', - name='cert_data', - field=models.JSONField(blank=True, default=None, null=True, verbose_name='暂存证书数据'), - ), - migrations.AddField( - model_name='certprovisiontoken', - name='hostname', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='主机名'), - ), - ] diff --git a/apps/bootstrap/migrations/0013_certprovisiontoken_ip_address.py b/apps/bootstrap/migrations/0013_certprovisiontoken_ip_address.py deleted file mode 100644 index de5339d..0000000 --- a/apps/bootstrap/migrations/0013_certprovisiontoken_ip_address.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-30 02:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bootstrap', '0012_certprovisiontoken_cert_data_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='certprovisiontoken', - name='ip_address', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='主机IP地址'), - ), - ] diff --git a/apps/bootstrap/migrations/__init__.py b/apps/bootstrap/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/bootstrap/models.py b/apps/bootstrap/models.py deleted file mode 100644 index aaa9636..0000000 --- a/apps/bootstrap/models.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.db import models -from apps.hosts.models import Host -import uuid -from django.utils import timezone -from datetime import timedelta -import secrets as _secrets -from django.conf import settings - - -class InitialToken(models.Model): - """初始配置令牌表 - 基于配对码的简化认证机制""" - STATUS_CHOICES = [ - ('ISSUED', '已签发'), - ('PAIRED', '已配对'), - ('CONSUMED', '已消耗'), - ] - - MAX_PAIRING_ATTEMPTS = 5 - - token = models.CharField(max_length=255, primary_key=True, verbose_name="AccessToken") - host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name="关联的主机", null=True, blank=True) - expires_at = models.DateTimeField(verbose_name="AccessToken过期时间") - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ISSUED', verbose_name="状态") - pairing_code = models.CharField(max_length=6, verbose_name="配对码", blank=True, null=True) - pairing_code_expires_at = models.DateTimeField(verbose_name="配对码过期时间", blank=True, null=True) - pairing_attempts = models.IntegerField(default=0, verbose_name="配对码验证尝试次数") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - cert_data = models.JSONField(verbose_name="暂存证书数据", blank=True, null=True, default=None) - - class Meta: - verbose_name = "初始令牌" - verbose_name_plural = "初始令牌" - db_table = "initial_token" - - def generate_pairing_code(self): - """生成6位数字配对码""" - code = f"{_secrets.randbelow(1000000):06d}" - self.pairing_code = code - self.pairing_code_expires_at = timezone.now() + timedelta(minutes=5) - self.pairing_attempts = 0 - self.save(update_fields=['pairing_code', 'pairing_code_expires_at', 'pairing_attempts']) - return code - - def verify_pairing_code(self, input_code): - """验证配对码是否正确且未过期,含尝试次数限制""" - if not self.pairing_code or not self.pairing_code_expires_at: - return False - - if timezone.now() > self.pairing_code_expires_at: - return False - - from django.db.models import F - InitialToken.objects.filter(pk=self.pk).update( - pairing_attempts=F('pairing_attempts') + 1 - ) - self.refresh_from_db() - - if self.pairing_attempts >= self.MAX_PAIRING_ATTEMPTS: - self.pairing_code = None - self.pairing_code_expires_at = None - self.save(update_fields=['pairing_code', 'pairing_code_expires_at']) - return False - - if self.pairing_code != input_code: - return False - - self.status = 'PAIRED' - self.pairing_code = None - self.pairing_code_expires_at = None - self.pairing_attempts = 0 - self.save(update_fields=['status', 'pairing_code', 'pairing_code_expires_at', 'pairing_attempts']) - return True - - -class ActiveSession(models.Model): - """活动会话表 - 基于配对码认证的会话管理""" - session_token = models.CharField(max_length=255, primary_key=True, verbose_name="临时凭证") - host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name="关联的主机") - bound_ip = models.GenericIPAddressField(verbose_name="绑定的请求源IP") - expires_at = models.DateTimeField(verbose_name="会话过期时间") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - - class Meta: - verbose_name = "活动会话" - verbose_name_plural = "活动会话" - db_table = "active_session" - - -class CertProvisionToken(models.Model): - STATUS_CHOICES = [ - ('ISSUED', '已签发'), - ('HOSTNAME_UPLOADED', '主机名已上传'), - ('CERT_ISSUED', '证书已签发'), - ('HOST_CONFIGURED', '主机已配置'), - ('CONSUMED', '已消耗'), - ] - - token = models.CharField(max_length=64, primary_key=True, verbose_name="配置令牌") - host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name="关联的主机", null=True, blank=True) - server_host = models.CharField(max_length=255, verbose_name="服务器地址") - hostname = models.CharField(max_length=255, verbose_name="主机名", blank=True, default='') - ip_address = models.CharField(max_length=255, verbose_name="主机IP地址", blank=True, default='') - expires_at = models.DateTimeField(verbose_name="过期时间") - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ISSUED', verbose_name="状态") - cert_data = models.JSONField(verbose_name="暂存证书数据", blank=True, null=True, default=None) - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建者") - consumed_at = models.DateTimeField(null=True, blank=True, verbose_name="消耗时间") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - - class Meta: - verbose_name = "证书配置令牌" - verbose_name_plural = "证书配置令牌" - db_table = "cert_provision_token" - - def is_expired(self): - return timezone.now() > self.expires_at - - def is_valid(self): - return self.status == 'ISSUED' and not self.is_expired() \ No newline at end of file diff --git a/apps/bootstrap/signals.py b/apps/bootstrap/signals.py deleted file mode 100644 index 57ad72a..0000000 --- a/apps/bootstrap/signals.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -主机引导应用的信号处理器 -""" -from django.db.models.signals import post_save, pre_delete -from django.dispatch import receiver -from .models import InitialToken, ActiveSession - - -# 可以在这里添加具体的信号处理器 -# 例如:在引导令牌即将过期时发送通知 \ No newline at end of file diff --git a/apps/bootstrap/tasks.py b/apps/bootstrap/tasks.py deleted file mode 100644 index aff8b27..0000000 --- a/apps/bootstrap/tasks.py +++ /dev/null @@ -1,272 +0,0 @@ -import base64 -import datetime -import logging -from datetime import timedelta -from typing import cast - -from celery import shared_task -from cryptography import x509 -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec -from django.utils import timezone - -from .models import ActiveSession, InitialToken - -logger = logging.getLogger(__name__) - - -@shared_task -def cleanup_expired_sessions(): - try: - expired_sessions = ActiveSession.objects.filter( - expires_at__lt=timezone.now() - ) - count = expired_sessions.count() - expired_sessions.delete() - logger.info(f"清理了 {count} 个过期的活动会话") - return f"清理了 {count} 个过期的活动会话" - except Exception as e: - logger.error(f"清理过期会话时出错: {str(e)}") - raise - - -@shared_task -def cleanup_expired_initial_tokens(): - try: - cutoff_time = timezone.now() - timedelta(days=7) - expired_tokens = InitialToken.objects.filter( - expires_at__lt=cutoff_time - ) - count = expired_tokens.count() - expired_tokens.delete() - logger.info(f"清理了 {count} 个过期的初始令牌") - return f"清理了 {count} 个过期的初始令牌" - except Exception as e: - logger.error(f"清理过期初始令牌时出错: {str(e)}") - raise - - -@shared_task -def generate_bootstrap_config(hostname, ip_address, operator_id): - try: - config = { - 'hostname': hostname, - 'ip_address': ip_address, - 'generated_at': timezone.now().isoformat(), - 'status': 'success', - } - return {'success': True, 'config': config} - except Exception as e: - logger.error(f"生成引导配置时出错: {str(e)}") - return {'success': False, 'error': str(e)} - - -@shared_task -def initialize_host_bootstrap(host_id, operator_id): - try: - from apps.hosts.models import Host - host = Host.objects.get(id=host_id) - return { - 'host_id': host_id, - 'hostname': host.hostname, - 'status': 'completed', - 'completed_at': timezone.now().isoformat(), - } - except Exception as e: - logger.error(f"初始化主机引导时出错: {str(e)}") - raise - - -@shared_task(bind=True, max_retries=1) -def cert_provision_issue_certs(self, token_str): - from apps.bootstrap.models import CertProvisionToken - from apps.certificates.models import CertificateAuthority - from utils.cert_service import ( - issue_server_cert, issue_client_cert, - generate_random_username, generate_random_password, - ) - from utils.cert_storage import generate_cert_paths, save_cert_files - - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return - - if provision_token.status != 'HOSTNAME_UPLOADED': - return - - host = provision_token.host - hostname = host.hostname if host else provision_token.hostname - if not hostname: - return - - ip_address = provision_token.ip_address or '' - - ca_obj = CertificateAuthority.objects.filter(is_active=True).first() - if not ca_obj: - from utils.cert_service import generate_ca as _gen_ca - - ca_key, ca_cert = _gen_ca() - ca_obj = CertificateAuthority( - name='WinRM-CA', is_active=True, - ) - ca_key_pem = ca_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - ca_cert_pem = ca_cert.public_bytes( - serialization.Encoding.PEM, - ) - ca_obj.save_ca_files(ca_key_pem, ca_cert_pem) - ca_obj.expires_at = ( - datetime.datetime.now(datetime.timezone.utc) - + datetime.timedelta(days=3650) - ) - ca_obj.save() - - ca_key_pem = ca_obj.private_key - ca_cert_pem = ca_obj.certificate - if not ca_key_pem or not ca_cert_pem: - logger.error( - f"CA {ca_obj.name} key/cert files not found on disk" - ) - return - - ca_key = cast( - ec.EllipticCurvePrivateKey, - serialization.load_pem_private_key( - ca_key_pem.encode(), password=None, - ), - ) - ca_cert = x509.load_pem_x509_certificate(ca_cert_pem.encode()) - - ntlm_user = generate_random_username() - ntlm_password = generate_random_password() - upn_value = f"{ntlm_user}@localhost" - - server_result = issue_server_cert( - ca_key=ca_key, - ca_cert=ca_cert, - hostname=hostname, - ip_address=ip_address or None, - ) - - client_key, client_cert = issue_client_cert( - ca_key=ca_key, - ca_cert=ca_cert, - upn_value=upn_value, - ) - - cert_root, cert_sub = generate_cert_paths() - - ca_cert_pem = ca_cert.public_bytes(serialization.Encoding.PEM) - client_cert_pem = client_cert.public_bytes(serialization.Encoding.PEM) - client_key_pem = client_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - cert_dir = save_cert_files( - cert_root=cert_root, - cert_sub=cert_sub, - ca_cert_pem=ca_cert_pem, - client_cert_pem=client_cert_pem, - server_pfx_bytes=server_result['pfx_data'], - client_key_pem=client_key_pem, - ) - - if host: - host.cert_root = cert_root - host.cert_sub = cert_sub - host.pfx_password = server_result['pfx_password'] - host.ntlm_fallback_user = ntlm_user - host.ntlm_fallback_password = ntlm_password - host.cert_provision_status = 'ready' - host.cert_pem_path = str(cert_dir / 'client.crt') - host.cert_key_path = str(cert_dir / 'client.key') - host.auth_method = 'certificate' - host.use_ssl = True - if host.port == 5985: - host.port = 5986 - host.save() - - if not host: - provision_token.cert_data = { - 'cert_root': cert_root, - 'cert_sub': cert_sub, - 'pfx_password': server_result['pfx_password'], - 'ntlm_user': ntlm_user, - 'ntlm_password': ntlm_password, - 'ca_cert_b64': base64.b64encode( - ca_cert_pem - ).decode('utf-8'), - 'client_cert_b64': base64.b64encode( - client_cert_pem - ).decode('utf-8'), - 'server_pfx_b64': base64.b64encode( - server_result['pfx_data'] - ).decode('utf-8'), - } - - provision_token.status = 'CERT_ISSUED' - provision_token.save() - - return {'success': True, 'host_id': host.pk if host else None} - - -@shared_task -def cleanup_expired_provision_tokens(): - from apps.bootstrap.models import CertProvisionToken - now = timezone.now() - CertProvisionToken.objects.filter( - status='ISSUED', expires_at__lt=now, - ).delete() - week_ago = now - timedelta(days=7) - CertProvisionToken.objects.filter(expires_at__lt=week_ago).delete() - - -@shared_task -def cleanup_unactivated_certificates(): - from apps.hosts.models import Host - from utils.cert_storage import delete_cert_files - now = timezone.now() - cutoff = now - timedelta(minutes=60) - hosts = Host.objects.filter( - cert_provision_status__in=['pending', 'ready'], - created_at__lt=cutoff, - cert_activated_at__isnull=True, - ) - for host in hosts: - if host.cert_root and host.cert_sub: - delete_cert_files(host.cert_root, host.cert_sub) - host.cert_provision_status = 'failed' - host.cert_root = '' - host.cert_sub = '' - host.save() - - -@shared_task -def cleanup_orphan_cert_dirs(): - from apps.hosts.models import Host - from utils.cert_storage import get_cert_base_dir - import shutil - base_dir = get_cert_base_dir() - if not base_dir.exists(): - return - active_paths = set() - for host in Host.objects.filter(cert_root__gt='', cert_sub__gt=''): - active_paths.add((host.cert_root, host.cert_sub)) - for root_dir in base_dir.iterdir(): - if root_dir.is_dir() and len(root_dir.name) == 2: - for sub_dir in root_dir.iterdir(): - if sub_dir.is_dir() and len(sub_dir.name) == 2: - if (root_dir.name, sub_dir.name) not in active_paths: - shutil.rmtree(sub_dir, ignore_errors=True) - try: - root_dir.rmdir() - except OSError: - logger.debug( - "Skipping removal of non-empty or inaccessible orphan cert root dir: %s", - root_dir, - ) diff --git a/apps/bootstrap/tests/__init__.py b/apps/bootstrap/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/bootstrap/token_utils.py b/apps/bootstrap/token_utils.py deleted file mode 100644 index 6a967ff..0000000 --- a/apps/bootstrap/token_utils.py +++ /dev/null @@ -1,21 +0,0 @@ -import base64 -import json - - -def encode_provision_token(raw_token: str, scheme: str, host: str) -> str: - payload = json.dumps({"t": raw_token, "s": scheme, "h": host}, separators=(',', ':')) - return base64.urlsafe_b64encode(payload.encode('utf-8')).decode('ascii') - - -def decode_provision_token(encoded: str) -> dict | None: - try: - padding = 4 - len(encoded) % 4 - if padding != 4: - encoded += '=' * padding - payload = base64.urlsafe_b64decode(encoded.encode('ascii')).decode('utf-8') - data = json.loads(payload) - if 't' in data and 's' in data and 'h' in data: - return data - except Exception: - pass - return None diff --git a/apps/bootstrap/urls.py b/apps/bootstrap/urls.py deleted file mode 100644 index b18bd70..0000000 --- a/apps/bootstrap/urls.py +++ /dev/null @@ -1,48 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'bootstrap' - -urlpatterns = [ - # 引导配置API - path('config/', views.get_bootstrap_config, name='get_bootstrap_config'), - path('trigger/', views.trigger_host_bootstrap, name='trigger_host_bootstrap'), - path('create-initial-token/', views.create_initial_token, name='create_initial_token'), - path('status/', views.check_bootstrap_status, name='check_bootstrap_status'), - path('validate-token/', views.validate_bootstrap_token, name='validate_bootstrap_token'), - - # 引导管理API - path('manage/', views.BootstrapManagementView.as_view(), name='bootstrap_management'), - - # 新增API端点 - 基于配对码的认证机制 - path('verify-pairing-code/', views.verify_pairing_code, name='verify_pairing_code'), - path('api/verify-pairing-code/', views.verify_pairing_code, name='api_verify_pairing_code'), - path('exchange-token/', views.exchange_token, name='exchange_token'), - path('session/', views.revoke_session, name='revoke_session'), - - # API端点别名 - 为H端提供兼容路径 - path('api/verify_pairing_code/', views.verify_pairing_code, name='api_verify_pairing_code'), - path('api/exchange_token/', views.exchange_token, name='api_exchange_token'), - path('api/get_session_token', views.get_session_token, name='api_get_session_token_no_slash'), # 不带斜杠版本 - path('api/get_session_token/', views.get_session_token, name='api_get_session_token'), - path('api/upload_host_cert/', views.upload_host_cert, name='api_upload_host_cert'), - path('api/check_pairing_status', views.check_pairing_status, name='api_check_pairing_status'), # 新增:检查配对状态 - path('api/session/', views.revoke_session, name='api_revoke_session'), - - # 自动注册API - path('api/auto-register/', views.auto_register_host, name='auto_register_host'), - path('api/complete-auto-register/', views.complete_auto_register, name='complete_auto_register'), - path('api/pending-hosts/', views.get_pending_hosts, name='get_pending_hosts'), - path('api/revoke-pending-host/', views.revoke_pending_host, name='revoke_pending_host'), - - path('sse/init-status/', views.sse_init_status, name='sse_init_status'), - - path('api/cert-provision/validate/', views.cert_provision_validate, name='cert_provision_validate'), - path('api/cert-provision/upload-hostname/', views.cert_provision_upload_hostname, name='cert_provision_upload_hostname'), - path('api/cert-provision/download-certs/', views.cert_provision_download_certs, name='cert_provision_download_certs'), - path('api/cert-provision/notify-complete/', views.cert_provision_notify_complete, name='cert_provision_notify_complete'), - path('api/cert-provision/disable-password-auth/', views.cert_provision_disable_password_auth, name='cert_provision_disable_password_auth'), - path('api/cert-provision/test-result/', views.cert_provision_test_result, name='cert_provision_test_result'), - path('api/cert-provision/status-stream/', views.cert_provision_status_stream, name='cert_provision_status_stream'), - path('api/cert-provision/test-stream/', views.cert_provision_test_stream, name='cert_provision_test_stream'), -] \ No newline at end of file diff --git a/apps/bootstrap/views.py b/apps/bootstrap/views.py deleted file mode 100644 index 4323ba7..0000000 --- a/apps/bootstrap/views.py +++ /dev/null @@ -1,1444 +0,0 @@ -from django.http import JsonResponse, StreamingHttpResponse -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.decorators import method_decorator -from django.views import View -from .models import InitialToken, ActiveSession, CertProvisionToken -from apps.hosts.models import Host -from apps.certificates.models import CertificateAuthority, ServerCertificate -from apps.tasks.models import AsyncTask -from apps.bootstrap.tasks import generate_bootstrap_config, initialize_host_bootstrap -from django.shortcuts import get_object_or_404 -import json -import logging -import base64 -from django.utils import timezone -from django.core.cache import cache -import secrets -import uuid -import time -import hmac - -from utils.helpers import get_client_ip - - -logger = logging.getLogger(__name__) - - -def _bootstrap_rate_limit(key_prefix, rate='10/m'): - limit, period = rate.lower().split('/') - limit = int(limit) - period_map = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} - period_seconds = period_map.get(period, 60) - - def decorator(view_func): - def wrapper(request, *args, **kwargs): - ip = get_client_ip(request) - window = int(time.time() // period_seconds) - cache_key = f'rl:{key_prefix}:{ip}:{window}' - current = cache.get(cache_key, 0) - if current >= limit: - return JsonResponse( - {'success': False, 'error': 'Too many requests'}, - status=429, - ) - cache.set(cache_key, current + 1, timeout=period_seconds + 1) - return view_func(request, *args, **kwargs) - wrapper.__name__ = view_func.__name__ - return wrapper - return decorator - - -def _save_cert_to_host(host, pfx_b64, pfx_password, service_user, service_password): - import base64 - from cryptography.hazmat.primitives.serialization import pkcs12, Encoding, PrivateFormat, NoEncryption - - try: - pfx_data = base64.b64decode(pfx_b64) - private_key, certificate, _ = pkcs12.load_key_and_certificates( - pfx_data, pfx_password.encode() - ) - except Exception as e: - logger.error(f"PFX decode failed for host {host.pk}: {e}") - return False - - if not private_key or not certificate: - return False - - import os - from django.conf import settings - cert_dir = os.path.join(settings.MEDIA_ROOT, 'certs', 'hosts', str(host.pk)) - os.makedirs(cert_dir, exist_ok=True) - - pem_path = os.path.join(cert_dir, 'client.pem') - key_path = os.path.join(cert_dir, 'client.key') - - with open(pem_path, 'wb') as f: - f.write(certificate.public_bytes(Encoding.PEM)) - with open(key_path, 'wb') as f: - f.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())) - - os.chmod(pem_path, 0o600) - os.chmod(key_path, 0o600) - - update_fields = {'cert_pem_path': pem_path, 'cert_key_path': key_path} - if service_user and service_password: - update_fields['username'] = service_user - from utils.crypto import encrypt_value - update_fields['_password'] = encrypt_value(service_password) - - from apps.hosts.models import Host - Host.objects.filter(pk=host.pk).update(**update_fields) - - try: - host.refresh_from_db() - from apps.hosts.tasks import test_winrm_connection - test_winrm_connection.delay(host.pk) - except Exception as e: - logger.warning(f"Failed to dispatch connection test after cert upload: {e}") - - return True - - -@csrf_exempt -@require_http_methods(["POST"]) -def upload_host_cert(request): - try: - data = json.loads(request.body) - token_value = data.get('token', '') - pfx_b64 = data.get('pfx_b64', '') - pfx_password = data.get('pfx_password', '') - service_user = data.get('service_user', '') - service_password = data.get('service_password', '') - - if not token_value or not pfx_b64: - return JsonResponse({'success': False, 'error': 'Missing required fields'}, status=400) - - try: - token_obj = InitialToken.objects.get(token=token_value) - except InitialToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Invalid token'}, status=401) - - if token_obj.host: - ok = _save_cert_to_host( - token_obj.host, pfx_b64, pfx_password, - service_user, service_password, - ) - if not ok: - return JsonResponse({'success': False, 'error': 'Invalid PFX data'}, status=400) - else: - token_obj.cert_data = { - 'pfx_b64': pfx_b64, - 'pfx_password': pfx_password, - 'service_user': service_user, - 'service_password': service_password, - } - token_obj.save(update_fields=['cert_data']) - logger.info(f"Cert data stored on token {token_obj.pk[:8]}, waiting for host association") - - logger.info(f"Cert uploaded for token {token_obj.pk[:8]}") - return JsonResponse({'success': True}) - - except Exception as e: - logger.error(f"upload_host_cert error: {e}", exc_info=True) - return JsonResponse({'success': False, 'error': 'Internal server error'}, status=500) - - -@login_required -@permission_required('hosts.delete_host', raise_exception=True) -def revoke_pending_host(request): - """ - 吊销待验证主机 - 删除InitialToken和关联的Host记录 - """ - try: - data = json.loads(request.body.decode('utf-8')) - token = data.get('token') - - if not token: - return JsonResponse({ - 'success': False, - 'error': 'Token is required' - }, status=400) - - try: - initial_token = InitialToken.objects.get(token=token) - host = initial_token.host - - initial_token.delete() - host.delete() - - return JsonResponse({ - 'success': True, - 'message': 'Host revoked successfully' - }) - - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Token not found' - }, status=404) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error revoking pending host: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to revoke pending host' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -@permission_required('bootstrap.view_initialtoken', raise_exception=True) -def get_pending_hosts(request): - """ - 获取待验证的主机列表 - 返回所有状态为ISSUED的InitialToken - """ - try: - from apps.hosts.models import Host - - current_time = timezone.now() - pending_tokens = InitialToken.objects.filter( - status='ISSUED', - expires_at__gt=current_time - ).select_related('host').order_by('-created_at') - - hosts = [] - for token in pending_tokens: - if token.host: - hosts.append({ - 'token': token.token, - 'hostname': token.host.hostname, - 'host_id': token.host.id, - 'created_at': token.created_at.strftime('%Y-%m-%d %H:%M:%S'), - 'expires_at': token.expires_at.strftime('%Y-%m-%d %H:%M:%S') - }) - - return JsonResponse({ - 'success': True, - 'data': { - 'hosts': hosts - } - }) - - except Exception as e: - logger.error(f"Error in get_pending_hosts: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to get pending hosts' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required('bootstrap.add_initialtoken', raise_exception=True) -def create_initial_token(request): - """创建初始令牌API - 基于配对码的简化认证机制""" - try: - data = json.loads(request.body.decode('utf-8')) - host_id = data.get('host_id') - operator_id = data.get('operator_id') - expire_hours = data.get('expire_hours', 24) - - if not host_id: - return JsonResponse({ - 'success': False, - 'error': 'Host ID is required' - }, status=400) - - if not operator_id: - return JsonResponse({ - 'success': False, - 'error': 'Operator ID is required' - }, status=400) - - try: - host = Host.objects.get(id=host_id) - except Host.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Host not found' - }, status=404) - - from datetime import timedelta - from django.utils import timezone - - token = secrets.token_urlsafe(32) - expires_at = timezone.now() + timedelta(hours=expire_hours) - - initial_token = InitialToken.objects.create( - token=token, - host=host, - expires_at=expires_at, - status='ISSUED' - ) - - pairing_code = initial_token.generate_pairing_code() - - import base64 - config_data = { - 'c_side_url': request.build_absolute_uri('/').rstrip('/'), - 'token': initial_token.token, - 'host_id': str(host.id), - 'expires_at': initial_token.expires_at.isoformat() - } - - config_json = json.dumps(config_data) - encoded_config = base64.b64encode(config_json.encode('utf-8')).decode('utf-8') - - return JsonResponse({ - 'success': True, - 'data': { - 'token': initial_token.token, - 'expires_at': initial_token.expires_at.isoformat(), - 'host_id': host.id, - 'hostname': host.hostname, - 'pairing_code': pairing_code, - 'encoded_config': encoded_config - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error creating initial token: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to create initial token' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_bootstrap_rate_limit('pairing_verify', '5/m') -def verify_pairing_code(request): - """配对码验证接口 - 简化的认证机制 - - 支持两种参数方式: - 1. 通过 host_id 和 pairing_code 验证 - 2. 通过 token 和 pairing_code 验证 - """ - try: - data = json.loads(request.body.decode('utf-8')) - host_id = data.get('host_id') - token = data.get('token') - pairing_code = data.get('pairing_code') - - if not pairing_code: - return JsonResponse({ - 'success': False, - 'error': 'Pairing code is required' - }, status=400) - - if not host_id and not token: - return JsonResponse({ - 'success': False, - 'error': 'Either host_id or token is required' - }, status=400) - - try: - if token: - try: - token_obj = InitialToken.objects.get( - token=token, - status='ISSUED', - expires_at__gt=timezone.now() - ) - - if token_obj.verify_pairing_code(pairing_code): - return JsonResponse({ - 'success': True, - 'message': 'Pairing code verification successful' - }) - else: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired pairing code' - }, status=400) - - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid request' - }, status=400) - else: - initial_tokens = InitialToken.objects.filter( - host_id=host_id, - status='ISSUED', - expires_at__gt=timezone.now() - ) - - if not initial_tokens.exists(): - return JsonResponse({ - 'success': False, - 'error': 'Invalid request' - }, status=400) - - verified = False - for token_obj in initial_tokens: - if token_obj.verify_pairing_code(pairing_code): - verified = True - break - - if verified: - return JsonResponse({ - 'success': True, - 'message': 'Pairing code verification successful' - }) - else: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired pairing code' - }, status=400) - - except Exception as e: - logger.error(f"Error verifying pairing code: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Pairing code verification failed' - }, status=500) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error validating pairing code: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Pairing code validation failed' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -def get_bootstrap_config(request): - """获取主机引导配置API""" - try: - data = json.loads(request.body.decode('utf-8')) - hostname = data.get('hostname') - ip_address = data.get('ip_address') - auth_token = data.get('auth_token') # 认证令牌 - - if not hostname or not auth_token: - return JsonResponse({ - 'success': False, - 'error': 'Hostname and auth_token are required' - }, status=400) - - # 验证初始令牌 - try: - token_obj = InitialToken.objects.get( - token=auth_token, - status='PAIRED', # 确保已经配对验证 - expires_at__gt=timezone.now() - ) - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or unauthorized bootstrap token' - }, status=401) - - # 验证主机是否匹配令牌 - if str(token_obj.host.id) != data.get('host_id', ''): - return JsonResponse({ - 'success': False, - 'error': 'Host ID does not match the token' - }, status=400) - - # 标记令牌为已使用 - token_obj.status = 'CONSUMED' - token_obj.save() - - # 生成活动会话 - session_token = str(uuid.uuid4()) - bound_ip = request.META.get('REMOTE_ADDR', '127.0.0.1') - - ActiveSession.objects.create( - session_token=session_token, - host=token_obj.host, - bound_ip=bound_ip, - expires_at=timezone.now() + timezone.timedelta(days=1) # 24小时有效期 - ) - - # 生成引导配置(异步任务) - from apps.accounts.models import User - admin_user = User.objects.filter(is_superuser=True).first() - operator_id = admin_user.id if admin_user else None - - task_result = generate_bootstrap_config.delay( - hostname=hostname, - ip_address=ip_address or token_obj.host.ip_address, - operator_id=operator_id - ) - - # 等待任务完成(最多等待30秒) - config_result = task_result.get(timeout=30) - - if config_result['success']: - return JsonResponse({ - 'success': True, - 'data': config_result['config'], - 'session_token': session_token # 返回新的会话令牌 - }) - else: - return JsonResponse({ - 'success': False, - 'error': config_result.get('error', 'Failed to generate bootstrap config') - }, status=500) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error getting bootstrap config: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to get bootstrap config' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required('bootstrap.change_initialtoken', raise_exception=True) -def trigger_host_bootstrap(request): - """触发主机引导流程API""" - try: - data = json.loads(request.body.decode('utf-8')) - host_id = data.get('host_id') - operator_id = data.get('operator_id') - - if not host_id: - return JsonResponse({ - 'success': False, - 'error': 'Host ID is required' - }, status=400) - - if not operator_id: - return JsonResponse({ - 'success': False, - 'error': 'Operator ID is required' - }, status=400) - - try: - host = Host.objects.get(id=host_id) - except Host.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Host not found' - }, status=404) - - task_result = initialize_host_bootstrap.delay( - host_id=host_id, - operator_id=operator_id - ) - - return JsonResponse({ - 'success': True, - 'data': { - 'task_id': task_result.id, - 'host_id': host_id, - 'hostname': host.hostname, - 'status': 'started' - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error triggering host bootstrap: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to trigger host bootstrap' - }, status=500) - - -@csrf_exempt -@require_http_methods(["GET"]) -@_bootstrap_rate_limit('bootstrap_status', '10/m') -def check_bootstrap_status(request): - """检查引导状态API""" - try: - token = request.GET.get('token') - host_id = request.GET.get('host_id') - - if not token and not host_id: - return JsonResponse({ - 'success': False, - 'error': 'Either token or host_id is required' - }, status=400) - - if token: - try: - initial_token = InitialToken.objects.get(token=token) - host = initial_token.host - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid token' - }, status=404) - else: - try: - host = Host.objects.get(id=host_id) - except Host.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Host not found' - }, status=404) - - return JsonResponse({ - 'success': True, - 'data': { - 'host_id': host.id, - 'hostname': host.hostname, - 'init_status': host.init_status if hasattr(host, 'init_status') else 'unknown', - 'initialized_at': getattr(host, 'initialized_at', None), - } - }) - - except Exception as e: - logger.error(f"Error checking bootstrap status: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to check bootstrap status' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_bootstrap_rate_limit('token_validate', '10/m') -def validate_bootstrap_token(request): - """验证引导令牌有效性""" - try: - data = json.loads(request.body.decode('utf-8')) - token = data.get('token') - - if not token: - return JsonResponse({ - 'success': False, - 'error': 'Token is required' - }, status=400) - - try: - token_obj = InitialToken.objects.get( - token=token, - status__in=['ISSUED', 'PAIRED'], - expires_at__gt=timezone.now() - ) - - return JsonResponse({ - 'success': True, - 'data': { - 'valid': True, - 'expires_at': token_obj.expires_at.isoformat(), - 'status': token_obj.status - } - }) - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired token' - }, status=401) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error validating bootstrap token: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Token validation failed' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_bootstrap_rate_limit('session_token', '10/m') -def get_session_token(request): - """获取会话令牌接口 - H端初始化流程的第一步""" - try: - client_ip = get_client_ip(request) - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - - if not auth_header.startswith('Bearer '): - return JsonResponse({ - 'success': False, - 'error': 'Authorization header missing or invalid' - }, status=401) - - initial_token = auth_header.split(' ')[1] - - try: - token_obj = InitialToken.objects.get( - token=initial_token, - status__in=['ISSUED', 'PAIRED'], - expires_at__gt=timezone.now() - ) - except InitialToken.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired initial token' - }, status=401) - - # 获取真实客户端IP - ip = client_ip - - # 原子操作:生成新的session_token,创建ActiveSession记录 - from django.db import transaction - with transaction.atomic(): - session_token = str(uuid.uuid4()) - - if token_obj.host: - ActiveSession.objects.create( - session_token=session_token, - host=token_obj.host, - bound_ip=ip, - expires_at=timezone.now() + timezone.timedelta(hours=1) - ) - - token_obj.status = 'CONSUMED' - token_obj.save() - - return JsonResponse({ - 'success': True, - 'session_token': session_token, - 'expires_in': 3600, - }) - - except Exception as e: - logger.error(f"Error creating session token: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to create session token' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_bootstrap_rate_limit('exchange_token', '10/m') -def exchange_token(request): - """令牌交换接口 - 根据规范""" - try: - client_ip = get_client_ip(request) - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - - if not auth_header.startswith('Bearer '): - return JsonResponse({ - 'success': False, - 'error': 'Authorization header missing or invalid' - }, status=401) - - session_token = auth_header.split(' ')[1] - - try: - active_session = ActiveSession.objects.get( - session_token=session_token, - expires_at__gt=timezone.now() - ) - except ActiveSession.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired session token' - }, status=401) - - # 验证IP绑定 - current_ip = client_ip - if active_session.bound_ip != current_ip: - return JsonResponse({ - 'success': False, - 'error': 'IP address mismatch' - }, status=403) - - # 延长会话有效期 - from django.db import transaction - with transaction.atomic(): - active_session.expires_at = timezone.now() + timezone.timedelta(days=7) # 延长到7天 - active_session.save() - - return JsonResponse({ - 'success': True, - 'session_token': session_token, - 'expires_in': 604800, - }) - - except Exception as e: - logger.error(f"Error exchanging token: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Token exchange failed' - }, status=500) - - -@csrf_exempt -@require_http_methods(["GET"]) -@_bootstrap_rate_limit('pairing_status', '10/m') -def check_pairing_status(request): - """检查配对状态接口""" - try: - # 从Authorization头获取InitialToken - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if not auth_header.startswith('Bearer '): - return JsonResponse({ - 'paired': False, - 'message': 'Invalid authorization header' - }, status=401) - - initial_token = auth_header.split(' ')[1] - - # 查找对应的初始令牌 - try: - token_obj = InitialToken.objects.get( - token=initial_token, - expires_at__gt=timezone.now() - ) - - if token_obj.status == 'PAIRED': - return JsonResponse({ - 'paired': True, - 'message': 'Pairing completed', - }) - elif token_obj.status == 'ISSUED': - return JsonResponse({ - 'paired': False, - 'message': 'Waiting for pairing code verification', - }) - elif token_obj.status == 'CONSUMED': - return JsonResponse({ - 'paired': True, - 'message': 'Token already consumed', - }) - else: - return JsonResponse({ - 'paired': False, - 'message': f'Token status: {token_obj.status}' - }) - - except InitialToken.DoesNotExist: - return JsonResponse({ - 'paired': False, - 'message': 'Invalid or expired token' - }, status=404) - - except Exception as e: - logger.error(f"Error checking pairing status: {str(e)}", exc_info=True) - return JsonResponse({ - 'paired': False, - 'message': 'Internal error' - }, status=500) - - -@csrf_exempt -@require_http_methods(["DELETE"]) -def revoke_session(request): - """吊销会话接口 - 根据规范""" - try: - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if not auth_header.startswith('Bearer '): - return JsonResponse({ - 'success': False, - 'error': 'Authorization header missing or invalid' - }, status=401) - - session_token = auth_header.split(' ')[1] - - # 删除ActiveSession表中的对应记录 - try: - session = ActiveSession.objects.get(session_token=session_token) - session.delete() - - return JsonResponse({ - 'success': True, - 'message': 'Session revoked successfully' - }) - except ActiveSession.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid session token' - }, status=401) - - except Exception as e: - logger.error(f"Error revoking session: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to revoke session' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required('hosts.add_host', raise_exception=True) -def complete_auto_register(request): - """ - 完成自动注册 - h_side_init.exe验证成功后调用此API创建主机记录 - """ - try: - import json - from apps.hosts.models import Host - - data = json.loads(request.body.decode('utf-8')) - - token = data.get('token', '') - hostname = data.get('hostname', '') - - if not token or not hostname: - return JsonResponse({ - 'success': False, - 'error': 'token and hostname are required' - }, status=400) - - # 检查是否已存在同名主机 - existing_host = Host.objects.filter(hostname=hostname).first() - if existing_host: - host = existing_host - host.status = 'offline' - host.save() - logger.info(f"主机 {hostname} 已存在,更新状态") - else: - # 创建新主机 - host = Host.objects.create( - name=hostname, - hostname=hostname, - connection_type='tunnel', - username='placeholder', - password='placeholder', - status='offline', - description='自动注册主机' - ) - logger.info(f"自动创建新主机: {hostname}") - - # 创建InitialToken - from datetime import timedelta - import secrets - - expires_at = timezone.now() + timedelta(hours=24) - initial_token = InitialToken.objects.create( - token=token, - host=host, - expires_at=expires_at, - status='ISSUED' - ) - - # 生成配对码 - pairing_code = initial_token.generate_pairing_code() - - return JsonResponse({ - 'success': True, - 'data': { - 'host_id': host.id, - 'hostname': host.hostname, - 'pairing_code': pairing_code, - 'token': initial_token.token - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error in complete_auto_register: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to complete auto register' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required('hosts.add_host', raise_exception=True) -def auto_register_host(request): - """ - 自动注册主机接口 - 只生成预注册token,不创建主机记录 - 主机记录在h_side_init.exe完成验证后创建 - """ - try: - import json - import secrets - from datetime import timedelta - - data = json.loads(request.body.decode('utf-8')) - hostname = data.get('hostname', '') - - # 验证必填字段 - if not hostname: - return JsonResponse({ - 'success': False, - 'error': 'hostname is required' - }, status=400) - - # 生成预注册token(不创建主机记录) - token = secrets.token_urlsafe(32) - expires_at = timezone.now() + timedelta(hours=24) - - # 构建配置数据(供h_side_init.exe使用) - current_site = request.build_absolute_uri('/').rstrip('/') - secret_data = { - "c_side_url": current_site, - "token": token, - "hostname": hostname, - "generated_at": timezone.now().isoformat(), - "expires_at": expires_at.isoformat(), - "auto_register": True # 标记为自动注册模式 - } - - import base64 - json_str = json.dumps(secret_data, ensure_ascii=False) - encoded_bytes = base64.b64encode(json_str.encode('utf-8')) - encoded_str = encoded_bytes.decode('utf-8') - - # 生成 PowerShell 脚本(下载并运行) - download_url = "https://2c2a.cc.cd/2c2a/HostInitBash/releases/latest/download/h_side_init.exe" - script = f'''$exe = "$env:TEMP\\h_side_init.exe" -Invoke-WebRequest -Uri "{download_url}" -OutFile $exe -UseBasicParsing -& $exe "{encoded_str}"''' - - return JsonResponse({ - 'success': True, - 'data': { - 'script': script, - 'secret': encoded_str - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error in auto_register_host: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to generate register script' - }, status=500) - - -@login_required -def sse_init_status(request): - import json as _json - - token = request.GET.get('token', '') - if not token: - return JsonResponse( - {'error': 'token required'}, status=400, - ) - - def event_stream(): - for _ in range(120): - try: - token_obj = InitialToken.objects.get(token=token) - status = token_obj.status - data = { - 'status': status, - 'host_id': ( - token_obj.host_id - if token_obj.host_id else None - ), - 'cert_uploaded': bool(token_obj.cert_data), - } - if status == 'CONSUMED' and token_obj.host: - host_status = Host.objects.filter( - pk=token_obj.host_id - ).values_list('status', flat=True).first() - data['host_status'] = host_status - if host_status == 'online': - yield f"data: {_json.dumps(data)}\n\n" - return - if status == 'CONSUMED' and token_obj.cert_data and not token_obj.host: - yield f"data: {_json.dumps(data)}\n\n" - return - yield f"data: {_json.dumps(data)}\n\n" - if status == 'CONSUMED': - for _ in range(24): - time.sleep(5) - token_obj.refresh_from_db() - if token_obj.cert_data and not token_obj.host: - data['cert_uploaded'] = True - yield f"data: {_json.dumps(data)}\n\n" - return - if token_obj.host: - host_status = Host.objects.filter( - pk=token_obj.host_id - ).values_list('status', flat=True).first() - data['host_status'] = host_status - if host_status == 'online': - yield f"data: {_json.dumps(data)}\n\n" - return - return - except InitialToken.DoesNotExist: - yield f"data: {_json.dumps({'status': 'NOT_FOUND'})}\n\n" - return - time.sleep(5) - yield f"data: {_json.dumps({'status': 'TIMEOUT'})}\n\n" - - response = StreamingHttpResponse( - event_stream(), - content_type='text/event-stream', - ) - response['Cache-Control'] = 'no-cache' - response['X-Accel-Buffering'] = 'no' - return response - - -def get_client_ip(request): - """获取客户端真实IP地址""" - from django.conf import settings as django_settings - if getattr(django_settings, 'USE_X_FORWARDED_FOR', False): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - return x_forwarded_for.split(',')[0].strip() - return request.META.get('REMOTE_ADDR', '127.0.0.1') - - -class BootstrapManagementView(View): - """引导管理视图 - 需要管理员权限""" - - @method_decorator(permission_required('bootstrap.view_initialtoken')) - def get(self, request): - """获取引导令牌列表""" - try: - page = int(request.GET.get('page', 1)) - page_size = min(int(request.GET.get('page_size', 20)), 100) # 最大100条每页 - status_filter = request.GET.get('status') # issued, paired, consumed, all - - queryset = InitialToken.objects.select_related('host').all() - - # 状态过滤 - if status_filter == 'issued': - queryset = queryset.filter(status='ISSUED') - elif status_filter == 'paired': - queryset = queryset.filter(status='PAIRED') - elif status_filter == 'consumed': - queryset = queryset.filter(status='CONSUMED') - elif status_filter == 'expired': - queryset = queryset.filter(expires_at__lt=timezone.now()) - elif status_filter != 'all': - # 默认显示未过期的 - queryset = queryset.filter(expires_at__gt=timezone.now()) - - # 分页 - start_idx = (page - 1) * page_size - end_idx = start_idx + page_size - tokens = queryset[start_idx:end_idx] - - total_count = queryset.count() - - result = { - 'success': True, - 'data': { - 'tokens': [ - { - 'id': token.token, - 'token': token.token, - 'hostname': token.host.hostname, - 'host_id': token.host.id, - 'created_at': token.created_at.isoformat(), - 'expires_at': token.expires_at.isoformat(), - 'status': token.status, - 'is_expired': token.expires_at < timezone.now() - } - for token in tokens - ], - 'pagination': { - 'page': page, - 'page_size': page_size, - 'total_count': total_count, - 'total_pages': (total_count + page_size - 1) // page_size - } - } - } - - return JsonResponse(result) - - except ValueError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid page or page_size parameter' - }, status=400) - except Exception as e: - logger.error(f"Error getting bootstrap tokens: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve bootstrap tokens' - }, status=500) - - @method_decorator(permission_required('bootstrap.delete_initialtoken')) - def delete(self, request): - """删除引导令牌""" - try: - token_id = request.GET.get('id') - - if not token_id: - return JsonResponse({ - 'success': False, - 'error': 'Token ID is required' - }, status=400) - - token = get_object_or_404(InitialToken, token=token_id) - token.delete() - - return JsonResponse({ - 'success': True, - 'message': 'Initial token deleted successfully' - }) - - except Exception as e: - logger.error(f"Error deleting initial token: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to delete initial token' - }, status=500) - - -@csrf_exempt -@require_http_methods(["GET"]) -def cert_provision_validate(request): - token_str = request.GET.get('token', '') - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - return JsonResponse({ - 'valid': provision_token.is_valid(), - 'server_host': provision_token.server_host, - 'status': provision_token.status, - }) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'valid': False, 'server_host': '', 'status': ''}) - - -@csrf_exempt -@require_http_methods(["POST"]) -def cert_provision_upload_hostname(request): - try: - data = json.loads(request.body) - token_str = data.get('token', '') - hostname = data.get('hostname', '') - except (json.JSONDecodeError, AttributeError): - return JsonResponse({'success': False, 'error': 'Invalid request'}, status=400) - - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Token not found'}, status=404) - - if not provision_token.is_valid(): - return JsonResponse({'success': False, 'error': 'Token expired'}, status=403) - - host = provision_token.host - if host: - Host.objects.filter(pk=host.pk).update(hostname=hostname) - host.refresh_from_db() - else: - provision_token.hostname = hostname - - provision_token.status = 'HOSTNAME_UPLOADED' - provision_token.save() - - from apps.bootstrap.tasks import cert_provision_issue_certs - cert_provision_issue_certs.delay(token_str) - - return JsonResponse({'success': True}) - - -@csrf_exempt -@require_http_methods(["GET"]) -def cert_provision_download_certs(request): - token_str = request.GET.get('token', '') - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Token not found'}, status=404) - - if provision_token.status != 'CERT_ISSUED': - return JsonResponse({'success': False, 'error': 'Certificates not ready'}, status=400) - - host = provision_token.host - if host and host.cert_root and host.cert_sub: - from utils.cert_storage import get_cert_file_paths - paths = get_cert_file_paths(host.cert_root, host.cert_sub) - - ca_cert_b64 = base64.b64encode(paths['ca_cert'].read_bytes()).decode('utf-8') - client_cert_b64 = base64.b64encode(paths['client_cert'].read_bytes()).decode('utf-8') - server_pfx_b64 = base64.b64encode(paths['server_pfx'].read_bytes()).decode('utf-8') - - return JsonResponse({ - 'success': True, - 'ca_cert': ca_cert_b64, - 'client_cert': client_cert_b64, - 'server_pfx': server_pfx_b64, - 'pfx_password': host.pfx_password, - 'ntlm_user': host.ntlm_fallback_user, - 'ntlm_password': host.ntlm_fallback_password, - 'upn_value': f"{host.ntlm_fallback_user}@localhost", - }) - elif provision_token.cert_data: - cd = provision_token.cert_data - return JsonResponse({ - 'success': True, - 'ca_cert': cd.get('ca_cert_b64', ''), - 'client_cert': cd.get('client_cert_b64', ''), - 'server_pfx': cd.get('server_pfx_b64', ''), - 'pfx_password': cd.get('pfx_password', ''), - 'ntlm_user': cd.get('ntlm_user', ''), - 'ntlm_password': cd.get('ntlm_password', ''), - 'upn_value': f"{cd.get('ntlm_user', '')}@localhost", - }) - - return JsonResponse({'success': False, 'error': 'Host not configured'}, status=400) - - -@csrf_exempt -@require_http_methods(["POST"]) -def cert_provision_notify_complete(request): - try: - data = json.loads(request.body) - token_str = data.get('token', '') - except (json.JSONDecodeError, AttributeError): - return JsonResponse({'success': False, 'error': 'Invalid request'}, status=400) - - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Token not found'}, status=404) - - provision_token.status = 'HOST_CONFIGURED' - provision_token.save() - - host = provision_token.host - if host: - from apps.hosts.tasks import test_winrm_connection - use_cert = host.auth_method == 'certificate' - test_winrm_connection.delay(host.pk, use_certificate_auth=use_cert) - return JsonResponse({'success': True, 'test': 'started'}) - else: - return JsonResponse({'success': True, 'test': 'deferred'}) - - -@csrf_exempt -@require_http_methods(["POST"]) -def cert_provision_disable_password_auth(request): - try: - data = json.loads(request.body) - token_str = data.get('token', '') - except (json.JSONDecodeError, AttributeError): - return JsonResponse({'success': False, 'error': 'Invalid request'}, status=400) - - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Token not found'}, status=404) - - provision_token.status = 'CONSUMED' - provision_token.consumed_at = timezone.now() - provision_token.save() - - return JsonResponse({'success': True}) - - -@csrf_exempt -@require_http_methods(["GET"]) -def cert_provision_test_result(request): - token_str = request.GET.get('token', '') - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - return JsonResponse({'success': False, 'error': 'Token not found'}, status=404) - - host = provision_token.host - if not host: - return JsonResponse({'status': 'testing'}) - - if host.cert_provision_status == 'configured': - return JsonResponse({'status': 'success'}) - elif host.cert_provision_status == 'failed': - return JsonResponse({'status': 'failed', 'error': 'Connection test failed'}) - else: - return JsonResponse({'status': 'testing'}) - - -@csrf_exempt -@require_http_methods(["GET"]) -def cert_provision_status_stream(request): - token_str = request.GET.get('token', '') - - def event_stream(): - for _ in range(120): - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - yield f"data: {json.dumps({'status': 'failed', 'error': 'Token not found'})}\n\n" - return - - if provision_token.status == 'CERT_ISSUED': - host = provision_token.host - yield f"data: {json.dumps({'status': 'ready'})}\n\n" - return - elif provision_token.status == 'HOST_CONFIGURED': - yield f"data: {json.dumps({'status': 'configured'})}\n\n" - return - elif provision_token.is_expired(): - yield f"data: {json.dumps({'status': 'failed', 'error': 'Token expired'})}\n\n" - return - - yield f"data: {json.dumps({'status': 'pending'})}\n\n" - time.sleep(5) - - yield f"data: {json.dumps({'status': 'failed', 'error': 'Timeout'})}\n\n" - - response = StreamingHttpResponse(event_stream(), content_type='text/event-stream') - response['Cache-Control'] = 'no-cache' - response['X-Accel-Buffering'] = 'no' - return response - - -@csrf_exempt -@require_http_methods(["GET"]) -def cert_provision_test_stream(request): - token_str = request.GET.get('token', '') - - def event_stream(): - for _ in range(60): - try: - provision_token = CertProvisionToken.objects.get(token=token_str) - except CertProvisionToken.DoesNotExist: - yield f"data: {json.dumps({'status': 'failed', 'error': 'Token not found'})}\n\n" - return - - host = provision_token.host - if host: - if host.cert_provision_status == 'configured': - yield f"data: {json.dumps({'status': 'success'})}\n\n" - return - elif host.cert_provision_status == 'failed': - yield f"data: {json.dumps({'status': 'failed', 'error': 'Connection test failed'})}\n\n" - return - - yield f"data: {json.dumps({'status': 'testing'})}\n\n" - time.sleep(5) - - yield f"data: {json.dumps({'status': 'failed', 'error': 'Timeout'})}\n\n" - - response = StreamingHttpResponse(event_stream(), content_type='text/event-stream') - response['Cache-Control'] = 'no-cache' - response['X-Accel-Buffering'] = 'no' - return response \ No newline at end of file diff --git a/apps/certificates/apps.py b/apps/certificates/apps.py deleted file mode 100755 index 8f4d391..0000000 --- a/apps/certificates/apps.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging - -from django.apps import AppConfig - - -logger = logging.getLogger(__name__) - - -class CertificatesConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.certificates" - verbose_name = "证书管理系统" - - def ready(self): - import apps.certificates.signals - - self._ensure_ca_exists() - - def _ensure_ca_exists(self): - import os - - if os.environ.get("RUN_MAIN") == "true": - return - if os.environ.get("DJANGO_AUTORELOAD") == "true": - return - try: - CertificateAuthority = self.get_model("CertificateAuthority") - if not CertificateAuthority.objects.filter(is_active=True).exists(): - from utils.cert_service import generate_ca - from cryptography.hazmat.primitives import serialization - import datetime - - ca_key, ca_cert = generate_ca() - ca_key_pem = ca_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - ca_cert_pem = ca_cert.public_bytes( - serialization.Encoding.PEM, - ) - - ca, created = CertificateAuthority.objects.get_or_create( - name="WinRM-CA", - defaults={"is_active": True}, - ) - if created: - ca.save_ca_files(ca_key_pem, ca_cert_pem) - ca.expires_at = datetime.datetime.now( - datetime.timezone.utc - ) + datetime.timedelta(days=3650) - ca.save() - except Exception: - logger.exception( - "Failed to ensure default certificate authority exists during app startup." - ) diff --git a/apps/certificates/migrations/0001_initial.py b/apps/certificates/migrations/0001_initial.py deleted file mode 100755 index bb1244d..0000000 --- a/apps/certificates/migrations/0001_initial.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 11:58 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CertificateAuthority', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True, verbose_name='CA名称')), - ('private_key', models.TextField(verbose_name='私钥(加密存储)')), - ('certificate', models.TextField(verbose_name='CA证书(PEM格式)')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('expires_at', models.DateTimeField(verbose_name='过期时间')), - ('is_active', models.BooleanField(default=True, verbose_name='是否激活')), - ('description', models.TextField(blank=True, null=True, verbose_name='描述')), - ], - options={ - 'verbose_name': '证书颁发机构', - 'verbose_name_plural': '证书颁发机构', - 'db_table': 'certificate_authority', - }, - ), - migrations.CreateModel( - name='ServerCertificate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hostname', models.CharField(max_length=255, unique=True, verbose_name='主机名')), - ('private_key', models.TextField(verbose_name='私钥(加密存储)')), - ('certificate', models.TextField(verbose_name='服务器证书(PEM格式)')), - ('pfx_data', models.TextField(verbose_name='PFX数据(Base64编码)')), - ('thumbprint', models.CharField(max_length=255, unique=True, verbose_name='证书指纹(SHA1)')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('expires_at', models.DateTimeField(verbose_name='过期时间')), - ('is_revoked', models.BooleanField(default=False, verbose_name='是否已吊销')), - ('revocation_reason', models.CharField(blank=True, max_length=255, null=True, verbose_name='吊销原因')), - ('revocation_date', models.DateTimeField(blank=True, null=True, verbose_name='吊销时间')), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='certificates.certificateauthority', verbose_name='所属CA')), - ], - options={ - 'verbose_name': '服务器证书', - 'verbose_name_plural': '服务器证书', - 'db_table': 'server_certificate', - }, - ), - migrations.CreateModel( - name='ClientCertificate', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='证书名称')), - ('private_key', models.TextField(verbose_name='私钥(加密存储)')), - ('certificate', models.TextField(verbose_name='客户端证书(PEM格式)')), - ('thumbprint', models.CharField(max_length=255, unique=True, verbose_name='证书指纹(SHA1)')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('expires_at', models.DateTimeField(verbose_name='过期时间')), - ('is_active', models.BooleanField(default=True, verbose_name='是否激活')), - ('description', models.TextField(blank=True, null=True, verbose_name='描述')), - ('assigned_to_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='分配给用户')), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='certificates.certificateauthority', verbose_name='所属CA')), - ], - options={ - 'verbose_name': '客户端证书', - 'verbose_name_plural': '客户端证书', - 'db_table': 'client_certificate', - }, - ), - ] diff --git a/apps/certificates/migrations/0002_alter_certificateauthority_expires_at_and_more.py b/apps/certificates/migrations/0002_alter_certificateauthority_expires_at_and_more.py deleted file mode 100755 index 7f43963..0000000 --- a/apps/certificates/migrations/0002_alter_certificateauthority_expires_at_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 12:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('certificates', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='certificateauthority', - name='expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='过期时间'), - ), - migrations.AlterField( - model_name='clientcertificate', - name='expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='过期时间'), - ), - migrations.AlterField( - model_name='servercertificate', - name='expires_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='过期时间'), - ), - ] diff --git a/apps/certificates/migrations/0003_remove_certificateauthority_private_key_and_more.py b/apps/certificates/migrations/0003_remove_certificateauthority_private_key_and_more.py deleted file mode 100644 index 3e192ed..0000000 --- a/apps/certificates/migrations/0003_remove_certificateauthority_private_key_and_more.py +++ /dev/null @@ -1,161 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 15:15 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('certificates', '0002_alter_certificateauthority_expires_at_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='certificateauthority', - name='private_key', - ), - migrations.RemoveField( - model_name='clientcertificate', - name='private_key', - ), - migrations.RemoveField( - model_name='servercertificate', - name='private_key', - ), - migrations.AddField( - model_name='certificateauthority', - name='_private_key', - field=models.TextField(db_column='private_key', default=1, verbose_name='私钥(加密)'), - preserve_default=False, - ), - migrations.AddField( - model_name='clientcertificate', - name='_private_key', - field=models.TextField(db_column='private_key', default=1, verbose_name='私钥(加密)'), - preserve_default=False, - ), - migrations.AddField( - model_name='servercertificate', - name='_private_key', - field=models.TextField(db_column='private_key', default=1, verbose_name='私钥(加密)'), - preserve_default=False, - ), - migrations.AlterField( - model_name='certificateauthority', - name='certificate', - field=models.TextField(verbose_name='CA证书(PEM)'), - ), - migrations.AlterField( - model_name='certificateauthority', - name='created_at', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='certificateauthority', - name='description', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='certificateauthority', - name='expires_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='certificateauthority', - name='is_active', - field=models.BooleanField(default=True), - ), - migrations.AlterField( - model_name='clientcertificate', - name='assigned_to_user', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='clientcertificate', - name='ca', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='certificates.certificateauthority'), - ), - migrations.AlterField( - model_name='clientcertificate', - name='certificate', - field=models.TextField(verbose_name='证书(PEM)'), - ), - migrations.AlterField( - model_name='clientcertificate', - name='created_at', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='clientcertificate', - name='description', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='clientcertificate', - name='expires_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='clientcertificate', - name='is_active', - field=models.BooleanField(default=True), - ), - migrations.AlterField( - model_name='clientcertificate', - name='name', - field=models.CharField(max_length=255), - ), - migrations.AlterField( - model_name='clientcertificate', - name='thumbprint', - field=models.CharField(max_length=255, unique=True), - ), - migrations.AlterField( - model_name='servercertificate', - name='ca', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='certificates.certificateauthority'), - ), - migrations.AlterField( - model_name='servercertificate', - name='certificate', - field=models.TextField(verbose_name='证书(PEM)'), - ), - migrations.AlterField( - model_name='servercertificate', - name='created_at', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='servercertificate', - name='expires_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='servercertificate', - name='is_revoked', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='servercertificate', - name='pfx_data', - field=models.TextField(verbose_name='PFX(Base64)'), - ), - migrations.AlterField( - model_name='servercertificate', - name='revocation_date', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='servercertificate', - name='revocation_reason', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AlterField( - model_name='servercertificate', - name='thumbprint', - field=models.CharField(max_length=255, unique=True), - ), - ] diff --git a/apps/certificates/migrations/0004_migrate_to_ecc_p256.py b/apps/certificates/migrations/0004_migrate_to_ecc_p256.py deleted file mode 100644 index 4b05436..0000000 --- a/apps/certificates/migrations/0004_migrate_to_ecc_p256.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-29 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('certificates', '0003_remove_certificateauthority_private_key_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='clientcertificate', - name='upn_value', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='UPN值'), - ), - migrations.AddField( - model_name='servercertificate', - name='_pfx_password', - field=models.CharField(blank=True, db_column='pfx_password', default='', max_length=255, verbose_name='PFX密码(加密)'), - ), - migrations.AddField( - model_name='servercertificate', - name='ip_address', - field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP地址'), - ), - ] diff --git a/apps/certificates/migrations/0005_remove_cert_content_from_db.py b/apps/certificates/migrations/0005_remove_cert_content_from_db.py deleted file mode 100644 index 63cd317..0000000 --- a/apps/certificates/migrations/0005_remove_cert_content_from_db.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-30 07:54 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('certificates', '0004_migrate_to_ecc_p256'), - ] - - operations = [ - migrations.RemoveField( - model_name='clientcertificate', - name='_private_key', - ), - migrations.RemoveField( - model_name='clientcertificate', - name='certificate', - ), - migrations.RemoveField( - model_name='servercertificate', - name='_pfx_password', - ), - migrations.RemoveField( - model_name='servercertificate', - name='_private_key', - ), - migrations.RemoveField( - model_name='servercertificate', - name='certificate', - ), - migrations.RemoveField( - model_name='servercertificate', - name='pfx_data', - ), - ] diff --git a/apps/certificates/migrations/0006_remove_certificateauthority__private_key_and_more.py b/apps/certificates/migrations/0006_remove_certificateauthority__private_key_and_more.py deleted file mode 100644 index de31648..0000000 --- a/apps/certificates/migrations/0006_remove_certificateauthority__private_key_and_more.py +++ /dev/null @@ -1,91 +0,0 @@ -import logging - -from django.db import migrations, models - -logger = logging.getLogger(__name__) - - -def migrate_ca_to_files(apps, schema_editor): - CertificateAuthority = apps.get_model('certificates', 'CertificateAuthority') - import secrets - import os - from django.conf import settings - from pathlib import Path - - ca_base_dir = Path(settings.MEDIA_ROOT) / 'certificates' / 'ca' - - for ca in CertificateAuthority.objects.all(): - raw_key = ca.__dict__.get('_private_key') or ca.__dict__.get('private_key') - raw_cert = ca.__dict__.get('certificate') - - if not raw_key or not raw_cert: - ca.is_active = False - ca.cert_dir = '' - ca.save() - logger.warning( - f"CA {ca.name} has no key/cert data, marked inactive" - ) - continue - - try: - import base64 - import hashlib - from cryptography.fernet import Fernet, InvalidToken - - key = hashlib.sha256(settings.SECRET_KEY.encode()).digest() - fernet = Fernet(base64.urlsafe_b64encode(key)) - decrypted_key = fernet.decrypt(raw_key.encode()).decode() - - cert_dir_name = secrets.token_hex(8) - ca_dir = ca_base_dir / cert_dir_name - ca_dir.mkdir(parents=True, exist_ok=True) - - key_path = ca_dir / 'ca.key' - key_path.write_text(decrypted_key, encoding='utf-8') - os.chmod(key_path, 0o600) - - cert_path = ca_dir / 'ca.crt' - cert_path.write_text(raw_cert, encoding='utf-8') - os.chmod(cert_path, 0o600) - - ca.cert_dir = cert_dir_name - ca.save() - logger.info(f"CA {ca.name} migrated to files at {cert_dir_name}") - except (InvalidToken, Exception) as e: - ca.is_active = False - ca.cert_dir = '' - ca.save() - logger.warning( - f"CA {ca.name} key decryption failed ({e}), " - f"marked inactive for re-creation" - ) - - -def reverse_migrate(apps, schema_editor): - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ("certificates", "0005_remove_cert_content_from_db"), - ] - - operations = [ - migrations.AddField( - model_name="certificateauthority", - name="cert_dir", - field=models.CharField( - blank=True, default="", max_length=64, verbose_name="证书目录" - ), - ), - migrations.RunPython(migrate_ca_to_files, reverse_migrate), - migrations.RemoveField( - model_name="certificateauthority", - name="_private_key", - ), - migrations.RemoveField( - model_name="certificateauthority", - name="certificate", - ), - ] diff --git a/apps/certificates/migrations/0007_cert_dir_to_cert_root_sub.py b/apps/certificates/migrations/0007_cert_dir_to_cert_root_sub.py deleted file mode 100644 index 44e00b8..0000000 --- a/apps/certificates/migrations/0007_cert_dir_to_cert_root_sub.py +++ /dev/null @@ -1,136 +0,0 @@ -import logging -import os -import shutil -import secrets - -from django.db import migrations, models -from pathlib import Path -from django.conf import settings - -logger = logging.getLogger(__name__) - - -def migrate_cert_dir_to_root_sub(apps, schema_editor): - CertificateAuthority = apps.get_model( - 'certificates', 'CertificateAuthority' - ) - ca_base_dir = Path(settings.MEDIA_ROOT) / 'certificates' / 'ca' - - for ca in CertificateAuthority.objects.all(): - old_dir_name = ca.cert_dir - if not old_dir_name: - ca.cert_root = '' - ca.cert_sub = '' - ca.save() - continue - - old_dir = ca_base_dir / old_dir_name - if not old_dir.exists(): - logger.warning( - f"CA {ca.name}: old dir {old_dir} not found, " - f"generating new paths" - ) - ca.cert_root = '' - ca.cert_sub = '' - ca.save() - continue - - cert_root = secrets.token_hex(1) - cert_sub = secrets.token_hex(1) - new_dir = ca_base_dir / cert_root / cert_sub - new_dir.mkdir(parents=True, exist_ok=True) - - for fname in os.listdir(old_dir): - src = old_dir / fname - dst = new_dir / fname - shutil.move(str(src), str(dst)) - - shutil.rmtree(old_dir) - try: - old_dir.parent.rmdir() - except OSError as exc: - logger.debug( - f"CA {ca.name}: could not remove parent dir {old_dir.parent}: {exc}" - ) - - ca.cert_root = cert_root - ca.cert_sub = cert_sub - ca.save() - logger.info( - f"CA {ca.name}: moved from {old_dir_name} " - f"to {cert_root}/{cert_sub}" - ) - - -def reverse_migrate(apps, schema_editor): - CertificateAuthority = apps.get_model( - 'certificates', 'CertificateAuthority' - ) - ca_base_dir = Path(settings.MEDIA_ROOT) / 'certificates' / 'ca' - - for ca in CertificateAuthority.objects.all(): - if not ca.cert_root or not ca.cert_sub: - ca.cert_dir = '' - ca.save() - continue - - new_dir_name = secrets.token_hex(8) - new_dir = ca_base_dir / new_dir_name - old_dir = ca_base_dir / ca.cert_root / ca.cert_sub - - if old_dir.exists(): - new_dir.mkdir(parents=True, exist_ok=True) - for fname in os.listdir(old_dir): - src = old_dir / fname - dst = new_dir / fname - shutil.move(str(src), str(dst)) - shutil.rmtree(old_dir) - try: - old_dir.parent.rmdir() - except OSError as exc: - logger.debug( - f"CA {ca.name}: could not remove parent dir {old_dir.parent}: {exc}" - ) - - ca.cert_dir = new_dir_name - ca.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "certificates", - "0006_remove_certificateauthority__private_key_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="certificateauthority", - name="cert_root", - field=models.CharField( - blank=True, - default="", - max_length=2, - verbose_name="证书存储根路径", - ), - ), - migrations.AddField( - model_name="certificateauthority", - name="cert_sub", - field=models.CharField( - blank=True, - default="", - max_length=2, - verbose_name="证书存储子路径", - ), - ), - migrations.RunPython( - migrate_cert_dir_to_root_sub, reverse_migrate - ), - migrations.RemoveField( - model_name="certificateauthority", - name="cert_dir", - ), - ] diff --git a/apps/certificates/migrations/__init__.py b/apps/certificates/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/certificates/models.py b/apps/certificates/models.py deleted file mode 100755 index 7630d17..0000000 --- a/apps/certificates/models.py +++ /dev/null @@ -1,108 +0,0 @@ -from django.db import models -import datetime - -from utils.cert_storage import ( - get_ca_file_paths, - save_ca_files, - generate_ca_paths, -) - - -class CertificateAuthority(models.Model): - name = models.CharField(max_length=255, unique=True, verbose_name="CA名称") - cert_root = models.CharField( - max_length=2, default="", blank=True, verbose_name="证书存储根路径" - ) - cert_sub = models.CharField( - max_length=2, default="", blank=True, verbose_name="证书存储子路径" - ) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField(null=True, blank=True) - is_active = models.BooleanField(default=True) - description = models.TextField(blank=True, null=True) - - class Meta: - verbose_name = "证书颁发机构" - verbose_name_plural = "证书颁发机构" - db_table = "certificate_authority" - - @property - def private_key(self): - paths = get_ca_file_paths(self.cert_root, self.cert_sub) - key_path = paths["key"] - if not key_path.exists(): - return None - return key_path.read_text(encoding="utf-8") - - @property - def certificate(self): - paths = get_ca_file_paths(self.cert_root, self.cert_sub) - cert_path = paths["cert"] - if not cert_path.exists(): - return None - return cert_path.read_text(encoding="utf-8") - - def save_ca_files(self, ca_key_pem: bytes, ca_cert_pem: bytes): - if not self.cert_root or not self.cert_sub: - self.cert_root, self.cert_sub = generate_ca_paths() - save_ca_files(self.cert_root, self.cert_sub, ca_key_pem, ca_cert_pem) - - def __str__(self): - return f"CA: {self.name}" - - -class ServerCertificate(models.Model): - hostname = models.CharField(max_length=255, unique=True, verbose_name="主机名") - ip_address = models.GenericIPAddressField( - null=True, blank=True, verbose_name="IP地址" - ) - ca = models.ForeignKey(CertificateAuthority, on_delete=models.CASCADE) - thumbprint = models.CharField(max_length=255, unique=True) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField(null=True, blank=True) - is_revoked = models.BooleanField(default=False) - revocation_reason = models.CharField(max_length=255, blank=True, null=True) - revocation_date = models.DateTimeField(blank=True, null=True) - - class Meta: - verbose_name = "服务器证书" - verbose_name_plural = "服务器证书" - db_table = "server_certificate" - - def revoke(self, reason=""): - self.is_revoked = True - self.revocation_reason = reason - self.revocation_date = datetime.datetime.utcnow() - self.save() - - def __str__(self): - return f"Server Cert: {self.hostname}" - - -class ClientCertificate(models.Model): - name = models.CharField(max_length=255) - upn_value = models.CharField( - max_length=255, blank=True, default="", verbose_name="UPN值" - ) - ca = models.ForeignKey(CertificateAuthority, on_delete=models.CASCADE) - thumbprint = models.CharField(max_length=255, unique=True) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField(null=True, blank=True) - assigned_to_user = models.ForeignKey( - "accounts.User", on_delete=models.SET_NULL, null=True, blank=True - ) - is_active = models.BooleanField(default=True) - description = models.TextField(blank=True, null=True) - - class Meta: - verbose_name = "客户端证书" - verbose_name_plural = "客户端证书" - db_table = "client_certificate" - - def __str__(self): - user_info = ( - f" (User: {self.assigned_to_user.username})" - if self.assigned_to_user - else "" - ) - return f"Client Cert: {self.name}{user_info}" diff --git a/apps/certificates/signals.py b/apps/certificates/signals.py deleted file mode 100755 index 64e89b3..0000000 --- a/apps/certificates/signals.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -证书管理应用的信号处理器 -""" -from django.db.models.signals import post_save, pre_delete -from django.dispatch import receiver -from .models import CertificateAuthority, ServerCertificate, ClientCertificate - - -# 可以在这里添加具体的信号处理器 -# 例如:在证书即将过期时发送通知 \ No newline at end of file diff --git a/apps/certificates/urls.py b/apps/certificates/urls.py deleted file mode 100755 index 7fecb9d..0000000 --- a/apps/certificates/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'certificates' - -urlpatterns = [ - # 证书签发API - path('issue-server-cert/', views.issue_server_certificate, name='issue_server_certificate'), - path('issue-client-cert/', views.issue_client_certificate, name='issue_client_certificate'), - path('validate-request/', views.validate_certificate_request, name='validate_certificate_request'), - path('get-ca-cert/', views.get_ca_certificate, name='get_ca_certificate'), - - # 证书管理API - path('manage/', views.CertificateManagementView.as_view(), name='certificate_management'), - path('renew/', views.renew_certificate, name='renew_certificate'), -] \ No newline at end of file diff --git a/apps/certificates/views.py b/apps/certificates/views.py deleted file mode 100755 index c1af73e..0000000 --- a/apps/certificates/views.py +++ /dev/null @@ -1,492 +0,0 @@ -from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from django.contrib.auth.decorators import login_required, permission_required -from .models import ServerCertificate, CertificateAuthority, ClientCertificate -from apps.hosts.models import Host -from django.utils.decorators import method_decorator -from django.views import View -from django.shortcuts import get_object_or_404 -import json -import logging -from datetime import datetime - -logger = logging.getLogger(__name__) - - -def _get_or_create_ca(ca_name='default-ca'): - ca, created = CertificateAuthority.objects.get_or_create( - name=ca_name, - defaults={ - 'name': ca_name, - 'description': f'Default CA for {ca_name}', - }, - ) - if created: - from utils.cert_service import generate_ca - from cryptography.hazmat.primitives import serialization - import datetime - - ca_key, ca_cert = generate_ca() - ca_key_pem = ca_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - ca_cert_pem = ca_cert.public_bytes(serialization.Encoding.PEM) - ca.save_ca_files(ca_key_pem, ca_cert_pem) - ca.expires_at = ( - datetime.datetime.now(datetime.timezone.utc) - + datetime.timedelta(days=3650) - ) - ca.save() - logger.info(f"Created new CA: {ca_name}") - - return ca, created - - -def _load_ca_crypto(ca): - from cryptography.hazmat.primitives import serialization - from cryptography import x509 as x509_mod - from typing import cast - from cryptography.hazmat.primitives.asymmetric import ec - - private_key_pem = ca.private_key - certificate_pem = ca.certificate - if not private_key_pem or not certificate_pem: - raise ValueError(f"CA {ca.name} key/cert files not found on disk") - - ca_key = cast( - ec.EllipticCurvePrivateKey, - serialization.load_pem_private_key( - private_key_pem.encode(), password=None, - ), - ) - ca_cert = x509_mod.load_pem_x509_certificate(certificate_pem.encode()) - return ca_key, ca_cert - - -@require_http_methods(["POST"]) -@login_required -@permission_required('certificates.add_servercertificate', raise_exception=True) -def issue_server_certificate(request): - try: - data = json.loads(request.body.decode('utf-8')) - hostname = data.get('hostname') - san_names = data.get('san_names', []) - ca_name = data.get('ca_name', 'default-ca') - - if not hostname: - return JsonResponse({ - 'success': False, - 'error': 'Hostname is required' - }, status=400) - - ca, created = _get_or_create_ca(ca_name) - - cert, created = ServerCertificate.objects.get_or_create( - hostname=hostname, - defaults={ - 'hostname': hostname, - 'ca': ca - } - ) - - if created or cert.is_revoked: - from utils.cert_service import issue_server_cert - from cryptography.hazmat.primitives import hashes - - ca_key, ca_cert = _load_ca_crypto(ca) - result = issue_server_cert( - ca_key=ca_key, - ca_cert=ca_cert, - hostname=hostname, - ip_address=None, - ) - fingerprint = result['server_cert'].fingerprint( - hashes.SHA1() - ) - cert.thumbprint = ":".join( - f"{byte:02X}" for byte in fingerprint - ) - cert.is_revoked = False - cert.expires_at = ( - datetime.utcnow() + __import__('datetime').timedelta( - days=3650 - ) - ) - cert.save() - logger.info(f"Issued new server certificate for {hostname}") - elif cert.expires_at and cert.expires_at < datetime.utcnow(): - cert.is_revoked = True - cert.save() - logger.info(f"Marked expired server cert for {hostname}") - - return JsonResponse({ - 'success': True, - 'data': { - 'thumbprint': cert.thumbprint, - 'expires_at': ( - cert.expires_at.isoformat() if cert.expires_at else None - ) - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error issuing server certificate: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to issue server certificate' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required('certificates.add_clientcertificate', raise_exception=True) -def issue_client_certificate(request): - try: - data = json.loads(request.body.decode('utf-8')) - name = data.get('name') - user_id = data.get('user_id') - description = data.get('description', '') - ca_name = data.get('ca_name', 'default-ca') - - if not name: - return JsonResponse({ - 'success': False, - 'error': 'Certificate name is required' - }, status=400) - - ca, created = _get_or_create_ca(ca_name) - - user = None - if user_id: - from django.contrib.auth.models import User - try: - user = User.objects.get(id=user_id) - except User.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'User not found' - }, status=404) - - cert, created = ClientCertificate.objects.get_or_create( - name=name, - defaults={ - 'name': name, - 'ca': ca, - 'assigned_to_user': user, - 'description': description - } - ) - - if created: - from utils.cert_service import issue_client_cert - from cryptography.hazmat.primitives import hashes - - ca_key, ca_cert = _load_ca_crypto(ca) - upn_value = f"{name}@localhost" - cert.upn_value = upn_value - client_key, client_cert = issue_client_cert( - ca_key=ca_key, - ca_cert=ca_cert, - upn_value=upn_value, - ) - fingerprint = client_cert.fingerprint(hashes.SHA1()) - cert.thumbprint = ":".join( - f"{byte:02X}" for byte in fingerprint - ) - cert.expires_at = ( - datetime.utcnow() + __import__('datetime').timedelta( - days=3650 - ) - ) - cert.save() - logger.info(f"Issued new client certificate for {name}") - - return JsonResponse({ - 'success': True, - 'data': { - 'thumbprint': cert.thumbprint, - 'expires_at': ( - cert.expires_at.isoformat() if cert.expires_at else None - ) - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error( - f"Error issuing client certificate: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Failed to issue client certificate' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -def validate_certificate_request(request): - try: - data = json.loads(request.body.decode('utf-8')) - hostname = data.get('hostname') - token = data.get('token') - - if not hostname or not token: - return JsonResponse({ - 'success': False, - 'error': 'Hostname and token are required' - }, status=400) - - host = Host.objects.filter( - hostname=hostname, - ).first() - - if not host: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired token' - }, status=401) - - return JsonResponse({ - 'success': True, - 'data': { - 'hostname': hostname, - 'host_id': host.id, - 'valid': True - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error( - f"Error validating certificate request: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Certificate validation failed' - }, status=500) - - -@require_http_methods(["GET"]) -@login_required -def get_ca_certificate(request): - try: - ca_name = request.GET.get('ca_name', 'default-ca') - - try: - ca = CertificateAuthority.objects.get( - name=ca_name, is_active=True - ) - ca_cert_pem = ca.certificate - if not ca_cert_pem: - return JsonResponse({ - 'success': False, - 'error': 'CA certificate file not found on disk' - }, status=404) - return JsonResponse({ - 'success': True, - 'data': { - 'ca_cert': ca_cert_pem, - 'expires_at': ca.expires_at.isoformat() - } - }) - except CertificateAuthority.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'CA not found or not active' - }, status=404) - - except Exception as e: - logger.error( - f"Error getting CA certificate: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve CA certificate' - }, status=500) - - -class CertificateManagementView(View): - - @method_decorator( - permission_required('certificates.view_certificateauthority') - ) - def get(self, request): - try: - cert_type = request.GET.get('type', 'all') - - result = {'success': True, 'data': {}} - - if cert_type in ['all', 'ca']: - cas = CertificateAuthority.objects.all() - result['data']['cas'] = [ - { - 'id': ca.id, - 'name': ca.name, - 'created_at': ca.created_at.isoformat(), - 'expires_at': ca.expires_at.isoformat(), - 'is_active': ca.is_active - } - for ca in cas - ] - - if cert_type in ['all', 'server']: - servers = ServerCertificate.objects.select_related('ca').all() - result['data']['servers'] = [ - { - 'id': cert.id, - 'hostname': cert.hostname, - 'ca_name': cert.ca.name, - 'thumbprint': cert.thumbprint, - 'created_at': cert.created_at.isoformat(), - 'expires_at': cert.expires_at.isoformat(), - 'is_revoked': cert.is_revoked - } - for cert in servers - ] - - if cert_type in ['all', 'client']: - clients = ClientCertificate.objects.select_related( - 'ca', 'assigned_to_user' - ).all() - result['data']['clients'] = [ - { - 'id': cert.id, - 'name': cert.name, - 'ca_name': cert.ca.name, - 'assigned_to_user': ( - cert.assigned_to_user.username - if cert.assigned_to_user else None - ), - 'thumbprint': cert.thumbprint, - 'created_at': cert.created_at.isoformat(), - 'expires_at': cert.expires_at.isoformat(), - 'is_active': cert.is_active - } - for cert in clients - ] - - return JsonResponse(result) - - except Exception as e: - logger.error( - f"Error getting certificates: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Failed to retrieve certificates' - }, status=500) - - @method_decorator( - permission_required('certificates.delete_servercertificate') - ) - def delete(self, request): - try: - cert_id = request.GET.get('id') - cert_type = request.GET.get('type', 'server') - - if not cert_id: - return JsonResponse({ - 'success': False, - 'error': 'Certificate ID is required' - }, status=400) - - if cert_type == 'server': - cert = get_object_or_404(ServerCertificate, id=cert_id) - cert.revoke("Revoked by admin") - elif cert_type == 'client': - cert = get_object_or_404(ClientCertificate, id=cert_id) - cert.is_active = False - cert.save() - else: - return JsonResponse({ - 'success': False, - 'error': 'Invalid certificate type' - }, status=400) - - return JsonResponse({ - 'success': True, - 'message': ( - f'{cert_type.title()} certificate revoked successfully' - ) - }) - - except Exception as e: - logger.error( - f"Error revoking certificate: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Failed to revoke certificate' - }, status=500) - - -@require_http_methods(["POST"]) -@login_required -@permission_required( - 'certificates.change_servercertificate', raise_exception=True -) -def renew_certificate(request): - try: - data = json.loads(request.body.decode('utf-8')) - cert_id = data.get('cert_id') - cert_type = data.get('type', 'server') - - if not cert_id: - return JsonResponse({ - 'success': False, - 'error': 'Certificate ID is required' - }, status=400) - - if cert_type == 'server': - cert = get_object_or_404(ServerCertificate, id=cert_id) - cert.is_revoked = False - cert.save() - elif cert_type == 'client': - cert = get_object_or_404(ClientCertificate, id=cert_id) - cert.is_active = True - cert.save() - else: - return JsonResponse({ - 'success': False, - 'error': 'Invalid certificate type' - }, status=400) - - return JsonResponse({ - 'success': True, - 'message': f'{cert_type.title()} certificate renewed successfully', - 'data': { - 'expires_at': ( - cert.expires_at.isoformat() if cert.expires_at else None - ) - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error( - f"Error renewing certificate: {str(e)}", exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': 'Failed to renew certificate' - }, status=500) diff --git a/apps/dashboard/__init__.py b/apps/dashboard/__init__.py deleted file mode 100755 index bdc2535..0000000 --- a/apps/dashboard/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -2c2a仪表盘应用 -""" -default_app_config = 'apps.dashboard.apps.DashboardConfig' diff --git a/apps/dashboard/admin.py b/apps/dashboard/admin.py deleted file mode 100755 index 4c53a6a..0000000 --- a/apps/dashboard/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.dashboard.views_admin) diff --git a/apps/dashboard/apps.py b/apps/dashboard/apps.py deleted file mode 100755 index 7a2152a..0000000 --- a/apps/dashboard/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -仪表盘应用配置 -""" -from django.apps import AppConfig - - -class DashboardConfig(AppConfig): - """仪表盘应用配置类""" - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.dashboard' - verbose_name = '仪表盘' diff --git a/apps/dashboard/context_processors.py b/apps/dashboard/context_processors.py deleted file mode 100644 index 050dca2..0000000 --- a/apps/dashboard/context_processors.py +++ /dev/null @@ -1,43 +0,0 @@ -from .models import SystemConfig -from utils.site_group import get_effective_config - - -def system_config(request): - try: - config = SystemConfig.get_config() - except Exception: - config = None - - site_group = getattr(request, "site_group", None) - effective_config = get_effective_config(site_group) if config else None - - if effective_config and effective_config.site_name: - site_name = effective_config.site_name - elif site_group and site_group.site_name: - site_name = site_group.site_name - else: - site_name = "2c2a" - - site_icon = None - if effective_config and effective_config.site_icon: - site_icon = effective_config.site_icon - elif site_group and site_group.site_icon: - site_icon = site_group.site_icon - if not site_icon and effective_config: - hostname = request.get_host().split(":")[0] if request else "" - site_icon = effective_config.get_site_icon_for_hostname(hostname) - if not site_icon: - site_icon = "/static/img/favicon.svg" - - is_site_group_admin = False - if request.user.is_authenticated: - is_site_group_admin = request.user.is_site_group_admin(site_group) - - return { - "system_config": config, - "effective_config": effective_config, - "site_name": site_name, - "site_icon": site_icon, - "site_group": site_group, - "is_site_group_admin": is_site_group_admin, - } diff --git a/apps/dashboard/forms.py b/apps/dashboard/forms.py deleted file mode 100755 index b9557e9..0000000 --- a/apps/dashboard/forms.py +++ /dev/null @@ -1,249 +0,0 @@ -""" -仪表盘表单 -""" -from django import forms -from django.core.mail import send_mail -from django.core.exceptions import ValidationError -from .models import DashboardWidget, SystemConfig - -MD_INPUT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition' -) -MD_SELECT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface appearance-none focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition cursor-pointer' -) -MD_CHECKBOX_CLASS = ( - 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 text-md-primary ' - 'focus:ring-md-primary focus:ring-2 transition cursor-pointer accent-md-primary' -) -MD_TEXTAREA_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition resize-y' -) - - -class DashboardWidgetForm(forms.ModelForm): - """ - 仪表盘组件表单 - 用于创建和编辑仪表盘组件 - """ - class Meta: - model = DashboardWidget - fields = [ - 'widget_type', 'title', 'display_order', - 'is_enabled', 'widget_config' - ] - widgets = { - 'widget_type': forms.Select(attrs={ - 'class': MD_SELECT_CLASS - }), - 'title': forms.TextInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入组件标题' - }), - 'display_order': forms.NumberInput(attrs={ - 'class': MD_INPUT_CLASS, - 'min': 0 - }), - 'is_enabled': forms.CheckboxInput(attrs={ - 'class': MD_CHECKBOX_CLASS - }), - 'widget_config': forms.Textarea(attrs={ - 'class': MD_TEXTAREA_CLASS, - 'rows': 5, - 'placeholder': '请输入JSON格式的配置参数' - }) - } - - def clean_widget_config(self): - """ - 验证widget_config字段 - 确保是有效的JSON格式 - """ - import json - config = self.cleaned_data.get('widget_config') - - if config: - try: - json.loads(config) - except json.JSONDecodeError: - raise forms.ValidationError('配置参数必须是有效的JSON格式') - - return config - - -class WidgetConfigForm(forms.Form): - """ - 组件配置表单 - 用于快速配置仪表盘组件 - """ - widget_id = forms.IntegerField( - widget=forms.HiddenInput(), - required=True - ) - is_enabled = forms.BooleanField( - label='启用组件', - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': MD_CHECKBOX_CLASS - }) - ) - display_order = forms.IntegerField( - label='显示顺序', - required=True, - min_value=0, - widget=forms.NumberInput(attrs={ - 'class': MD_INPUT_CLASS - }) - ) - - -class SystemConfigForm(forms.ModelForm): - """系统配置表单""" - - _PRESERVE_IF_EMPTY = [ - 'smtp_password', - ] - - class Meta: - model = SystemConfig - fields = [ - 'site_name', - 'icp_number', - 'police_number', - 'smtp_host', - 'smtp_port', - 'smtp_encryption', - 'smtp_username', - 'smtp_password', - 'smtp_from_email', - 'captcha_provider', - 'captcha_type', - 'login_captcha_type', - 'register_captcha_type', - 'email_captcha_type', - 'email_suffix_whitelist', - 'email_suffix_blacklist', - ] - widgets = { - 'site_name': forms.TextInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入站点名称' - }), - 'icp_number': forms.TextInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '例如:京ICP备12345678号' - }), - 'police_number': forms.TextInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '例如:京公网安备 11010502000000号' - }), - 'enable_registration': forms.CheckboxInput(attrs={ - 'class': MD_CHECKBOX_CLASS - }), - 'smtp_host': forms.TextInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入SMTP服务器地址' - }), - 'smtp_port': forms.NumberInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入SMTP端口' - }), - 'smtp_encryption': forms.Select(attrs={ - 'class': MD_INPUT_CLASS - }), - 'smtp_username': forms.EmailInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入SMTP用户名' - }), - 'smtp_password': forms.PasswordInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入SMTP密码', - 'render_value': True - }), - 'smtp_from_email': forms.EmailInput(attrs={ - 'class': MD_INPUT_CLASS, - 'placeholder': '请输入发件人邮箱' - }), - 'captcha_provider': forms.Select(attrs={ - 'class': MD_SELECT_CLASS - }), - 'email_suffix_whitelist': forms.Textarea(attrs={ - 'class': MD_TEXTAREA_CLASS, - 'rows': 5, - 'placeholder': ( - '每行一个允许的邮箱后缀,例如:\n' - '@example.com\n@gmail.com\n@company.com' - ) - }), - 'email_suffix_blacklist': forms.Textarea(attrs={ - 'class': MD_TEXTAREA_CLASS, - 'rows': 5, - 'placeholder': ( - '每行一个禁止的邮箱后缀,例如:\n' - '@tempmail.com\n@spam.com' - ) - }), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._original_values = {} - if self.instance and self.instance.pk: - for field in self._PRESERVE_IF_EMPTY: - self._original_values[field] = getattr( - self.instance, field - ) - for field in self._PRESERVE_IF_EMPTY: - self.fields[field].required = False - - def clean_smtp_port(self): - """验证SMTP端口""" - port = self.cleaned_data.get('smtp_port') - if port and (port < 1 or port > 65535): - raise forms.ValidationError('端口号必须在1-65535之间') - return port - - def clean(self): - cleaned = super().clean() - if cleaned is None: - cleaned = {} - return cleaned - - def save(self, commit=True): - config = super().save(commit=False) - - if self.instance and self.instance.pk: - for field in self._PRESERVE_IF_EMPTY: - if not getattr(config, field): - original = self._original_values.get(field) - if original: - setattr(config, field, original) - - smtp_configured = ( - config.smtp_host and config.smtp_port and - config.smtp_username and config.smtp_password and - config.smtp_from_email - ) - if smtp_configured: - try: - send_mail( - subject='系统配置测试邮件', - message='这是一封测试邮件,用于验证系统邮件配置是否正确。', - from_email=config.smtp_from_email, - recipient_list=[config.smtp_username], - fail_silently=False, - ) - except Exception as e: - raise ValidationError( - f'邮件配置测试失败: {str(e)}' - ) - - if commit: - config.save() - return config diff --git a/apps/dashboard/forms_admin.py b/apps/dashboard/forms_admin.py deleted file mode 100644 index d49a3e9..0000000 --- a/apps/dashboard/forms_admin.py +++ /dev/null @@ -1,158 +0,0 @@ -from django import forms -from .models import DashboardWidget, SystemConfig - - -class DashboardWidgetForm(forms.ModelForm): - - class Meta: - model = DashboardWidget - fields = [ - "widget_type", - "title", - "display_order", - "is_enabled", - "widget_config", - ] - - def clean_widget_config(self): - import json - - config = self.cleaned_data.get("widget_config") - if config: - if isinstance(config, str): - try: - json.loads(config) - except json.JSONDecodeError: - raise forms.ValidationError("配置参数必须是有效的 JSON 格式") - return config - - -class HostnameBrandingWidget(forms.Textarea): - def __init__(self, *args, **kwargs): - kwargs.setdefault("attrs", {}) - kwargs["attrs"]["rows"] = 6 - kwargs["attrs"]["class"] = ( - "w-full bg-white/5 backdrop-blur-xl border border-white/10 " - "rounded-md px-4 py-3 text-white placeholder-slate-500 " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 " - "focus:border-cyan-500 transition resize-y font-mono text-sm" - ) - kwargs["attrs"]["placeholder"] = ( - "{\n" - ' "site-a.example.com": {\n' - ' "site_name": "站点A",\n' - ' "site_icon": "/media/branding/site-a-icon.svg"\n' - " },\n" - ' "site-b.example.com": {\n' - ' "site_name": "站点B",\n' - ' "site_icon": "/media/branding/site-b-icon.svg"\n' - " }\n" - "}" - ) - super().__init__(*args, **kwargs) - - def format_value(self, value): - import json - - if value and isinstance(value, dict): - return json.dumps(value, indent=2, ensure_ascii=False) - return super().format_value(value) - - -class SystemConfigForm(forms.ModelForm): - - _PRESERVE_IF_EMPTY = [ - "smtp_password", - ] - - hostname_branding = forms.CharField( - widget=HostnameBrandingWidget(), - required=False, - label="主机名品牌绑定", - help_text=( - "按主机名绑定专用站点名和图标,格式:\n" - '{"host.example.com": {"site_name": "站点名", "site_icon": "/media/branding/icon.svg"}}\n' - "未配置的主机名使用全局默认值\n\n" - "⚠️ 此功能已由「站点组管理」替代,建议运行 python manage.py migrate_hostname_branding 迁移数据" - ), - ) - - class Meta: - model = SystemConfig - fields = [ - "site_name", - "enable_registration", - "icp_number", - "police_number", - "smtp_host", - "smtp_port", - "smtp_encryption", - "smtp_username", - "smtp_password", - "smtp_from_email", - "smtp_from_name", - "captcha_provider", - "captcha_type", - "login_captcha_type", - "register_captcha_type", - "email_captcha_type", - "email_suffix_whitelist", - "email_suffix_blacklist", - "local_access_locked", - "hostname_branding", - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._original_values = {} - if self.instance and self.instance.pk: - for field in self._PRESERVE_IF_EMPTY: - self._original_values[field] = getattr(self.instance, field) - for field in self._PRESERVE_IF_EMPTY: - self.fields[field].required = False - - def clean_hostname_branding(self): - import json - - value = self.cleaned_data.get("hostname_branding") - if not value: - return {} - if isinstance(value, str): - try: - value = json.loads(value) - except json.JSONDecodeError: - raise forms.ValidationError("必须是有效的 JSON 格式") - if not isinstance(value, dict): - raise forms.ValidationError("必须是 JSON 对象格式") - for hostname, config in value.items(): - if not isinstance(hostname, str) or not hostname.strip(): - raise forms.ValidationError(f'主机名 "{hostname}" 无效') - if not isinstance(config, dict): - raise forms.ValidationError(f'主机名 "{hostname}" 的配置必须是对象') - allowed_keys = {"site_name", "site_icon"} - invalid_keys = set(config.keys()) - allowed_keys - if invalid_keys: - raise forms.ValidationError( - f'主机名 "{hostname}" 包含无效字段: ' - f'{", ".join(invalid_keys)},' - f'仅支持: {", ".join(allowed_keys)}' - ) - return value - - def clean_smtp_port(self): - port = self.cleaned_data.get("smtp_port") - if port and (port < 1 or port > 65535): - raise forms.ValidationError("端口号必须在 1-65535 之间") - return port - - def save(self, commit=True): - instance = super().save(commit=False) - if self.instance and self.instance.pk: - for field in self._PRESERVE_IF_EMPTY: - if not getattr(instance, field): - original = self._original_values.get(field) - if original: - setattr(instance, field, original) - if commit: - instance.save() - return instance diff --git a/apps/dashboard/forms_sitegroup.py b/apps/dashboard/forms_sitegroup.py deleted file mode 100644 index ee63a98..0000000 --- a/apps/dashboard/forms_sitegroup.py +++ /dev/null @@ -1,199 +0,0 @@ -from django import forms -from .models import SiteGroup, SiteGroupHostname, SiteGroupConfig - - -GLOW_INPUT = ( - "w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 " - "rounded px-3 py-2 text-slate-200 placeholder-slate-500 " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 " - "focus:border-cyan-500 transition" -) -GLOW_SELECT = ( - "w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 " - "rounded px-3 py-2 text-slate-200 appearance-none " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 " - "focus:border-cyan-500 transition cursor-pointer" -) -GLOW_TEXTAREA = ( - "w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 " - "rounded px-3 py-2 text-slate-200 placeholder-slate-500 " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 " - "focus:border-cyan-500 transition font-mono text-sm" -) -GLOW_CHECKBOX = ( - "w-4 h-4 bg-slate-900/50 border-slate-700/50 rounded " - "focus:ring-cyan-500/50 text-cyan-500" -) - - -class SiteGroupForm(forms.ModelForm): - class Meta: - model = SiteGroup - fields = ["name", "slug", "description", "site_name", "site_icon", "is_active"] - widgets = { - "name": forms.TextInput(attrs={"class": GLOW_INPUT}), - "slug": forms.TextInput(attrs={"class": GLOW_INPUT}), - "description": forms.Textarea( - attrs={ - "class": GLOW_INPUT, - "rows": 3, - } - ), - "site_name": forms.TextInput(attrs={"class": GLOW_INPUT}), - "site_icon": forms.TextInput(attrs={"class": GLOW_INPUT}), - "is_active": forms.CheckboxInput(attrs={"class": GLOW_CHECKBOX}), - } - - -class SiteGroupHostnameForm(forms.ModelForm): - class Meta: - model = SiteGroupHostname - fields = ["hostname"] - widgets = { - "hostname": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "demo.example.com", - } - ), - } - - -class SiteGroupConfigForm(forms.ModelForm): - """站点组配置覆盖表单""" - - class Meta: - model = SiteGroupConfig - fields = [ - "site_name", - "site_icon", - "icp_number", - "police_number", - "smtp_host", - "smtp_port", - "smtp_encryption", - "smtp_username", - "smtp_password", - "smtp_from_email", - "smtp_from_name", - "captcha_provider", - "captcha_type", - "login_captcha_type", - "register_captcha_type", - "email_captcha_type", - "enable_registration", - "email_suffix_whitelist", - "email_suffix_blacklist", - ] - widgets = { - "site_name": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "site_icon": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置,如 /media/branding/icon.svg", - } - ), - "icp_number": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "police_number": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "smtp_host": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "smtp_port": forms.NumberInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "smtp_encryption": forms.Select(attrs={"class": GLOW_SELECT}), - "smtp_username": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "smtp_password": forms.PasswordInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空保持原值", - } - ), - "smtp_from_email": forms.EmailInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "smtp_from_name": forms.TextInput( - attrs={ - "class": GLOW_INPUT, - "placeholder": "留空使用全局配置", - } - ), - "captcha_provider": forms.Select(attrs={"class": GLOW_SELECT}), - "captcha_type": forms.Select(attrs={"class": GLOW_SELECT}), - "login_captcha_type": forms.Select(attrs={"class": GLOW_SELECT}), - "register_captcha_type": forms.Select(attrs={"class": GLOW_SELECT}), - "email_captcha_type": forms.Select(attrs={"class": GLOW_SELECT}), - "enable_registration": forms.Select( - attrs={ - "class": GLOW_SELECT, - }, - choices=[ - ("", "--- 使用全局配置 ---"), - ("true", "启用"), - ("false", "禁用"), - ], - ), - "email_suffix_whitelist": forms.Textarea( - attrs={ - "class": GLOW_TEXTAREA, - "rows": 4, - "placeholder": "留空使用全局配置\n@example.com\n@gmail.com", - } - ), - "email_suffix_blacklist": forms.Textarea( - attrs={ - "class": GLOW_TEXTAREA, - "rows": 4, - "placeholder": "留空使用全局配置\n@tempmail.com\n@spam.com", - } - ), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # 为验证码类型字段添加"使用全局配置"空选项 - for field_name in [ - "captcha_type", - "login_captcha_type", - "register_captcha_type", - "email_captcha_type", - ]: - self.fields[field_name].widget.choices = [ - ("", "--- 使用全局配置 ---"), - ] + list(self.fields[field_name].widget.choices) - - def clean_smtp_password(self): - """密码字段留空时保留原值""" - password = self.cleaned_data.get("smtp_password") - if not password and self.instance and self.instance.pk: - return self.instance.smtp_password - return password diff --git a/apps/dashboard/management/__init__.py b/apps/dashboard/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/dashboard/management/commands/__init__.py b/apps/dashboard/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/dashboard/management/commands/locallock.py b/apps/dashboard/management/commands/locallock.py deleted file mode 100644 index cd2bbb4..0000000 --- a/apps/dashboard/management/commands/locallock.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.core.management.base import BaseCommand -from apps.dashboard.models import SystemConfig - - -class Command(BaseCommand): - help = '禁止本地访问(localhost/127.0.0.1)' - - def handle(self, *args, **options): - config = SystemConfig.get_config() - if config.local_access_locked: - self.stdout.write( - self.style.WARNING('本地访问限制已经处于启用状态') - ) - return - - config.local_access_locked = True - config.save(update_fields=['local_access_locked', 'updated_at']) - self.stdout.write( - self.style.SUCCESS( - '已启用本地访问限制,' - '来自 localhost/127.0.0.1 的请求将被拒绝' - ) - ) diff --git a/apps/dashboard/management/commands/localunlock.py b/apps/dashboard/management/commands/localunlock.py deleted file mode 100644 index 9db2144..0000000 --- a/apps/dashboard/management/commands/localunlock.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.core.management.base import BaseCommand -from apps.dashboard.models import SystemConfig - - -class Command(BaseCommand): - help = '解除本地访问限制(localhost/127.0.0.1)' - - def handle(self, *args, **options): - config = SystemConfig.get_config() - if not config.local_access_locked: - self.stdout.write( - self.style.WARNING('本地访问限制已经处于关闭状态') - ) - return - - config.local_access_locked = False - config.save(update_fields=['local_access_locked', 'updated_at']) - self.stdout.write( - self.style.SUCCESS( - '已解除本地访问限制,' - '来自 localhost/127.0.0.1 的请求将被允许' - ) - ) diff --git a/apps/dashboard/management/commands/migrate_hostname_branding.py b/apps/dashboard/management/commands/migrate_hostname_branding.py deleted file mode 100644 index d7d138b..0000000 --- a/apps/dashboard/management/commands/migrate_hostname_branding.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.cache import cache -from apps.dashboard.models import SystemConfig, SiteGroup, SiteGroupHostname - - -class Command(BaseCommand): - help = '将 SystemConfig.hostname_branding 数据迁移到 SiteGroup 模型' - - def handle(self, *args, **options): - try: - config = SystemConfig.get_config() - except SystemConfig.DoesNotExist: - self.stdout.write(self.style.ERROR('SystemConfig 不存在')) - return - - if not config.hostname_branding: - self.stdout.write(self.style.WARNING('hostname_branding 为空,无需迁移')) - return - - created_count = 0 - hostname_count = 0 - - for hostname, branding in config.hostname_branding.items(): - site_name = branding.get('site_name', '') - site_icon = branding.get('site_icon', '') - - if not site_name and not site_icon: - continue - - slug = hostname.replace('.', '-').replace(':', '-') - site_group, created = SiteGroup.objects.get_or_create( - slug=slug, - defaults={ - 'name': site_name or hostname, - 'site_name': site_name, - 'site_icon': site_icon, - } - ) - - if created: - created_count += 1 - self.stdout.write(self.style.SUCCESS(f'创建站点组: {site_group.name} (slug={slug})')) - else: - updated = False - if site_name and not site_group.site_name: - site_group.site_name = site_name - updated = True - if site_icon and not site_group.site_icon: - site_group.site_icon = site_icon - updated = True - if updated: - site_group.save() - self.stdout.write(f'更新站点组: {site_group.name}') - - _, hn_created = SiteGroupHostname.objects.get_or_create( - hostname=hostname, - defaults={'site_group': site_group} - ) - if hn_created: - hostname_count += 1 - self.stdout.write(f'绑定主机名: {hostname} -> {site_group.name}') - - cache.delete_pattern('site_group:hostname:*') if hasattr(cache, 'delete_pattern') else None - - self.stdout.write(self.style.SUCCESS( - f'\n迁移完成: 创建 {created_count} 个站点组, 绑定 {hostname_count} 个主机名' - )) diff --git a/apps/dashboard/middleware.py b/apps/dashboard/middleware.py deleted file mode 100644 index 89997fc..0000000 --- a/apps/dashboard/middleware.py +++ /dev/null @@ -1,121 +0,0 @@ -from django.core.cache import cache -from django.shortcuts import redirect -from django.urls import resolve - - -class SiteGroupMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - request.site_group = self._resolve_site_group(request) - response = self._check_pending_migration(request) - if response: - return response - response = self._check_banned_user(request) - if response: - return response - return self.get_response(request) - - def _check_pending_migration(self, request): - """已登录用户有 pending_migration_sg_id 时,强制跳转迁移页""" - if not request.user.is_authenticated: - return None - sg_id = request.session.get("pending_migration_sg_id") - if not sg_id: - return None - # 跳过静态文件和媒体文件 - if request.path.startswith("/static/") or request.path.startswith("/media/"): - return None - try: - match = resolve(request.path) - # 允许访问迁移页、登出、邮箱绑定相关 API - allowed_names = ( - "migrate", - "logout", - "email_bind", - "send_bind_email_code", - "email_list", - "email_set_primary", - "email_unbind", - "email_merge_confirm", - ) - if match.url_name in allowed_names: - return None - except Exception: - pass - return redirect("accounts:migrate") - - def _check_banned_user(self, request): - """封禁用户只能访问封禁提示页和工单相关页面""" - if not request.user.is_authenticated: - return None - # 使用自定义 UserBan 模型判断封禁状态 - from apps.accounts.models import UserBan - if not UserBan.objects.filter(user=request.user).exists(): - return None - # 静态文件和媒体文件放行 - if request.path.startswith("/static/") or request.path.startswith("/media/"): - return None - try: - match = resolve(request.path) - # 允许访问封禁提示页、登出、工单相关页面 - allowed_names = ( - "banned", - "logout", - "ticket_list", - "ticket_create", - "ticket_detail", - "my_tickets", - "ticket_comment", - ) - if match.url_name in allowed_names: - return None - # 允许工单 app 的所有视图 - if match.app_name == "tickets": - return None - except Exception: - pass - return redirect("accounts:banned") - - def _resolve_site_group(self, request): - try: - hostname = request.get_host().split(":")[0] - except Exception: - return None - - if not hostname: - return None - - cache_key = f"site_group:hostname:{hostname}" - site_group_id = cache.get(cache_key) - - if site_group_id is not None: - if site_group_id == 0: - return None - from apps.dashboard.models import SiteGroup - - try: - site_group = SiteGroup.objects.get(pk=site_group_id, is_active=True) - return site_group - except SiteGroup.DoesNotExist: - cache.delete(cache_key) - return None - - from apps.dashboard.models import SiteGroupHostname - - try: - mapping = SiteGroupHostname.objects.select_related("site_group").get( - hostname=hostname - ) - except SiteGroupHostname.DoesNotExist: - cache.set(cache_key, 0, timeout=300) - return None - - site_group = mapping.site_group - if not site_group.is_active: - cache.set(cache_key, 0, timeout=300) - return None - - cache.set(cache_key, site_group.pk, timeout=300) - return site_group diff --git a/apps/dashboard/migrations/0001_initial.py b/apps/dashboard/migrations/0001_initial.py deleted file mode 100755 index aea5ccf..0000000 --- a/apps/dashboard/migrations/0001_initial.py +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:24 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='SystemConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('smtp_host', models.CharField(blank=True, help_text='SMTP服务器地址,如smtp.gmail.com', max_length=255, null=True, verbose_name='SMTP服务器')), - ('smtp_port', models.IntegerField(blank=True, help_text='SMTP服务器端口,通常为587或465', null=True, verbose_name='SMTP端口')), - ('smtp_use_tls', models.BooleanField(default=True, help_text='是否使用TLS加密连接', verbose_name='使用TLS')), - ('smtp_username', models.CharField(blank=True, help_text='SMTP登录用户名,通常是邮箱地址', max_length=255, null=True, verbose_name='SMTP用户名')), - ('smtp_password', models.CharField(blank=True, help_text='SMTP登录密码或应用专用密码', max_length=255, null=True, verbose_name='SMTP密码')), - ('smtp_from_email', models.EmailField(blank=True, help_text='系统发送邮件时使用的发件人地址', max_length=254, null=True, verbose_name='发件人邮箱')), - ('captcha_id', models.CharField(blank=True, help_text='验证码服务的公共ID(Geetest的captcha_id 或 Turnstile的site key)', max_length=255, null=True, verbose_name='验证码 ID')), - ('captcha_key', models.CharField(blank=True, help_text='验证码服务的密钥(Geetest的private_key 或 Turnstile的secret key)', max_length=255, null=True, verbose_name='验证码密钥')), - ('captcha_provider', models.CharField(choices=[('none', '无'), ('geetest', 'Geetest (极验 v4)'), ('turnstile', 'Cloudflare Turnstile'), ('local', '本地图片验证码')], default='none', help_text='选择要启用的验证码提供器(只能选择其一)', max_length=32, verbose_name='验证码提供器')), - ('email_captcha_provider', models.CharField(blank=True, choices=[('none', '无'), ('geetest', 'Geetest (极验 v4)'), ('turnstile', 'Cloudflare Turnstile'), ('local', '本地图片验证码')], help_text='邮箱场景的验证码提供器(留空则使用全局配置)', max_length=32, null=True, verbose_name='邮箱验证码提供器')), - ('email_captcha_id', models.CharField(blank=True, help_text='邮箱场景验证码服务的公共ID(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='邮箱验证码 ID')), - ('email_captcha_key', models.CharField(blank=True, help_text='邮箱场景验证码服务的密钥(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='邮箱验证码密钥')), - ('login_captcha_provider', models.CharField(blank=True, choices=[('none', '无'), ('geetest', 'Geetest (极验 v4)'), ('turnstile', 'Cloudflare Turnstile'), ('local', '本地图片验证码')], help_text='登录场景的验证码提供器(留空则使用全局配置)', max_length=32, null=True, verbose_name='登录验证码提供器')), - ('login_captcha_id', models.CharField(blank=True, help_text='登录场景验证码服务的公共ID(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='登录验证码 ID')), - ('login_captcha_key', models.CharField(blank=True, help_text='登录场景验证码服务的密钥(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='登录验证码密钥')), - ('register_captcha_provider', models.CharField(blank=True, choices=[('none', '无'), ('geetest', 'Geetest (极验 v4)'), ('turnstile', 'Cloudflare Turnstile'), ('local', '本地图片验证码')], help_text='注册场景的验证码提供器(留空则使用全局配置)', max_length=32, null=True, verbose_name='注册验证码提供器')), - ('register_captcha_id', models.CharField(blank=True, help_text='注册场景验证码服务的公共ID(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='注册验证码 ID')), - ('register_captcha_key', models.CharField(blank=True, help_text='注册场景验证码服务的密钥(如果为空,则使用全局配置)', max_length=255, null=True, verbose_name='注册验证码密钥')), - ('site_name', models.CharField(default='2c2a', help_text='系统显示的站点名称', max_length=100, verbose_name='站点名称')), - ('enable_registration', models.BooleanField(default=False, help_text='是否开启用户注册功能,默认为关闭', verbose_name='启用用户注册')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '系统配置', - 'verbose_name_plural': '系统配置', - }, - ), - migrations.CreateModel( - name='SystemStats', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stats_type', models.CharField(choices=[('user_count', '用户数量'), ('host_count', '主机数量'), ('operation_count', '操作数量'), ('active_host_count', '活跃主机数'), ('error_count', '错误数量')], help_text='统计数据的类型', max_length=50, verbose_name='统计类型')), - ('stats_value', models.IntegerField(help_text='统计数据的值', verbose_name='统计值')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='统计数据创建时间', verbose_name='创建时间')), - ], - options={ - 'verbose_name': '系统统计', - 'verbose_name_plural': '系统统计', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['stats_type'], name='dashboard_s_stats_t_d000e0_idx'), models.Index(fields=['created_at'], name='dashboard_s_created_068de5_idx')], - }, - ), - migrations.CreateModel( - name='DashboardWidget', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('widget_type', models.CharField(choices=[('stat_card', '统计卡片'), ('chart', '图表'), ('recent_operations', '最近操作'), ('host_status', '主机状态'), ('system_alerts', '系统告警')], help_text='组件的类型', max_length=50, verbose_name='组件类型')), - ('title', models.CharField(help_text='组件显示的标题', max_length=200, verbose_name='标题')), - ('display_order', models.IntegerField(default=0, help_text='组件在仪表盘上的显示顺序', verbose_name='显示顺序')), - ('is_enabled', models.BooleanField(default=True, help_text='组件是否在仪表盘上显示', verbose_name='是否启用')), - ('widget_config', models.JSONField(blank=True, default=dict, help_text='组件的配置参数', verbose_name='组件配置')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='组件创建时间', verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='组件更新时间', verbose_name='更新时间')), - ], - options={ - 'verbose_name': '仪表盘组件', - 'verbose_name_plural': '仪表盘组件', - 'ordering': ['display_order'], - 'indexes': [models.Index(fields=['widget_type'], name='dashboard_d_widget__357338_idx'), models.Index(fields=['is_enabled'], name='dashboard_d_is_enab_79f78f_idx'), models.Index(fields=['display_order'], name='dashboard_d_display_5437b7_idx')], - }, - ), - migrations.CreateModel( - name='UserActivity', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('activity_type', models.CharField(help_text='用户活动的类型', max_length=100, verbose_name='活动类型')), - ('description', models.TextField(blank=True, help_text='活动描述', verbose_name='描述')), - ('ip_address', models.GenericIPAddressField(blank=True, help_text='用户操作的IP地址', null=True, verbose_name='IP地址')), - ('user_agent', models.TextField(blank=True, help_text='用户浏览器的User-Agent信息', verbose_name='用户代理')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='活动记录创建时间', verbose_name='创建时间')), - ('user', models.ForeignKey(help_text='关联的用户', on_delete=django.db.models.deletion.CASCADE, related_name='activities', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '用户活动', - 'verbose_name_plural': '用户活动', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['user'], name='dashboard_u_user_id_00b481_idx'), models.Index(fields=['activity_type'], name='dashboard_u_activit_a0fab2_idx'), models.Index(fields=['ip_address'], name='dashboard_u_ip_addr_76ce0f_idx'), models.Index(fields=['created_at'], name='dashboard_u_created_8f7e1e_idx')], - }, - ), - ] diff --git a/apps/dashboard/migrations/0002_systemconfig_email_suffix_list_and_more.py b/apps/dashboard/migrations/0002_systemconfig_email_suffix_list_and_more.py deleted file mode 100755 index a4a3d1d..0000000 --- a/apps/dashboard/migrations/0002_systemconfig_email_suffix_list_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-28 03:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='systemconfig', - name='email_suffix_list', - field=models.TextField(blank=True, help_text='允许或禁止的邮箱后缀列表,每行一个后缀,例如:\n@example.com\n@gmail.com\n@company.com', null=True, verbose_name='邮箱后缀列表'), - ), - migrations.AddField( - model_name='systemconfig', - name='email_suffix_mode', - field=models.CharField(choices=[('allow_all', '全部允许'), ('whitelist', '白名单'), ('blacklist', '黑名单')], default='allow_all', help_text='邮箱后缀验证模式:全部允许、白名单或黑名单', max_length=20, verbose_name='邮箱后缀模式'), - ), - ] diff --git a/apps/dashboard/migrations/0003_systemconfig_icp_number_systemconfig_police_number.py b/apps/dashboard/migrations/0003_systemconfig_icp_number_systemconfig_police_number.py deleted file mode 100644 index 1f70b06..0000000 --- a/apps/dashboard/migrations/0003_systemconfig_icp_number_systemconfig_police_number.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-06 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0002_systemconfig_email_suffix_list_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="icp_number", - field=models.CharField( - blank=True, - help_text="ICP备案号,例如:京ICP备12345678号", - max_length=100, - null=True, - verbose_name="ICP备案号", - ), - ), - migrations.AddField( - model_name="systemconfig", - name="police_number", - field=models.CharField( - blank=True, - help_text="公安备案号,例如:京公网安备 11010502000000号", - max_length=100, - null=True, - verbose_name="公安备案号", - ), - ), - ] diff --git a/apps/dashboard/migrations/0004_remove_system_stats.py b/apps/dashboard/migrations/0004_remove_system_stats.py deleted file mode 100644 index 29ac643..0000000 --- a/apps/dashboard/migrations/0004_remove_system_stats.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-07 13:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0003_systemconfig_icp_number_systemconfig_police_number"), - ] - - operations = [ - migrations.DeleteModel( - name="SystemStats", - ), - ] diff --git a/apps/dashboard/migrations/0005_add_local_access_locked.py b/apps/dashboard/migrations/0005_add_local_access_locked.py deleted file mode 100644 index 917daef..0000000 --- a/apps/dashboard/migrations/0005_add_local_access_locked.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-10 19:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0004_remove_system_stats"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="local_access_locked", - field=models.BooleanField( - default=False, - help_text="启用后将禁止来自 localhost/127.0.0.1 的访问", - verbose_name="禁止本地访问", - ), - ), - ] diff --git a/apps/dashboard/migrations/0006_delete_useractivity.py b/apps/dashboard/migrations/0006_delete_useractivity.py deleted file mode 100644 index efcd6d5..0000000 --- a/apps/dashboard/migrations/0006_delete_useractivity.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-01 14:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0005_add_local_access_locked'), - ] - - operations = [ - migrations.DeleteModel( - name='UserActivity', - ), - ] diff --git a/apps/dashboard/migrations/0007_systemconfig_email_suffix_whitelist_blacklist.py b/apps/dashboard/migrations/0007_systemconfig_email_suffix_whitelist_blacklist.py deleted file mode 100644 index 6df0a01..0000000 --- a/apps/dashboard/migrations/0007_systemconfig_email_suffix_whitelist_blacklist.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-02 09:05 - -from django.db import migrations, models - - -def migrate_email_suffix_data(apps, schema_editor): - SystemConfig = apps.get_model('dashboard', 'SystemConfig') - for config in SystemConfig.objects.all(): - old_mode = getattr(config, 'email_suffix_mode', None) - old_list = getattr(config, 'email_suffix_list', None) or '' - if old_mode == 'whitelist' and old_list.strip(): - config.email_suffix_whitelist = old_list - config.email_suffix_blacklist = '' - elif old_mode == 'blacklist' and old_list.strip(): - config.email_suffix_whitelist = '' - config.email_suffix_blacklist = old_list - else: - config.email_suffix_whitelist = '' - config.email_suffix_blacklist = '' - config.save(update_fields=['email_suffix_whitelist', 'email_suffix_blacklist']) - - -def reverse_migrate_email_suffix_data(apps, schema_editor): - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0006_delete_useractivity'), - ] - - operations = [ - migrations.AddField( - model_name='systemconfig', - name='email_suffix_blacklist', - field=models.TextField(blank=True, help_text='禁止注册的邮箱后缀列表,每行一个后缀,例如:\n@tempmail.com\n@spam.com\n留空表示不限制', null=True, verbose_name='邮箱后缀黑名单'), - ), - migrations.AddField( - model_name='systemconfig', - name='email_suffix_whitelist', - field=models.TextField(blank=True, help_text='允许注册的邮箱后缀列表,每行一个后缀,例如:\n@example.com\n@gmail.com\n@company.com\n留空表示不限制', null=True, verbose_name='邮箱后缀白名单'), - ), - migrations.RunPython(migrate_email_suffix_data, reverse_migrate_email_suffix_data), - migrations.RemoveField( - model_name='systemconfig', - name='email_suffix_list', - ), - migrations.RemoveField( - model_name='systemconfig', - name='email_suffix_mode', - ), - ] diff --git a/apps/dashboard/migrations/0008_add_qq_bot_default_config.py b/apps/dashboard/migrations/0008_add_qq_bot_default_config.py deleted file mode 100644 index 8b42e97..0000000 --- a/apps/dashboard/migrations/0008_add_qq_bot_default_config.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-02 09:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0007_systemconfig_email_suffix_whitelist_blacklist'), - ] - - operations = [ - migrations.AddField( - model_name='systemconfig', - name='qq_bot_host', - field=models.CharField(blank=True, help_text='QQ机器人服务器的主机地址(系统默认配置)', max_length=255, null=True, verbose_name='QQ机器人服务器地址'), - ), - migrations.AddField( - model_name='systemconfig', - name='qq_bot_port', - field=models.CharField(blank=True, help_text='QQ机器人服务器的端口号(系统默认配置)', max_length=20, null=True, verbose_name='QQ机器人服务器端口'), - ), - migrations.AddField( - model_name='systemconfig', - name='qq_bot_token', - field=models.CharField(blank=True, help_text='用于认证的访问令牌(系统默认配置)', max_length=255, null=True, verbose_name='QQ机器人访问令牌'), - ), - ] diff --git a/apps/dashboard/migrations/0009_remove_qq_bot_fields.py b/apps/dashboard/migrations/0009_remove_qq_bot_fields.py deleted file mode 100644 index c1d17b3..0000000 --- a/apps/dashboard/migrations/0009_remove_qq_bot_fields.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-03 12:32 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0008_add_qq_bot_default_config'), - ] - - operations = [ - migrations.RemoveField( - model_name='systemconfig', - name='qq_bot_host', - ), - migrations.RemoveField( - model_name='systemconfig', - name='qq_bot_port', - ), - migrations.RemoveField( - model_name='systemconfig', - name='qq_bot_token', - ), - ] diff --git a/apps/dashboard/migrations/0010_migrate_qq_bot_to_plugin_config.py b/apps/dashboard/migrations/0010_migrate_qq_bot_to_plugin_config.py deleted file mode 100644 index badc01b..0000000 --- a/apps/dashboard/migrations/0010_migrate_qq_bot_to_plugin_config.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import migrations - - -def migrate_qq_bot_config_to_plugin(apps, schema_editor): - SystemConfig = apps.get_model('dashboard', 'SystemConfig') - PluginRecord = apps.get_model('plugins', 'PluginRecord') - PluginConfiguration = apps.get_model( - 'plugins', 'PluginConfiguration' - ) - - try: - config = SystemConfig.objects.first() - if not config: - return - except Exception: - return - - record, _ = PluginRecord.objects.get_or_create( - plugin_id='qq_verification', - defaults={ - 'name': 'QQ Verification Plugin', - 'version': '1.0.0', - 'description': 'QQ验证插件', - 'is_active': True, - }, - ) - - for field_name in ( - 'qq_bot_host', 'qq_bot_port', 'qq_bot_token' - ): - value = getattr(config, field_name, None) or '' - PluginConfiguration.objects.update_or_create( - plugin=record, - key=field_name, - defaults={'value': value}, - ) - - -def reverse_migrate(apps, schema_editor): - SystemConfig = apps.get_model('dashboard', 'SystemConfig') - PluginRecord = apps.get_model('plugins', 'PluginRecord') - PluginConfiguration = apps.get_model( - 'plugins', 'PluginConfiguration' - ) - - try: - record = PluginRecord.objects.get( - plugin_id='qq_verification' - ) - except PluginRecord.DoesNotExist: - return - - config = SystemConfig.objects.first() - if not config: - return - - for field_name in ( - 'qq_bot_host', 'qq_bot_port', 'qq_bot_token' - ): - try: - pc = PluginConfiguration.objects.get( - plugin=record, key=field_name - ) - setattr(config, field_name, pc.value) - except PluginConfiguration.DoesNotExist: - pass - - config.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0009_remove_qq_bot_fields'), - ('plugins', '0007_add_use_default_bot_and_group_ids'), - ] - - operations = [ - migrations.RunPython( - migrate_qq_bot_config_to_plugin, - reverse_migrate, - ), - ] diff --git a/apps/dashboard/migrations/0011_remove_systemconfig_captcha_id_and_more.py b/apps/dashboard/migrations/0011_remove_systemconfig_captcha_id_and_more.py deleted file mode 100644 index b014c58..0000000 --- a/apps/dashboard/migrations/0011_remove_systemconfig_captcha_id_and_more.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-29 15:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0010_migrate_qq_bot_to_plugin_config'), - ] - - operations = [ - migrations.RemoveField( - model_name='systemconfig', - name='captcha_id', - ), - migrations.RemoveField( - model_name='systemconfig', - name='captcha_key', - ), - migrations.RemoveField( - model_name='systemconfig', - name='email_captcha_id', - ), - migrations.RemoveField( - model_name='systemconfig', - name='email_captcha_key', - ), - migrations.RemoveField( - model_name='systemconfig', - name='email_captcha_provider', - ), - migrations.RemoveField( - model_name='systemconfig', - name='login_captcha_id', - ), - migrations.RemoveField( - model_name='systemconfig', - name='login_captcha_key', - ), - migrations.RemoveField( - model_name='systemconfig', - name='login_captcha_provider', - ), - migrations.RemoveField( - model_name='systemconfig', - name='register_captcha_id', - ), - migrations.RemoveField( - model_name='systemconfig', - name='register_captcha_key', - ), - migrations.RemoveField( - model_name='systemconfig', - name='register_captcha_provider', - ), - migrations.AlterField( - model_name='systemconfig', - name='captcha_provider', - field=models.CharField(choices=[('none', '无'), ('tianai', '天爱验证码')], default='none', help_text='选择要启用的验证码提供器', max_length=32, verbose_name='验证码提供器'), - ), - ] diff --git a/apps/dashboard/migrations/0012_systemconfig_captcha_type_and_more.py b/apps/dashboard/migrations/0012_systemconfig_captcha_type_and_more.py deleted file mode 100644 index 3f31402..0000000 --- a/apps/dashboard/migrations/0012_systemconfig_captcha_type_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-29 15:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0011_remove_systemconfig_captcha_id_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='systemconfig', - name='captcha_type', - field=models.CharField(choices=[('SLIDER', '滑块验证'), ('ROTATE', '旋转验证'), ('CONCAT', '滑动还原'), ('WORD_IMAGE_CLICK', '文字点选')], default='SLIDER', help_text='全局默认的验证码类型', max_length=32, verbose_name='默认验证码类型'), - ), - migrations.AddField( - model_name='systemconfig', - name='email_captcha_type', - field=models.CharField(blank=True, choices=[('SLIDER', '滑块验证'), ('ROTATE', '旋转验证'), ('CONCAT', '滑动还原'), ('WORD_IMAGE_CLICK', '文字点选')], help_text='邮箱发送验证码场景的验证码类型(留空则使用默认类型)', max_length=32, null=True, verbose_name='邮箱验证码类型'), - ), - migrations.AddField( - model_name='systemconfig', - name='login_captcha_type', - field=models.CharField(blank=True, choices=[('SLIDER', '滑块验证'), ('ROTATE', '旋转验证'), ('CONCAT', '滑动还原'), ('WORD_IMAGE_CLICK', '文字点选')], help_text='登录场景的验证码类型(留空则使用默认类型)', max_length=32, null=True, verbose_name='登录验证码类型'), - ), - migrations.AddField( - model_name='systemconfig', - name='register_captcha_type', - field=models.CharField(blank=True, choices=[('SLIDER', '滑块验证'), ('ROTATE', '旋转验证'), ('CONCAT', '滑动还原'), ('WORD_IMAGE_CLICK', '文字点选')], help_text='注册场景的验证码类型(留空则使用默认类型)', max_length=32, null=True, verbose_name='注册验证码类型'), - ), - ] diff --git a/apps/dashboard/migrations/0013_systemconfig_hostname_branding.py b/apps/dashboard/migrations/0013_systemconfig_hostname_branding.py deleted file mode 100644 index 831f2e4..0000000 --- a/apps/dashboard/migrations/0013_systemconfig_hostname_branding.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 05:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0012_systemconfig_captcha_type_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="hostname_branding", - field=models.JSONField( - blank=True, - default=dict, - help_text='按主机名绑定专用站点名和图标,格式:\n{"host.example.com": {"site_name": "站点名", "site_icon": "/media/branding/icon.svg"}}\n未配置的主机名使用全局默认值', - verbose_name="主机名品牌绑定", - ), - ), - ] diff --git a/apps/dashboard/migrations/0014_sitegroup_sitegrouphostname_and_more.py b/apps/dashboard/migrations/0014_sitegroup_sitegrouphostname_and_more.py deleted file mode 100644 index f50e22a..0000000 --- a/apps/dashboard/migrations/0014_sitegroup_sitegrouphostname_and_more.py +++ /dev/null @@ -1,148 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 13:27 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("dashboard", "0013_systemconfig_hostname_branding"), - ] - - operations = [ - migrations.CreateModel( - name="SiteGroup", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField( - help_text="站点组的显示名称", - max_length=100, - verbose_name="站点组名称", - ), - ), - ( - "slug", - models.SlugField( - help_text="唯一标识符,用于URL和内部引用", - max_length=100, - unique=True, - verbose_name="标识符", - ), - ), - ( - "description", - models.TextField( - blank=True, help_text="站点组的描述信息", verbose_name="描述" - ), - ), - ( - "site_name", - models.CharField( - blank=True, - help_text="该站点组的站点名称,留空则使用全局默认值", - max_length=100, - verbose_name="站点名称", - ), - ), - ( - "site_icon", - models.CharField( - blank=True, - help_text="该站点组的站点图标路径,留空则使用全局默认值", - max_length=500, - verbose_name="站点图标", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="禁用后该站点组的所有功能将不可用", - verbose_name="是否启用", - ), - ), - ( - "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), - ), - ( - "updated_at", - models.DateTimeField(auto_now=True, verbose_name="更新时间"), - ), - ( - "admins", - models.ManyToManyField( - blank=True, - help_text="该站点组的管理员,在当前站点组内拥有类似超级管理员的权限", - related_name="admin_site_groups", - to=settings.AUTH_USER_MODEL, - verbose_name="站点组管理员", - ), - ), - ], - options={ - "verbose_name": "站点组", - "verbose_name_plural": "站点组", - "ordering": ["name"], - }, - ), - migrations.CreateModel( - name="SiteGroupHostname", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "hostname", - models.CharField( - help_text="HTTP Host头中的主机名(不含端口),如 demo.example.com", - max_length=255, - unique=True, - verbose_name="主机名", - ), - ), - ( - "site_group", - models.ForeignKey( - help_text="该主机名所属的站点组", - on_delete=django.db.models.deletion.CASCADE, - related_name="hostnames", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ], - options={ - "verbose_name": "站点组主机名", - "verbose_name_plural": "站点组主机名", - "indexes": [ - models.Index( - fields=["hostname"], name="dashboard_s_hostnam_daee7a_idx" - ) - ], - }, - ), - migrations.AddIndex( - model_name="sitegroup", - index=models.Index(fields=["slug"], name="dashboard_s_slug_5cb7f5_idx"), - ), - ] diff --git a/apps/dashboard/migrations/0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more.py b/apps/dashboard/migrations/0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more.py deleted file mode 100644 index e70c3bc..0000000 --- a/apps/dashboard/migrations/0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-01 13:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0014_sitegroup_sitegrouphostname_and_more"), - ] - - operations = [ - migrations.RemoveIndex( - model_name="sitegroup", - name="dashboard_s_slug_5cb7f5_idx", - ), - migrations.RemoveIndex( - model_name="sitegrouphostname", - name="dashboard_s_hostnam_daee7a_idx", - ), - ] diff --git a/apps/dashboard/migrations/0016_systemconfig_smtp_from_name.py b/apps/dashboard/migrations/0016_systemconfig_smtp_from_name.py deleted file mode 100644 index 3c09949..0000000 --- a/apps/dashboard/migrations/0016_systemconfig_smtp_from_name.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-06 17:23 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="smtp_from_name", - field=models.CharField( - blank=True, - help_text='系统发送邮件时显示的发件人名称,如"XX云服务"', - max_length=255, - null=True, - verbose_name="发件人名称", - ), - ), - ] diff --git a/apps/dashboard/migrations/0017_remove_systemconfig_smtp_use_tls_and_more.py b/apps/dashboard/migrations/0017_remove_systemconfig_smtp_use_tls_and_more.py deleted file mode 100644 index 4bcf781..0000000 --- a/apps/dashboard/migrations/0017_remove_systemconfig_smtp_use_tls_and_more.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-06 17:56 - -from django.db import migrations, models - - -def migrate_smtp_use_tls_to_encryption(apps, schema_editor): - SystemConfig = apps.get_model('dashboard', 'SystemConfig') - for config in SystemConfig.objects.all(): - if config.smtp_use_tls: - config.smtp_encryption = 'TLS' - else: - config.smtp_encryption = 'NONE' - config.save(update_fields=['smtp_encryption']) - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0016_systemconfig_smtp_from_name"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="smtp_encryption", - field=models.CharField( - choices=[ - ("NONE", "无加密"), - ("TLS", "TLS (STARTTLS)"), - ("SSL", "SSL (SMTPS)"), - ], - default="TLS", - help_text="TLS: 端口通常为587;SSL: 端口通常为465", - max_length=8, - verbose_name="加密方式", - ), - ), - migrations.RunPython( - migrate_smtp_use_tls_to_encryption, - migrations.RunPython.noop, - ), - migrations.RemoveField( - model_name="systemconfig", - name="smtp_use_tls", - ), - ] diff --git a/apps/dashboard/migrations/0018_sitegroupconfig.py b/apps/dashboard/migrations/0018_sitegroupconfig.py deleted file mode 100644 index 4a04657..0000000 --- a/apps/dashboard/migrations/0018_sitegroupconfig.py +++ /dev/null @@ -1,226 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 06:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0017_remove_systemconfig_smtp_use_tls_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="SiteGroupConfig", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "smtp_host", - models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=255, - null=True, - verbose_name="SMTP服务器", - ), - ), - ( - "smtp_port", - models.IntegerField( - blank=True, - help_text="留空使用全局配置", - null=True, - verbose_name="SMTP端口", - ), - ), - ( - "smtp_encryption", - models.CharField( - blank=True, - choices=[ - ("NONE", "无加密"), - ("TLS", "TLS (STARTTLS)"), - ("SSL", "SSL (SMTPS)"), - ], - help_text="留空使用全局配置", - max_length=8, - null=True, - verbose_name="加密方式", - ), - ), - ( - "smtp_username", - models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=255, - null=True, - verbose_name="SMTP用户名", - ), - ), - ( - "smtp_password", - models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=255, - null=True, - verbose_name="SMTP密码", - ), - ), - ( - "smtp_from_email", - models.EmailField( - blank=True, - help_text="留空使用全局配置", - max_length=254, - null=True, - verbose_name="发件人邮箱", - ), - ), - ( - "smtp_from_name", - models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=255, - null=True, - verbose_name="发件人名称", - ), - ), - ( - "captcha_provider", - models.CharField( - blank=True, - choices=[("none", "无"), ("tianai", "天爱验证码")], - help_text="留空使用全局配置", - max_length=32, - null=True, - verbose_name="验证码提供器", - ), - ), - ( - "captcha_type", - models.CharField( - blank=True, - choices=[ - ("SLIDER", "滑块验证"), - ("ROTATE", "旋转验证"), - ("CONCAT", "滑动还原"), - ("WORD_IMAGE_CLICK", "文字点选"), - ], - help_text="留空使用全局配置", - max_length=32, - null=True, - verbose_name="默认验证码类型", - ), - ), - ( - "login_captcha_type", - models.CharField( - blank=True, - choices=[ - ("SLIDER", "滑块验证"), - ("ROTATE", "旋转验证"), - ("CONCAT", "滑动还原"), - ("WORD_IMAGE_CLICK", "文字点选"), - ], - help_text="留空使用全局配置", - max_length=32, - null=True, - verbose_name="登录验证码类型", - ), - ), - ( - "register_captcha_type", - models.CharField( - blank=True, - choices=[ - ("SLIDER", "滑块验证"), - ("ROTATE", "旋转验证"), - ("CONCAT", "滑动还原"), - ("WORD_IMAGE_CLICK", "文字点选"), - ], - help_text="留空使用全局配置", - max_length=32, - null=True, - verbose_name="注册验证码类型", - ), - ), - ( - "email_captcha_type", - models.CharField( - blank=True, - choices=[ - ("SLIDER", "滑块验证"), - ("ROTATE", "旋转验证"), - ("CONCAT", "滑动还原"), - ("WORD_IMAGE_CLICK", "文字点选"), - ], - help_text="留空使用全局配置", - max_length=32, - null=True, - verbose_name="邮箱验证码类型", - ), - ), - ( - "enable_registration", - models.BooleanField( - blank=True, - help_text="留空使用全局配置", - null=True, - verbose_name="启用用户注册", - ), - ), - ( - "email_suffix_whitelist", - models.TextField( - blank=True, - help_text="留空使用全局配置。每行一个后缀", - null=True, - verbose_name="邮箱后缀白名单", - ), - ), - ( - "email_suffix_blacklist", - models.TextField( - blank=True, - help_text="留空使用全局配置。每行一个后缀", - null=True, - verbose_name="邮箱后缀黑名单", - ), - ), - ( - "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), - ), - ( - "updated_at", - models.DateTimeField(auto_now=True, verbose_name="更新时间"), - ), - ( - "site_group", - models.OneToOneField( - help_text="关联的站点组", - on_delete=django.db.models.deletion.CASCADE, - related_name="config", - to="dashboard.sitegroup", - verbose_name="站点组", - ), - ), - ], - options={ - "verbose_name": "站点组配置", - "verbose_name_plural": "站点组配置", - }, - ), - ] diff --git a/apps/dashboard/migrations/0019_sitegroupconfig_icp_number_and_more.py b/apps/dashboard/migrations/0019_sitegroupconfig_icp_number_and_more.py deleted file mode 100644 index a6921e7..0000000 --- a/apps/dashboard/migrations/0019_sitegroupconfig_icp_number_and_more.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 10:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0018_sitegroupconfig"), - ] - - operations = [ - migrations.AddField( - model_name="sitegroupconfig", - name="icp_number", - field=models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=100, - null=True, - verbose_name="ICP备案号", - ), - ), - migrations.AddField( - model_name="sitegroupconfig", - name="police_number", - field=models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=100, - null=True, - verbose_name="公安备案号", - ), - ), - migrations.AddField( - model_name="sitegroupconfig", - name="site_icon", - field=models.CharField( - blank=True, - help_text="留空使用全局配置。图标路径,如 /media/branding/icon.svg", - max_length=500, - null=True, - verbose_name="站点图标", - ), - ), - migrations.AddField( - model_name="sitegroupconfig", - name="site_name", - field=models.CharField( - blank=True, - help_text="留空使用全局配置", - max_length=100, - null=True, - verbose_name="站点名称", - ), - ), - ] diff --git a/apps/dashboard/migrations/0020_systemconfig_site_icon.py b/apps/dashboard/migrations/0020_systemconfig_site_icon.py deleted file mode 100644 index 0af5959..0000000 --- a/apps/dashboard/migrations/0020_systemconfig_site_icon.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 10:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0019_sitegroupconfig_icp_number_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="systemconfig", - name="site_icon", - field=models.CharField( - blank=True, - default="", - help_text="站点图标路径,如 /media/branding/icon.svg,留空使用默认图标", - max_length=500, - verbose_name="站点图标", - ), - ), - ] diff --git a/apps/dashboard/migrations/__init__.py b/apps/dashboard/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/dashboard/models.py b/apps/dashboard/models.py deleted file mode 100755 index 99d3b50..0000000 --- a/apps/dashboard/models.py +++ /dev/null @@ -1,540 +0,0 @@ -""" -仪表盘数据模型 -""" -from django.db import models -from django.conf import settings -from django.contrib.auth import get_user_model - -User = get_user_model() - - -class DashboardWidget(models.Model): - """ - 仪表盘组件模型 - 用于配置仪表盘上的各种组件 - """ - class Meta: - verbose_name = '仪表盘组件' - verbose_name_plural = verbose_name - ordering = ['display_order'] - indexes = [ - models.Index(fields=['widget_type']), - models.Index(fields=['is_enabled']), - models.Index(fields=['display_order']), - ] - - WIDGET_TYPES = ( - ('stat_card', '统计卡片'), - ('chart', '图表'), - ('recent_operations', '最近操作'), - ('host_status', '主机状态'), - ('system_alerts', '系统告警'), - ) - - widget_type = models.CharField( - '组件类型', - max_length=50, - choices=WIDGET_TYPES, - help_text='组件的类型' - ) - title = models.CharField( - '标题', - max_length=200, - help_text='组件显示的标题' - ) - display_order = models.IntegerField( - '显示顺序', - default=0, - help_text='组件在仪表盘上的显示顺序' - ) - is_enabled = models.BooleanField( - '是否启用', - default=True, - help_text='组件是否在仪表盘上显示' - ) - widget_config = models.JSONField( - '组件配置', - default=dict, - blank=True, - help_text='组件的配置参数' - ) - created_at = models.DateTimeField( - '创建时间', - auto_now_add=True, - help_text='组件创建时间' - ) - updated_at = models.DateTimeField( - '更新时间', - auto_now=True, - help_text='组件更新时间' - ) - - def __str__(self): - return self.title - - -class SystemConfig(models.Model): - """ - 系统配置模型 - - 用于存储系统的全局配置,如SMTP服务器、验证码服务等 - """ - # SMTP配置 - smtp_host = models.CharField( - max_length=255, - blank=True, - null=True, - verbose_name='SMTP服务器', - help_text='SMTP服务器地址,如smtp.gmail.com' - ) - smtp_port = models.IntegerField( - blank=True, - null=True, - verbose_name='SMTP端口', - help_text='SMTP服务器端口,通常为587或465' - ) - SMTP_ENCRYPTION_TYPES = ( - ('NONE', '无加密'), - ('TLS', 'TLS (STARTTLS)'), - ('SSL', 'SSL (SMTPS)'), - ) - - smtp_encryption = models.CharField( - max_length=8, - choices=SMTP_ENCRYPTION_TYPES, - default='TLS', - verbose_name='加密方式', - help_text='TLS: 端口通常为587;SSL: 端口通常为465' - ) - smtp_username = models.CharField( - max_length=255, - blank=True, - null=True, - verbose_name='SMTP用户名', - help_text='SMTP登录用户名,通常是邮箱地址' - ) - smtp_password = models.CharField( - max_length=255, - blank=True, - null=True, - verbose_name='SMTP密码', - help_text='SMTP登录密码或应用专用密码' - ) - smtp_from_email = models.EmailField( - blank=True, - null=True, - verbose_name='发件人邮箱', - help_text='系统发送邮件时使用的发件人地址' - ) - smtp_from_name = models.CharField( - max_length=255, - blank=True, - null=True, - verbose_name='发件人名称', - help_text='系统发送邮件时显示的发件人名称,如"XX云服务"' - ) - - CAPTCHA_TYPES = ( - ('SLIDER', '滑块验证'), - ('ROTATE', '旋转验证'), - ('CONCAT', '滑动还原'), - ('WORD_IMAGE_CLICK', '文字点选'), - ) - - captcha_provider = models.CharField( - max_length=32, - choices=( - ('none', '无'), - ('tianai', '天爱验证码'), - ), - default='none', - verbose_name='验证码提供器', - help_text='选择要启用的验证码提供器' - ) - captcha_type = models.CharField( - max_length=32, - choices=CAPTCHA_TYPES, - default='SLIDER', - verbose_name='默认验证码类型', - help_text='全局默认的验证码类型' - ) - login_captcha_type = models.CharField( - max_length=32, - choices=CAPTCHA_TYPES, - blank=True, - null=True, - verbose_name='登录验证码类型', - help_text='登录场景的验证码类型(留空则使用默认类型)' - ) - register_captcha_type = models.CharField( - max_length=32, - choices=CAPTCHA_TYPES, - blank=True, - null=True, - verbose_name='注册验证码类型', - help_text='注册场景的验证码类型(留空则使用默认类型)' - ) - email_captcha_type = models.CharField( - max_length=32, - choices=CAPTCHA_TYPES, - blank=True, - null=True, - verbose_name='邮箱验证码类型', - help_text='邮箱发送验证码场景的验证码类型(留空则使用默认类型)' - ) - - # 其他配置 - site_name = models.CharField( - max_length=100, - default='2c2a', - verbose_name='站点名称', - help_text='系统显示的站点名称' - ) - site_icon = models.CharField( - max_length=500, - blank=True, - default='', - verbose_name='站点图标', - help_text='站点图标路径,如 /media/branding/icon.svg,留空使用默认图标' - ) - - # 注册开关 - enable_registration = models.BooleanField( - default=False, - verbose_name='启用用户注册', - help_text='是否开启用户注册功能,默认为关闭' - ) - - # ICP备案号配置 - icp_number = models.CharField( - max_length=100, - blank=True, - null=True, - verbose_name='ICP备案号', - help_text='ICP备案号,例如:京ICP备12345678号' - ) - - # 公安备案号配置 - police_number = models.CharField( - max_length=100, - blank=True, - null=True, - verbose_name='公安备案号', - help_text='公安备案号,例如:京公网安备 11010502000000号' - ) - - email_suffix_whitelist = models.TextField( - blank=True, - null=True, - verbose_name='邮箱后缀白名单', - help_text=( - '允许注册的邮箱后缀列表,每行一个后缀,' - '例如:\n@example.com\n@gmail.com\n@company.com\n' - '留空表示不限制' - ) - ) - email_suffix_blacklist = models.TextField( - blank=True, - null=True, - verbose_name='邮箱后缀黑名单', - help_text=( - '禁止注册的邮箱后缀列表,每行一个后缀,' - '例如:\n@tempmail.com\n@spam.com\n' - '留空表示不限制' - ) - ) - - local_access_locked = models.BooleanField( - default=False, - verbose_name='禁止本地访问', - help_text='启用后将禁止来自 localhost/127.0.0.1 的访问' - ) - - hostname_branding = models.JSONField( - default=dict, - blank=True, - verbose_name='主机名品牌绑定', - help_text=( - '按主机名绑定专用站点名和图标,格式:\n' - '{"host.example.com": {"site_name": "站点名", "site_icon": "/media/branding/icon.svg"}}\n' - '未配置的主机名使用全局默认值' - ) - ) - - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name='创建时间' - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name='更新时间' - ) - - class Meta: - verbose_name = '系统配置' - verbose_name_plural = '系统配置' - - def __str__(self): - return f'{self.site_name} 配置' - - def clean(self): - pass - - @classmethod - def get_config(cls): - """获取当前系统配置(带缓存)""" - from django.core.cache import cache - cache_key = 'system_config:singleton' - config = cache.get(cache_key) - if config is not None: - return config - config, created = cls.objects.get_or_create(pk=1) - cache.set(cache_key, config, timeout=300) - return config - - def save(self, *args, **kwargs): - from django.core.cache import cache - result = super().save(*args, **kwargs) - cache.delete('system_config:singleton') - return result - - def delete(self, *args, **kwargs): - from django.core.cache import cache - result = super().delete(*args, **kwargs) - cache.delete('system_config:singleton') - return result - - def get_captcha_config(self, scene=None): - provider = self.captcha_provider - if scene == 'login': - captcha_type = self.login_captcha_type or self.captcha_type - elif scene == 'register': - captcha_type = self.register_captcha_type or self.captcha_type - elif scene == 'email': - captcha_type = self.email_captcha_type or self.captcha_type - else: - captcha_type = self.captcha_type - return provider, captcha_type - - def get_branding_for_hostname(self, hostname): - if self.hostname_branding and hostname in self.hostname_branding: - return self.hostname_branding[hostname] - return {} - - def get_site_name_for_hostname(self, hostname): - branding = self.get_branding_for_hostname(hostname) - return branding.get('site_name') or self.site_name - - def get_site_icon_for_hostname(self, hostname): - branding = self.get_branding_for_hostname(hostname) - return branding.get('site_icon') or '/static/img/favicon.svg' - - -class SiteGroup(models.Model): - name = models.CharField('站点组名称', max_length=100, help_text='站点组的显示名称') - slug = models.SlugField('标识符', max_length=100, unique=True, help_text='唯一标识符,用于URL和内部引用') - description = models.TextField('描述', blank=True, help_text='站点组的描述信息') - site_name = models.CharField('站点名称', max_length=100, blank=True, help_text='该站点组的站点名称,留空则使用全局默认值') - site_icon = models.CharField('站点图标', max_length=500, blank=True, help_text='该站点组的站点图标路径,留空则使用全局默认值') - is_active = models.BooleanField('是否启用', default=True, help_text='禁用后该站点组的所有功能将不可用') - admins = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - related_name='admin_site_groups', - verbose_name='站点组管理员', - help_text='该站点组的管理员,在当前站点组内拥有类似超级管理员的权限', - ) - created_at = models.DateTimeField('创建时间', auto_now_add=True) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - class Meta: - verbose_name = '站点组' - verbose_name_plural = '站点组' - ordering = ['name'] - - def __str__(self): - return self.name - - -class SiteGroupConfig(models.Model): - """ - 站点组配置覆盖模型 - - 允许每个站点组覆盖 SystemConfig 中的配置项。 - 字段留空(null)表示使用 SystemConfig 的全局默认值。 - """ - site_group = models.OneToOneField( - SiteGroup, - on_delete=models.CASCADE, - related_name='config', - verbose_name='站点组', - help_text='关联的站点组', - ) - - # SMTP 配置覆盖 - smtp_host = models.CharField( - max_length=255, blank=True, null=True, - verbose_name='SMTP服务器', - help_text='留空使用全局配置', - ) - smtp_port = models.IntegerField( - blank=True, null=True, - verbose_name='SMTP端口', - help_text='留空使用全局配置', - ) - smtp_encryption = models.CharField( - max_length=8, - choices=SystemConfig.SMTP_ENCRYPTION_TYPES, - blank=True, null=True, - verbose_name='加密方式', - help_text='留空使用全局配置', - ) - smtp_username = models.CharField( - max_length=255, blank=True, null=True, - verbose_name='SMTP用户名', - help_text='留空使用全局配置', - ) - smtp_password = models.CharField( - max_length=255, blank=True, null=True, - verbose_name='SMTP密码', - help_text='留空使用全局配置', - ) - smtp_from_email = models.EmailField( - blank=True, null=True, - verbose_name='发件人邮箱', - help_text='留空使用全局配置', - ) - smtp_from_name = models.CharField( - max_length=255, blank=True, null=True, - verbose_name='发件人名称', - help_text='留空使用全局配置', - ) - - # 验证码配置覆盖 - captcha_provider = models.CharField( - max_length=32, - choices=(('none', '无'), ('tianai', '天爱验证码')), - blank=True, null=True, - verbose_name='验证码提供器', - help_text='留空使用全局配置', - ) - captcha_type = models.CharField( - max_length=32, - choices=SystemConfig.CAPTCHA_TYPES, - blank=True, null=True, - verbose_name='默认验证码类型', - help_text='留空使用全局配置', - ) - login_captcha_type = models.CharField( - max_length=32, - choices=SystemConfig.CAPTCHA_TYPES, - blank=True, null=True, - verbose_name='登录验证码类型', - help_text='留空使用全局配置', - ) - register_captcha_type = models.CharField( - max_length=32, - choices=SystemConfig.CAPTCHA_TYPES, - blank=True, null=True, - verbose_name='注册验证码类型', - help_text='留空使用全局配置', - ) - email_captcha_type = models.CharField( - max_length=32, - choices=SystemConfig.CAPTCHA_TYPES, - blank=True, null=True, - verbose_name='邮箱验证码类型', - help_text='留空使用全局配置', - ) - - # 注册与邮箱配置覆盖 - enable_registration = models.BooleanField( - blank=True, null=True, - verbose_name='启用用户注册', - help_text='留空使用全局配置', - ) - email_suffix_whitelist = models.TextField( - blank=True, null=True, - verbose_name='邮箱后缀白名单', - help_text='留空使用全局配置。每行一个后缀', - ) - email_suffix_blacklist = models.TextField( - blank=True, null=True, - verbose_name='邮箱后缀黑名单', - help_text='留空使用全局配置。每行一个后缀', - ) - - # 站点外观配置覆盖 - site_name = models.CharField( - max_length=100, blank=True, null=True, - verbose_name='站点名称', - help_text='留空使用全局配置', - ) - site_icon = models.CharField( - max_length=500, blank=True, null=True, - verbose_name='站点图标', - help_text='留空使用全局配置。图标路径,如 /media/branding/icon.svg', - ) - icp_number = models.CharField( - max_length=100, blank=True, null=True, - verbose_name='ICP备案号', - help_text='留空使用全局配置', - ) - police_number = models.CharField( - max_length=100, blank=True, null=True, - verbose_name='公安备案号', - help_text='留空使用全局配置', - ) - - created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') - - class Meta: - verbose_name = '站点组配置' - verbose_name_plural = '站点组配置' - - def __str__(self): - return f'{self.site_group.name} 配置' - - def save(self, *args, **kwargs): - from django.core.cache import cache - result = super().save(*args, **kwargs) - cache.delete(f'site_group_config:{self.site_group_id}') - return result - - def delete(self, *args, **kwargs): - from django.core.cache import cache - cache.delete(f'site_group_config:{self.site_group_id}') - return super().delete(*args, **kwargs) - - @classmethod - def get_config(cls, site_group): - """获取站点组配置(带缓存)""" - if site_group is None: - return None - from django.core.cache import cache - cache_key = f'site_group_config:{site_group.pk}' - config = cache.get(cache_key) - if config is not None: - return config - config, _ = cls.objects.get_or_create(site_group=site_group) - cache.set(cache_key, config, timeout=300) - return config - - -class SiteGroupHostname(models.Model): - hostname = models.CharField('主机名', max_length=255, unique=True, help_text='HTTP Host头中的主机名(不含端口),如 demo.example.com') - site_group = models.ForeignKey( - SiteGroup, - on_delete=models.CASCADE, - related_name='hostnames', - verbose_name='所属站点组', - help_text='该主机名所属的站点组', - ) - - class Meta: - verbose_name = '站点组主机名' - verbose_name_plural = '站点组主机名' - - def __str__(self): - return f'{self.hostname} -> {self.site_group.name}' diff --git a/apps/dashboard/signals.py b/apps/dashboard/signals.py deleted file mode 100644 index c7dca0c..0000000 --- a/apps/dashboard/signals.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.dispatch import Signal - -system_config_saved = Signal() diff --git a/apps/dashboard/templatetags/__init__.py b/apps/dashboard/templatetags/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/dashboard/templatetags/markdown_extras.py b/apps/dashboard/templatetags/markdown_extras.py deleted file mode 100755 index 92a8f0f..0000000 --- a/apps/dashboard/templatetags/markdown_extras.py +++ /dev/null @@ -1,48 +0,0 @@ -import markdown -from django import template -from django.utils.safestring import mark_safe - -register = template.Library() - - -@register.filter -def get_item(dictionary, key): - if dictionary is None: - return None - return dictionary.get(key) - - -@register.filter -def markdown_filter(value): - """ - 将 Markdown 文本转换为 HTML - """ - if not value: - return value - - md = markdown.Markdown(extensions=[ - 'extra', - 'codehilite', - 'tables', - 'toc', - ]) - html = md.convert(value) - return mark_safe(html) - - -@register.simple_tag -def markdown_render(text): - """ - 渲染 Markdown 文本的简单标签 - """ - if not text: - return "" - - md = markdown.Markdown(extensions=[ - 'extra', - 'codehilite', - 'tables', - 'toc', - ]) - html = md.convert(text) - return mark_safe(html) \ No newline at end of file diff --git a/apps/dashboard/urls.py b/apps/dashboard/urls.py deleted file mode 100755 index 7e79e09..0000000 --- a/apps/dashboard/urls.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -仪表盘URL配置 -""" - -from django.urls import path -from . import views -from . import views_sitegroup -from . import views_sitegroup_users - -app_name = "dashboard" - -urlpatterns = [ - path("", views.DashboardView.as_view(), name="index"), - path("widget-config/", views.WidgetConfigView.as_view(), name="widget_config"), - path("api/stats/", views.StatsAPIView.as_view(), name="stats_api"), - path( - "api/widget-config/", views.WidgetConfigView.as_view(), name="widget_config_api" - ), - path("sitegroup/", views_sitegroup.sitegroup_list, name="sitegroup_list"), - path( - "sitegroup/create/", views_sitegroup.sitegroup_create, name="sitegroup_create" - ), - path( - "sitegroup//", views_sitegroup.sitegroup_detail, name="sitegroup_detail" - ), - path( - "sitegroup//update/", - views_sitegroup.sitegroup_update, - name="sitegroup_update", - ), - path( - "sitegroup//delete/", - views_sitegroup.sitegroup_delete, - name="sitegroup_delete", - ), - path( - "sitegroup//add-hostname/", - views_sitegroup.sitegroup_add_hostname, - name="sitegroup_add_hostname", - ), - path( - "sitegroup//remove-hostname//", - views_sitegroup.sitegroup_remove_hostname, - name="sitegroup_remove_hostname", - ), - path( - "sitegroup//add-admin/", - views_sitegroup.sitegroup_add_admin, - name="sitegroup_add_admin", - ), - path( - "sitegroup//remove-admin//", - views_sitegroup.sitegroup_remove_admin, - name="sitegroup_remove_admin", - ), - path( - "sitegroup//config/", - views_sitegroup.sitegroup_config, - name="sitegroup_config", - ), - # 站点组用户管理 - path( - "sitegroup/users/", - views_sitegroup_users.sitegroup_user_list, - name="sitegroup_user_list", - ), - path( - "sitegroup/users//toggle-active/", - views_sitegroup_users.sitegroup_user_toggle_active, - name="sitegroup_user_toggle_active", - ), - path( - "sitegroup/users//reset-password/", - views_sitegroup_users.sitegroup_user_reset_password, - name="sitegroup_user_reset_password", - ), - path( - "sitegroup/users//remove/", - views_sitegroup_users.sitegroup_user_remove, - name="sitegroup_user_remove", - ), - # 站点组管理员配置(无需 pk,使用 request.site_group) - path( - "sitegroup/my-config/", - views_sitegroup.sitegroup_my_config, - name="sitegroup_my_config", - ), -] diff --git a/apps/dashboard/urls_admin.py b/apps/dashboard/urls_admin.py deleted file mode 100644 index a4b515a..0000000 --- a/apps/dashboard/urls_admin.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.urls import path - -from .views_admin import ( - widget_list, - widget_create, - widget_edit, - widget_delete, - systemconfig_edit, - systemconfig_send_test_email, - test_email_progress, - test_email_sse, -) - -app_name = 'admin_dashboard_config' - -urlpatterns = [ - path('widgets/', widget_list, name='widget_list'), - path('widgets/create/', widget_create, name='widget_create'), - path('widgets//edit/', widget_edit, name='widget_edit'), - path('widgets//delete/', widget_delete, name='widget_delete'), - path('config/', systemconfig_edit, name='systemconfig_edit'), - path( - 'config/send-test-email/', - systemconfig_send_test_email, - name='systemconfig_send_test_email', - ), - path( - 'config/test-email//', - test_email_progress, - name='test_email_progress', - ), - path( - 'config/test-email//sse/', - test_email_sse, - name='test_email_sse', - ), -] diff --git a/apps/dashboard/views.py b/apps/dashboard/views.py deleted file mode 100755 index 18a372e..0000000 --- a/apps/dashboard/views.py +++ /dev/null @@ -1,433 +0,0 @@ -""" -仪表盘视图 -""" - -from typing import Any -from django.shortcuts import render, redirect -from django.views import View -from django.views.generic import TemplateView -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.http import JsonResponse -from django.db.models import Count, Q -from django.utils import timezone -from datetime import timedelta -from django.contrib.auth import get_user_model -from django.contrib import messages - -from apps.hosts.models import Host -from apps.operations.models import ( - AccountOpeningRequest, - CloudComputerUser, - Product, - ProductGroup, - ProductAccessGrant, -) -from apps.audit.models import AuditLog -from .models import DashboardWidget, SystemConfig -from .forms import SystemConfigForm -from utils.helpers import get_client_ip - -User = get_user_model() - - -class DashboardView(LoginRequiredMixin, TemplateView): - """ - 仪表盘主视图 - 展示机器一览和注册主机入口 - """ - - template_name = "dashboard/index.html" - - def get_context_data(self, **kwargs): - """获取仪表盘上下文数据""" - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, 'site_group', None) - if site_group: - product_groups = ProductGroup.objects.filter( - is_active=True, site_group=site_group - ).order_by( - "display_order", "name" - ) - else: - product_groups = ProductGroup.objects.filter(is_active=True, site_group__isnull=True).order_by( - "display_order", "name" - ) - - if site_group: - products_qs = Product.objects.filter( - is_available=True, site_group=site_group - ).select_related( - "host", "product_group" - ) - else: - products_qs = Product.objects.filter(is_available=True, site_group__isnull=True).select_related( - "host", "product_group" - ) - - search = self.request.GET.get("search", "") - if search: - products_qs = products_qs.filter( - Q(display_name__icontains=search) - | Q(display_description__icontains=search) - | Q(name__icontains=search) - ) - - status_filter = self.request.GET.get("status", "") - if status_filter: - products_qs = products_qs.filter(host__status=status_filter) - - group_filter = self.request.GET.get("group", "") - if group_filter: - products_qs = products_qs.filter(product_group_id=group_filter) - - auto_approval_filter = self.request.GET.get("auto_approval", "") - if auto_approval_filter == "true": - products_qs = products_qs.filter(auto_approval=True) - elif auto_approval_filter == "false": - products_qs = products_qs.filter(auto_approval=False) - - # 邀请访问权限过滤 - user = self.request.user - if not user.is_staff and not user.is_superuser: - # 获取用户有效的产品授权 - granted_product_ids = set( - ProductAccessGrant.objects.filter( - user=user, - product__isnull=False, - is_revoked=False, - ).exclude( - expires_at__lt=timezone.now() - ).values_list('product_id', flat=True) - ) - # 获取用户有效的产品组授权 - granted_group_ids = set( - ProductAccessGrant.objects.filter( - user=user, - product_group__isnull=False, - is_revoked=False, - ).exclude( - expires_at__lt=timezone.now() - ).values_list('product_group_id', flat=True) - ) - # 提供商可以看到自己创建的所有产品 - provider_created_ids = set() - if hasattr(user, 'created_products'): - provider_created_ids = set( - Product.objects.filter(created_by=user).values_list('id', flat=True) - ) - - # 过滤:公开产品 或 已授权产品 或 已授权产品组下的产品 或 提供商自己创建的产品 - products_qs = products_qs.filter( - Q(visibility='public') | - Q(id__in=granted_product_ids) | - Q(product_group_id__in=granted_group_ids) | - Q(id__in=provider_created_ids) - ) - - all_products = list(products_qs.order_by("-created_at")) - - user = self.request.user - existing_cloud_users = {} - if not (user.is_staff or user.is_superuser): - cloud_user_qs = CloudComputerUser.objects.filter( - Q(owner=user) | Q(created_from_request__applicant=user), - status__in=['active', 'inactive', 'disabled'], - ).values_list('product_id', 'pk') - for product_id, cloud_user_pk in cloud_user_qs: - if product_id not in existing_cloud_users: - existing_cloud_users[product_id] = cloud_user_pk - - pending_request_ids = {} - if not (user.is_staff or user.is_superuser): - request_qs = AccountOpeningRequest.objects.filter( - applicant=user, - status__in=['pending', 'approved', 'processing'], - ).values_list('target_product_id', 'pk') - for product_id, request_pk in request_qs: - if product_id not in pending_request_ids: - pending_request_ids[product_id] = request_pk - - grouped_products: list[dict[str, Any]] = [] - for group in product_groups: - products = [p for p in all_products if p.product_group_id == group.id] - if products: - grouped_products.append({"group": group, "products": products}) - - ungrouped = [p for p in all_products if p.product_group_id is None] - if ungrouped: - grouped_products.append({"group": None, "products": ungrouped}) - - context["existing_cloud_users"] = existing_cloud_users - context["pending_request_ids"] = pending_request_ids - - context["grouped_products"] = grouped_products - - context["products"] = all_products - - context["public_hosts"] = all_products - - context["product_groups"] = product_groups - context["status_choices"] = Host._meta.get_field("status").choices - context["search"] = search - context["status_filter"] = status_filter - context["group_filter"] = group_filter - context["auto_approval_filter"] = auto_approval_filter - - if site_group: - stats = AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ).aggregate( - pending_count=Count("id", filter=Q(status="pending")), - ) - context["cloud_users_total"] = CloudComputerUser.objects.filter( - product__site_group=site_group - ).count() - else: - stats = AccountOpeningRequest.objects.filter(target_product__site_group__isnull=True).aggregate( - pending_count=Count("id", filter=Q(status="pending")), - ) - context["cloud_users_total"] = CloudComputerUser.objects.filter(product__site_group__isnull=True).count() - context["account_requests_pending"] = stats["pending_count"] - - if self.request.user.is_staff or self.request.user.is_superuser: - if site_group: - context["account_requests_recent"] = ( - AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ).select_related( - "applicant", "target_product", "target_product__host" - ).order_by("-created_at")[:5] - ) - else: - context["account_requests_recent"] = ( - AccountOpeningRequest.objects.filter(target_product__site_group__isnull=True).select_related( - "applicant", "target_product", "target_product__host" - ).order_by("-created_at")[:5] - ) - else: - if site_group: - context["account_requests_recent"] = AccountOpeningRequest.objects.filter( - applicant=self.request.user, - ).filter( - target_product__site_group=site_group - ).select_related( - "applicant", "target_product", "target_product__host" - ).order_by("-created_at")[:5] - else: - context["account_requests_recent"] = AccountOpeningRequest.objects.filter( - applicant=self.request.user, target_product__site_group__isnull=True - ).select_related( - "applicant", "target_product", "target_product__host" - ).order_by("-created_at")[:5] - - try: - AuditLog.objects.create( - user=self.request.user, - action="dashboard_view", - description="访问仪表盘", - ip_address=get_client_ip(self.request), - user_agent=self.request.META.get("HTTP_USER_AGENT", ""), - ) - except Exception: - pass - - return context - - -class StatsAPIView(LoginRequiredMixin, View): - """提供JSON格式的统计数据""" - - def get(self, request, *args, **kwargs): - """获取统计数据""" - stats_type = request.GET.get("type", "all") - site_group = getattr(request, 'site_group', None) - - if stats_type == "all": - data = self._get_all_stats(site_group) - elif stats_type == "hosts": - data = self._get_host_stats(site_group) - elif stats_type == "operations": - data = self._get_operation_stats() - elif stats_type == "users": - data = self._get_user_stats() - elif stats_type == "account_opening": - data = self._get_account_opening_stats(site_group) - else: - data = {"error": "Invalid stats type"} - - return JsonResponse(data) - - def _get_all_stats(self, site_group): - """获取所有统计数据""" - return { - "hosts": self._get_host_stats(site_group), - "operations": self._get_operation_stats(), - "users": self._get_user_stats(), - "account_opening": self._get_account_opening_stats(site_group), - } - - def _get_host_stats(self, site_group): - """获取主机统计""" - from django.db.models import Count, Q - if site_group: - host_qs = Host.objects.filter(site_group=site_group) - else: - host_qs = Host.objects.filter(site_group__isnull=True) - stats = host_qs.aggregate( - total=Count('id'), - online=Count('id', filter=Q(status='online')), - offline=Count('id', filter=Q(status='offline')), - error=Count('id', filter=Q(status='error')), - ) - by_type = dict( - host_qs.values("connection_type") - .annotate(count=Count("id")) - .values_list("connection_type", "count") - ) - stats["by_type"] = by_type - return stats - - def _get_operation_stats(self): - """获取操作统计""" - # 由于已移除 OperationLog,返回空统计 - return { - "total": 0, - "success": 0, - "failed": 0, - "recent_7_days": 0, - "by_type": {}, - } - - def _get_user_stats(self): - """获取用户统计""" - from django.db.models import Count, Q - seven_days_ago = timezone.now() - timedelta(days=7) - - return User.objects.aggregate( - total=Count('id'), - active=Count('id', filter=Q(is_active=True)), - recent_7_days=Count('id', filter=Q(date_joined__gte=seven_days_ago)), - ) - - def _get_account_opening_stats(self, site_group): - """获取开户统计""" - from django.db.models import Count, Q - - if site_group: - request_qs = AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ) - cloud_qs = CloudComputerUser.objects.filter( - product__site_group=site_group - ) - else: - request_qs = AccountOpeningRequest.objects.filter(target_product__site_group__isnull=True) - cloud_qs = CloudComputerUser.objects.filter(product__site_group__isnull=True) - request_stats = request_qs.aggregate( - requests_total=Count('id'), - requests_pending=Count('id', filter=Q(status='pending')), - requests_approved=Count('id', filter=Q(status='approved')), - requests_completed=Count('id', filter=Q(status='completed')), - requests_failed=Count('id', filter=Q(status='failed')), - ) - cloud_user_stats = cloud_qs.aggregate( - cloud_users_total=Count('id'), - cloud_users_active=Count('id', filter=Q(status='active')), - ) - return {**request_stats, **cloud_user_stats} - - -class SystemConfigView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): - """ - 系统配置视图 - 仅限管理员访问 - """ - - template_name = "dashboard/system_config.html" - - def test_func(self): - """检查用户是否为管理员""" - return ( - self.request.user.is_staff or self.request.user.is_superuser - ) # type: ignore - - def handle_no_permission(self): - """处理无权限访问的情况""" - messages.error(self.request, "您没有权限访问系统配置页面") - return redirect("dashboard:index") - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - # 获取或创建系统配置 - config = SystemConfig.get_config() - context["form"] = SystemConfigForm(instance=config) - return context - - def post(self, request, *args, **kwargs): - """处理系统配置更新""" - config = SystemConfig.get_config() - form = SystemConfigForm(request.POST, instance=config) - - if form.is_valid(): - form.save() - messages.success(request, "系统配置已更新") - - AuditLog.objects.create( - user=request.user, - action="system_config_update", - description="更新系统配置", - ip_address=get_client_ip(request), - user_agent=request.META.get("HTTP_USER_AGENT", ""), - ) - - return redirect("dashboard:index") - else: - messages.error(request, "系统配置更新失败,请检查表单中的错误") - context = self.get_context_data() - context["form"] = form - return self.render_to_response(context) - - -class WidgetConfigView(LoginRequiredMixin, View): - """ - 仪表盘组件配置视图 - 用于管理仪表盘组件的显示和配置 - """ - - def get(self, request, *args, **kwargs): - """渲染组件配置页面""" - widgets = DashboardWidget.objects.all() - context = {"widgets": widgets} - return render(request, "dashboard/widget_config.html", context) - - def post(self, request, *args, **kwargs): - """更新组件配置""" - import json - - try: - data = json.loads(request.body) - widgets_data = data.get("widgets", []) - - for widget_data in widgets_data: - widget_id = widget_data.get("widget_id") - is_enabled = widget_data.get("is_enabled", False) - display_order = widget_data.get("display_order", 0) - - try: - widget = DashboardWidget.objects.get(id=widget_id) - widget.is_enabled = is_enabled - widget.display_order = display_order - widget.save() - except DashboardWidget.DoesNotExist: - return JsonResponse( - {"status": "error", "message": f"Widget {widget_id} not found"}, - status=404, - ) - - return JsonResponse({"status": "success"}) - except json.JSONDecodeError: - return JsonResponse( - {"status": "error", "message": "Invalid JSON data"}, status=400 - ) diff --git a/apps/dashboard/views_admin.py b/apps/dashboard/views_admin.py deleted file mode 100644 index c44ef73..0000000 --- a/apps/dashboard/views_admin.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -仪表盘超级管理员视图 - -包含: -- DashboardWidget CRUD -- SystemConfig 单例编辑 + 发送测试邮件 -""" - -import json -import logging - -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.core.paginator import Paginator -from django.http import StreamingHttpResponse -from django.utils import timezone -from django.views.decorators.http import require_POST - -from apps.accounts.provider_decorators import superadmin_required -from .models import DashboardWidget, SystemConfig -from .forms_admin import DashboardWidgetForm, SystemConfigForm - -logger = logging.getLogger('2c2a') - - -# ============================================================ -# DashboardWidget CRUD -# ============================================================ - -@superadmin_required -def widget_list(request): - """仪表盘组件列表""" - queryset = DashboardWidget.objects.order_by('display_order') - - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - title__icontains=search - ) | queryset.filter( - widget_type__icontains=search - ) - - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'search': search, - 'active_nav': 'dashboard_widgets', - } - return render(request, 'admin_base/dashboard/widget_list.html', context) - - -@superadmin_required -def widget_create(request): - """创建仪表盘组件""" - if request.method == 'POST': - form = DashboardWidgetForm(request.POST) - if form.is_valid(): - widget = form.save() - messages.success( - request, f'仪表盘组件「{widget.title}」创建成功。' - ) - return redirect('admin:admin_dashboard_config:widget_list') - else: - form = DashboardWidgetForm() - - context = { - 'form': form, - 'active_nav': 'dashboard_widgets', - 'is_create': True, - } - return render(request, 'admin_base/dashboard/widget_form.html', context) - - -@superadmin_required -def widget_edit(request, pk): - """编辑仪表盘组件""" - widget = get_object_or_404(DashboardWidget, pk=pk) - - if request.method == 'POST': - form = DashboardWidgetForm(request.POST, instance=widget) - if form.is_valid(): - widget = form.save() - messages.success( - request, f'仪表盘组件「{widget.title}」更新成功。' - ) - return redirect('admin:admin_dashboard_config:widget_list') - else: - form = DashboardWidgetForm(instance=widget) - - context = { - 'form': form, - 'widget': widget, - 'active_nav': 'dashboard_widgets', - 'is_create': False, - } - return render(request, 'admin_base/dashboard/widget_form.html', context) - - -@superadmin_required -def widget_delete(request, pk): - """删除仪表盘组件""" - widget = get_object_or_404(DashboardWidget, pk=pk) - - if request.method == 'POST': - title = widget.title - widget.delete() - messages.success( - request, f'仪表盘组件「{title}」已删除。' - ) - return redirect('admin:admin_dashboard_config:widget_list') - - context = { - 'widget': widget, - 'active_nav': 'dashboard_widgets', - } - return render( - request, 'admin_base/dashboard/widget_confirm_delete.html', context - ) - - -# ============================================================ -# SystemConfig 单例编辑 + 发送测试邮件 -# ============================================================ - -@superadmin_required -def systemconfig_edit(request): - """系统配置编辑(单例,自动 get_or_create)""" - config, _ = SystemConfig.objects.get_or_create(pk=1) - - if request.method == 'POST': - form = SystemConfigForm(request.POST, instance=config) - if form.is_valid(): - form.save() - from .signals import system_config_saved - system_config_saved.send( - sender=SystemConfig, - request=request, - ) - messages.success(request, '系统配置已更新。') - return redirect('admin:admin_dashboard_config:systemconfig_edit') - else: - form = SystemConfigForm(instance=config) - - context = { - 'form': form, - 'config': config, - 'active_nav': 'dashboard_config', - } - return render( - request, 'admin_base/dashboard/systemconfig_edit.html', context - ) - - -@superadmin_required -@require_POST -def systemconfig_send_test_email(request): - """发送测试邮件(异步,跳转中间页通过 SSE 追踪状态)""" - config = get_object_or_404(SystemConfig, pk=1) - - test_email = ( - request.POST.get('test_email') - or request.user.email - or config.smtp_from_email - ) - - if not test_email: - messages.error(request, '未提供测试邮箱地址。') - return redirect('admin:admin_dashboard_config:systemconfig_edit') - - # 创建 AsyncTask 追踪记录 - from apps.tasks.models import AsyncTask - import uuid - task_record = AsyncTask.objects.create( - task_id=str(uuid.uuid4()), - name='测试邮件发送', - created_by=request.user, - status='pending', - result={'test_email': test_email}, - ) - - # 派发 Celery 任务 - from apps.accounts.tasks import send_test_email_task - result = send_test_email_task.delay(task_record.pk) - - # 回填 Celery task ID - task_record.task_id = result.id - task_record.save(update_fields=['task_id']) - - # 跳转到中间页 - return redirect( - 'admin:admin_dashboard_config:test_email_progress', - task_pk=task_record.pk - ) - - -@superadmin_required -def test_email_progress(request, task_pk): - """测试邮件发送进度中间页""" - from apps.tasks.models import AsyncTask - task_record = get_object_or_404(AsyncTask, pk=task_pk) - - # 安全检查:只允许创建者或超管查看 - if ( - task_record.created_by != request.user - and not request.user.is_superuser - ): - messages.error(request, '无权查看此任务。') - return redirect('admin:admin_dashboard_config:systemconfig_edit') - - context = { - 'task_record': task_record, - 'test_email': ( - task_record.result.get('test_email', '') - if task_record.result else '' - ), - 'active_nav': 'dashboard_config', - } - return render( - request, - 'admin_base/dashboard/test_email_progress.html', - context, - ) - - -@superadmin_required -def test_email_sse(request, task_pk): - """测试邮件发送状态 SSE 端点""" - from apps.tasks.models import AsyncTask - import time - - def event_stream(): - for _ in range(120): # 最多等待 60 秒 - try: - task_record = AsyncTask.objects.get(pk=task_pk) - except AsyncTask.DoesNotExist: - yield f"data: {json.dumps({'status': 'failed', 'error': '任务不存在'})}\n\n" - return - - result = task_record.result or {} - data = { - 'status': task_record.status, - 'progress': task_record.progress, - 'error': task_record.error_message, - 'logs': result.get('logs', []), - } - yield f"data: {json.dumps(data)}\n\n" - - if task_record.status in ('success', 'failed', 'cancelled'): - return - - time.sleep(0.5) - - # 超时 - yield f"data: {json.dumps({'status': 'timeout'})}\n\n" - - response = StreamingHttpResponse( - event_stream(), content_type='text/event-stream' - ) - response['Cache-Control'] = 'no-cache' - response['X-Accel-Buffering'] = 'no' - return response diff --git a/apps/dashboard/views_sitegroup.py b/apps/dashboard/views_sitegroup.py deleted file mode 100644 index 188956e..0000000 --- a/apps/dashboard/views_sitegroup.py +++ /dev/null @@ -1,225 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from apps.accounts.provider_decorators import superadmin_required, site_group_admin_required -from .models import SiteGroup, SiteGroupHostname, SiteGroupConfig -from .forms_sitegroup import SiteGroupForm, SiteGroupHostnameForm, SiteGroupConfigForm - - -@superadmin_required -def sitegroup_list(request): - sitegroups = SiteGroup.objects.all() - return render( - request, - "dashboard/sitegroup_list.html", - { - "sitegroups": sitegroups, - "active_nav": "sitegroups", - }, - ) - - -@superadmin_required -def sitegroup_create(request): - if request.method == "POST": - form = SiteGroupForm(request.POST) - if form.is_valid(): - form.save() - messages.success(request, "站点组创建成功") - return redirect("dashboard:sitegroup_list") - else: - form = SiteGroupForm() - return render( - request, - "dashboard/sitegroup_form.html", - { - "form": form, - "title": "创建站点组", - "active_nav": "sitegroups", - }, - ) - - -@superadmin_required -def sitegroup_update(request, pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - if request.method == "POST": - form = SiteGroupForm(request.POST, instance=sitegroup) - if form.is_valid(): - form.save() - messages.success(request, "站点组更新成功") - return redirect("dashboard:sitegroup_list") - else: - form = SiteGroupForm(instance=sitegroup) - return render( - request, - "dashboard/sitegroup_form.html", - { - "form": form, - "title": "编辑站点组", - "sitegroup": sitegroup, - "active_nav": "sitegroups", - }, - ) - - -@superadmin_required -def sitegroup_delete(request, pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - if request.method == "POST": - sitegroup.delete() - messages.success(request, "站点组已删除") - return redirect("dashboard:sitegroup_list") - return render( - request, - "dashboard/sitegroup_detail.html", - { - "sitegroup": sitegroup, - "active_nav": "sitegroups", - }, - ) - - -@superadmin_required -def sitegroup_detail(request, pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - hostnames = sitegroup.hostnames.all() - admins = sitegroup.admins.all() - return render( - request, - "dashboard/sitegroup_detail.html", - { - "sitegroup": sitegroup, - "hostnames": hostnames, - "admins": admins, - "active_nav": "sitegroups", - }, - ) - - -@superadmin_required -def sitegroup_add_hostname(request, pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - if request.method == "POST": - form = SiteGroupHostnameForm(request.POST) - if form.is_valid(): - hostname = form.save(commit=False) - hostname.site_group = sitegroup - hostname.save() - messages.success(request, f"主机名 {hostname.hostname} 已绑定") - else: - for error in form.errors.get_json_data().values(): - for e in error: - messages.error(request, e["message"]) - return redirect("dashboard:sitegroup_detail", pk=pk) - - -@superadmin_required -def sitegroup_remove_hostname(request, pk, hostname_pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - hostname = get_object_or_404( - SiteGroupHostname, pk=hostname_pk, site_group=sitegroup - ) - if request.method == "POST": - hostname.delete() - messages.success(request, f"主机名 {hostname.hostname} 已解绑") - return redirect("dashboard:sitegroup_detail", pk=pk) - - -@superadmin_required -def sitegroup_add_admin(request, pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - if request.method == "POST": - username = request.POST.get("username", "").strip() - if username: - from django.contrib.auth import get_user_model - - User = get_user_model() - try: - user = User.objects.get(username=username) - if user.is_superuser: - messages.warning( - request, f"用户 {username} 已是超级管理员,无需添加" - ) - elif sitegroup.admins.filter(pk=user.pk).exists(): - messages.warning(request, f"用户 {username} 已是该站点组管理员") - else: - sitegroup.admins.add(user) - messages.success(request, f"已将 {username} 添加为站点组管理员") - except User.DoesNotExist: - messages.error(request, f"用户 {username} 不存在") - return redirect("dashboard:sitegroup_detail", pk=pk) - - -@superadmin_required -def sitegroup_remove_admin(request, pk, user_pk): - sitegroup = get_object_or_404(SiteGroup, pk=pk) - from django.contrib.auth import get_user_model - - User = get_user_model() - user = get_object_or_404(User, pk=user_pk) - if request.method == "POST": - sitegroup.admins.remove(user) - messages.success(request, f"已移除 {user.username} 的站点组管理员权限") - return redirect("dashboard:sitegroup_detail", pk=pk) - - -@superadmin_required -def sitegroup_config(request, pk): - """站点组配置覆盖编辑""" - sitegroup = get_object_or_404(SiteGroup, pk=pk) - config, _ = SiteGroupConfig.objects.get_or_create(site_group=sitegroup) - - # 获取全局配置用于显示默认值 - from .models import SystemConfig - - global_config = SystemConfig.get_config() - - if request.method == "POST": - form = SiteGroupConfigForm(request.POST, instance=config) - if form.is_valid(): - form.save() - messages.success(request, f"站点组「{sitegroup.name}」配置已更新") - return redirect("dashboard:sitegroup_config", pk=pk) - else: - form = SiteGroupConfigForm(instance=config) - - return render( - request, - "dashboard/sitegroup_config.html", - { - "sitegroup": sitegroup, - "form": form, - "global_config": global_config, - "active_nav": "sitegroups", - }, - ) - - -@site_group_admin_required -def sitegroup_my_config(request): - """站点组管理员编辑自己站点组的配置覆盖""" - site_group = request.site_group - config, _ = SiteGroupConfig.objects.get_or_create(site_group=site_group) - - from .models import SystemConfig - global_config = SystemConfig.get_config() - - if request.method == "POST": - form = SiteGroupConfigForm(request.POST, instance=config) - if form.is_valid(): - form.save() - messages.success(request, f"站点组「{site_group.name}」配置已更新") - return redirect("dashboard:sitegroup_my_config") - else: - form = SiteGroupConfigForm(instance=config) - - return render( - request, - "dashboard/sitegroup_config.html", - { - "sitegroup": site_group, - "form": form, - "global_config": global_config, - "active_nav": "sitegroup_config", - }, - ) diff --git a/apps/dashboard/views_sitegroup_users.py b/apps/dashboard/views_sitegroup_users.py deleted file mode 100644 index 697c919..0000000 --- a/apps/dashboard/views_sitegroup_users.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -站点组管理员 - 用户管理视图 - -站点组管理员只能管理本站点组内的用户。 -支持:用户列表、封禁/解封、重置密码。 -""" - -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.core.paginator import Paginator -from django.db.models import Q - -from apps.accounts.provider_decorators import site_group_admin_required -from apps.accounts.forms_admin import AdminPasswordResetForm -from apps.accounts.models import UserBan -from apps.accounts.user_service import ban_user, unban_user - -User = get_user_model() - - -@site_group_admin_required -def sitegroup_user_list(request): - """站点组用户列表""" - site_group = request.site_group - if not site_group: - messages.error(request, "未识别到站点组") - return redirect("dashboard:index") - - queryset = ( - User.objects.filter(site_groups=site_group) - .prefetch_related("groups") - .select_related("active_ban") - .order_by("-created_at") - ) - - search = request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(username__icontains=search) - | Q(email__icontains=search) - | Q(first_name__icontains=search) - | Q(last_name__icontains=search) - ) - - active_filter = request.GET.get("is_active", "").strip() - if active_filter == "1": - queryset = queryset.filter(is_active=True) - elif active_filter == "0": - queryset = queryset.filter(is_active=False) - - paginator = Paginator(queryset, 25) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - admin_ids = set(site_group.admins.values_list("pk", flat=True)) - - context = { - "site_group": site_group, - "admin_ids": admin_ids, - "page_obj": page_obj, - "search": search, - "active_filter": active_filter, - "active_nav": "sitegroup_users", - } - return render(request, "dashboard/sitegroup_user_list.html", context) - - -@site_group_admin_required -def sitegroup_user_toggle_active(request, user_pk): - """站点组管理员封禁/解封用户""" - site_group = request.site_group - if not site_group: - messages.error(request, "未识别到站点组") - return redirect("dashboard:index") - - user = get_object_or_404( - User, pk=user_pk, site_groups=site_group - ) - - # 不能封禁自己 - if user.pk == request.user.pk: - messages.error(request, "不能封禁自己的账号") - return redirect("dashboard:sitegroup_user_list") - - # 不能封禁超管 - if user.is_superuser: - messages.error(request, "不能封禁超级管理员") - return redirect("dashboard:sitegroup_user_list") - - if request.method == "POST": - is_banned = UserBan.objects.filter(user=user).exists() - if is_banned: - # 解封 - unban_user(user, unbanned_by=request.user) - status_text = "解封" - else: - # 封禁 - reason = request.POST.get("ban_reason", "").strip() - if not reason: - reason = f"站点组 {site_group.name} 管理员封禁" - ban_user(user, reason=reason, banned_by=request.user) - status_text = "封禁" - - messages.success( - request, f"用户「{user.username}」已{status_text}" - ) - - return redirect("dashboard:sitegroup_user_list") - - -@site_group_admin_required -def sitegroup_user_reset_password(request, user_pk): - """站点组管理员重置用户密码""" - site_group = request.site_group - if not site_group: - messages.error(request, "未识别到站点组") - return redirect("dashboard:index") - - user = get_object_or_404( - User, pk=user_pk, site_groups=site_group - ) - - if user.is_superuser: - messages.error(request, "不能重置超级管理员密码") - return redirect("dashboard:sitegroup_user_list") - - if request.method == "POST": - form = AdminPasswordResetForm(request.POST) - if form.is_valid(): - user.set_password(form.cleaned_data["new_password1"]) - user.save() - messages.success( - request, f"用户「{user.username}」密码已重置" - ) - return redirect("dashboard:sitegroup_user_list") - else: - form = AdminPasswordResetForm() - - context = { - "site_group": site_group, - "form": form, - "target_user": user, - "active_nav": "sitegroup_users", - } - return render( - request, "dashboard/sitegroup_user_reset_password.html", context - ) - - -@site_group_admin_required -def sitegroup_user_remove(request, user_pk): - """站点组管理员将用户移出本站点组""" - site_group = request.site_group - if not site_group: - messages.error(request, "未识别到站点组") - return redirect("dashboard:index") - - user = get_object_or_404( - User, pk=user_pk, site_groups=site_group - ) - - if user.is_superuser: - messages.error(request, "不能移出超级管理员") - return redirect("dashboard:sitegroup_user_list") - - if request.method == "POST": - user.site_groups.remove(site_group) - messages.success( - request, - f"用户「{user.username}」已从站点组「{site_group.name}」移出", - ) - return redirect("dashboard:sitegroup_user_list") - - context = { - "site_group": site_group, - "target_user": user, - "active_nav": "sitegroup_users", - } - return render( - request, "dashboard/sitegroup_user_remove.html", context - ) diff --git a/apps/errors/__init__.py b/apps/errors/__init__.py deleted file mode 100755 index 13eca5e..0000000 --- a/apps/errors/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -自定义错误页面视图 -""" -from django.shortcuts import render -from django.http import HttpResponseServerError, HttpResponseNotFound, HttpResponseForbidden -import logging - -logger = logging.getLogger('2c2a') - - -def handler403(request, exception=None): - """403 错误处理""" - logger.warning(f"403 Forbidden access from {request.META.get('REMOTE_ADDR')} to {request.path}") - return render(request, 'errors/403.html', { - 'error_title': '访问被拒绝', - 'error_message': '您没有权限访问此页面。', - 'request_id': getattr(request, 'request_id', None) - }, status=403) - - -def handler404(request, exception=None): - """404 错误处理""" - logger.info(f"404 Not found: {request.path}") - return render(request, 'errors/404.html', { - 'error_title': '页面未找到', - 'error_message': '您请求的页面不存在或已被移动。', - 'request_path': request.path, - 'request_id': getattr(request, 'request_id', None) - }, status=404) - - -def handler500(request): - """500 错误处理""" - logger.error(f"500 Server error at {request.path}", exc_info=True) - # 使用通用错误消息,不暴露技术细节 - return render(request, 'errors/500.html', { - 'error_title': '服务器错误', - 'error_message': '服务器遇到了意外情况,我们正在努力修复此问题。', - 'request_id': getattr(request, 'request_id', None), - 'support_message': '如果问题持续存在,请联系技术支持团队', - 'trace_id': '请联系技术支持人员并提供错误ID' - }, status=500) - - -def handler400(request, exception=None): - """400 错误处理""" - logger.warning(f"400 Bad request from {request.META.get('REMOTE_ADDR')}: {request.path}") - return render(request, 'errors/400.html', { - 'error_title': '错误的请求', - 'error_message': '您的请求格式不正确或包含无效数据。', - 'request_id': getattr(request, 'request_id', None) - }, status=400) \ No newline at end of file diff --git a/apps/errors/urls.py b/apps/errors/urls.py deleted file mode 100755 index def489a..0000000 --- a/apps/errors/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -错误处理 URL 配置 -""" -from django.urls import path -from . import views - -# 错误处理器 -handler400 = 'apps.errors.handler400' -handler403 = 'apps.errors.handler403' -handler404 = 'apps.errors.handler404' -handler500 = 'apps.errors.handler500' - -urlpatterns = [ - # 可以添加错误页面测试路由 - # path('403/', views.handler_test403, name='test_403'), - # path('404/', views.handler_test404, name='test_404'), - # path('500/', views.handler_test500, name='test_500'), -] \ No newline at end of file diff --git a/apps/errors/views.py b/apps/errors/views.py deleted file mode 100755 index 4dc55a6..0000000 --- a/apps/errors/views.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -错误处理视图 -""" -from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponseServerError, HttpResponseForbidden -from django.shortcuts import render -import logging - -logger = logging.getLogger('2c2a') - - -def handler400(request, exception=None): - """400 错误处理""" - logger.warning(f"400 Bad request from {request.META.get('REMOTE_ADDR')} to {request.path}") - return render(request, 'errors/400.html', { - 'error_title': '错误的请求', - 'error_message': '您的请求格式不正确或包含无效数据。', - 'request_id': getattr(request, 'request_id', None) - }, status=400) - - -def handler403(request, exception=None): - """403 错误处理""" - logger.warning(f"403 Forbidden access from {request.META.get('REMOTE_ADDR')} to {request.path}") - return render(request, 'errors/403.html', { - 'error_title': '访问被拒绝', - 'error_message': '您没有权限访问此页面。', - 'request_id': getattr(request, 'request_id', None) - }, status=403) - - -def handler404(request, exception=None): - """404 错误处理""" - logger.info(f"404 Not found: {request.path}") - return render(request, 'errors/404.html', { - 'error_title': '页面未找到', - 'error_message': '您请求的页面不存在或已被移动。', - 'request_path': request.path, - 'request_id': getattr(request, 'request_id', None) - }, status=404) - - -def handler500(request): - """500 错误处理""" - logger.error(f"500 Server error at {request.path}", exc_info=True) - # 获取追踪 ID - import uuid - trace_id = str(uuid.uuid4()) - - # 使用通用错误消息,不暴露技术细节 - return render(request, 'errors/500.html', { - 'error_title': '服务器错误', - 'error_message': '服务器遇到了意外情况,我们正在努力修复此问题。', - 'request_id': getattr(request, 'request_id', None), - 'trace_id': trace_id, - 'support_message': '如果问题持续存在,请联系技术支持团队' - }, status=500) \ No newline at end of file diff --git a/apps/hosts/__init__.py b/apps/hosts/__init__.py deleted file mode 100755 index 81716a5..0000000 --- a/apps/hosts/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Hosts 应用初始化 -""" -default_app_config = 'apps.hosts.apps.HostsConfig' \ No newline at end of file diff --git a/apps/hosts/admin.py b/apps/hosts/admin.py deleted file mode 100644 index 0baebbe..0000000 --- a/apps/hosts/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.hosts.views_admin) 和提供商后台 (apps.hosts.views_provider) diff --git a/apps/hosts/apps.py b/apps/hosts/apps.py deleted file mode 100755 index b76792a..0000000 --- a/apps/hosts/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Hosts 应用配置 -""" -from django.apps import AppConfig - - -class HostsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.hosts' - verbose_name = '主机管理' \ No newline at end of file diff --git a/apps/hosts/forms_admin.py b/apps/hosts/forms_admin.py deleted file mode 100644 index c82ff2f..0000000 --- a/apps/hosts/forms_admin.py +++ /dev/null @@ -1,330 +0,0 @@ -""" -主机管理 - 超管后台表单 - -超管可操作所有字段,无提供商数据隔离。 -包含主机创建/编辑表单和主机组表单。 -""" - -import os - -from django import forms -from django.contrib.auth import get_user_model -from django.conf import settings - -from utils.provider import PROVIDER_GROUP_NAME -from .models import Host, HostGroup -from .forms_wizard import ( - validate_certificate_pem, - validate_private_key_pem, - _ensure_cert_dir, -) - -User = get_user_model() - -INPUT_CLASS = ( - 'w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 ' - 'rounded px-3 py-2 text-slate-200 placeholder-slate-500 ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition' -) -SELECT_CLASS = ( - 'w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 ' - 'rounded px-3 py-2 text-slate-200 appearance-none ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition cursor-pointer' -) -CHECKBOX_CLASS = ( - 'w-5 h-5 rounded border-slate-700/50 bg-slate-900/50 ' - 'text-cyan-400 focus:ring-cyan-500 focus:ring-2 transition ' - 'accent-cyan-500 cursor-pointer' -) -MULTI_SELECT_CLASS = ( - 'w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 ' - 'rounded px-3 py-2 text-slate-200 ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition min-h-[120px]' -) -FILE_INPUT_CLASS = ( - 'w-full text-sm text-slate-200 file:mr-4 file:py-2 file:px-4 ' - 'file:rounded file:border-0 file:text-sm file:font-medium ' - 'file:bg-cyan-600/20 file:text-cyan-400 hover:file:bg-cyan-600/30 ' - 'file:cursor-pointer cursor-pointer' -) - - -class AdminHostForm(forms.ModelForm): - """ - 超管主机表单 - - 包含所有主机字段,无提供商过滤。 - 密码字段可选,留空则自动生成(创建时)或不修改(编辑时)。 - """ - - password = forms.CharField( - widget=forms.PasswordInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入远程主机登录密码', - 'autocomplete': 'new-password', - }), - required=False, - label='密码', - ) - - cert_pem = forms.FileField( - label='客户端证书(公钥)', - required=False, - widget=forms.ClearableFileInput(attrs={ - 'class': FILE_INPUT_CLASS, - 'accept': '.pem,.cer,.crt,.cert', - }), - help_text='PEM格式的客户端证书文件', - ) - - cert_key = forms.FileField( - label='客户端私钥', - required=False, - widget=forms.ClearableFileInput(attrs={ - 'class': FILE_INPUT_CLASS, - 'accept': '.pem,.key', - }), - help_text='PEM格式的客户端私钥文件', - ) - - class Meta: - model = Host - fields = [ - 'name', 'os_type', 'hostname', 'connection_type', - 'auth_method', 'port', 'rdp_port', - 'use_ssl', 'username', - 'providers', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入主机名称', - }), - 'os_type': forms.Select(attrs={ - 'class': SELECT_CLASS, - }), - 'hostname': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入主机地址', - }), - 'connection_type': forms.Select(attrs={ - 'class': SELECT_CLASS, - }), - 'auth_method': forms.Select(attrs={ - 'class': SELECT_CLASS, - }), - 'port': forms.NumberInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '5985', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '3389', - }), - 'username': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入连接用户名', - }), - 'use_ssl': forms.CheckboxInput(attrs={ - 'class': CHECKBOX_CLASS, - }), - 'providers': forms.SelectMultiple(attrs={ - 'class': MULTI_SELECT_CLASS, - 'size': '6', - }), - } - labels = { - 'name': '主机名称', - 'os_type': '主机系统', - 'hostname': '主机地址', - 'connection_type': '连接类型', - 'auth_method': '连接方式', - 'port': 'WinRM端口', - 'rdp_port': 'RDP端口', - 'use_ssl': '使用SSL', - 'username': '用户名', - 'providers': '管理提供商', - } - help_texts = { - 'providers': '按住 Ctrl / Cmd 可多选提供商', - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - provider_users = User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ).order_by('username') - self.fields['providers'].queryset = provider_users - - self.fields['os_type'].choices = Host.OS_TYPE_CHOICES - self.fields['connection_type'].choices = [ - ('winrm', 'WinRM'), - ('localwinserver', '本地WinServer'), - ] - self.fields['auth_method'].choices = Host.AUTH_METHOD_CHOICES - - if self.instance.pk: - self.fields['password'].help_text = ( - '留空则不修改密码。为安全起见,此处不显示原密码。' - ) - self.fields['password'].required = False - - def clean(self): - cleaned_data = super().clean() - connection_type = cleaned_data.get('connection_type') - auth_method = cleaned_data.get('auth_method') - - if connection_type == 'winrm' and auth_method == 'certificate': - cert_pem = self.files.get('cert_pem') - cert_key = self.files.get('cert_key') - has_existing = ( - self.instance.pk - and self.instance.cert_pem_path - and os.path.exists(self.instance.cert_pem_path) - ) - if not cert_pem and not has_existing: - self.add_error('cert_pem', '证书认证方式必须上传客户端证书') - if not cert_key and not has_existing: - self.add_error('cert_key', '证书认证方式必须上传客户端私钥') - if cert_pem: - try: - validate_certificate_pem(cert_pem.read()) - except forms.ValidationError as e: - self.add_error('cert_pem', e) - finally: - cert_pem.seek(0) - if cert_key: - try: - validate_private_key_pem(cert_key.read()) - except forms.ValidationError as e: - self.add_error('cert_key', e) - finally: - cert_key.seek(0) - - if connection_type == 'winrm' and auth_method == 'ntlm': - if not cleaned_data.get('username'): - self.add_error('username', 'NTLM认证方式必须填写用户名') - if not self.instance.pk and not cleaned_data.get('password'): - self.add_error('password', 'NTLM认证方式必须填写密码') - - if connection_type == 'localwinserver': - if not cleaned_data.get('username'): - self.add_error('username', '必须填写用户名') - if not self.instance.pk and not cleaned_data.get('password'): - self.add_error('password', '必须填写密码') - - return cleaned_data - - def save(self, commit=True): - instance = super().save(commit=False) - auth_method = self.cleaned_data.get('auth_method') - connection_type = self.cleaned_data.get('connection_type') - password = self.cleaned_data.get('password') - - if self.instance.pk: - if password: - instance.password = password - else: - if connection_type == 'winrm' and auth_method == 'ntlm': - instance.password = password - elif connection_type == 'winrm' and auth_method == 'certificate': - instance.cert_pem_path = instance.cert_pem_path or '' - instance.cert_key_path = instance.cert_key_path or '' - elif connection_type == 'localwinserver': - instance.password = password - - if commit: - instance.save() - self.save_m2m() - if connection_type == 'winrm' and auth_method == 'certificate': - self._save_cert_files(instance) - - return instance - - def _save_cert_files(self, host): - cert_pem = self.files.get('cert_pem') - cert_key = self.files.get('cert_key') - if not cert_pem or not cert_key: - return - - cert_dir = _ensure_cert_dir(host.pk) - pem_path = os.path.join(cert_dir, 'client.pem') - key_path = os.path.join(cert_dir, 'client.key') - - with open(pem_path, 'wb') as f: - for chunk in cert_pem.chunks(): - f.write(chunk) - - with open(key_path, 'wb') as f: - for chunk in cert_key.chunks(): - f.write(chunk) - - os.chmod(pem_path, 0o600) - os.chmod(key_path, 0o600) - - host.cert_pem_path = pem_path - host.cert_key_path = key_path - Host.objects.filter(pk=host.pk).update( - cert_pem_path=pem_path, - cert_key_path=key_path, - ) - - -class AdminHostGroupForm(forms.ModelForm): - """ - 超管主机组表单 - - 包含所有主机组字段,无提供商过滤。 - providers 字段显示所有提供商组用户。 - """ - - class Meta: - model = HostGroup - fields = ['name', 'description', 'hosts', 'providers'] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入主机组名称', - }), - 'description': forms.Textarea(attrs={ - 'class': INPUT_CLASS + ' resize-y', - 'rows': 3, - 'placeholder': '输入主机组描述(可选)', - }), - 'hosts': forms.SelectMultiple(attrs={ - 'class': MULTI_SELECT_CLASS, - 'size': '8', - }), - 'providers': forms.SelectMultiple(attrs={ - 'class': MULTI_SELECT_CLASS, - 'size': '6', - }), - } - labels = { - 'name': '组名称', - 'description': '描述', - 'hosts': '主机', - 'providers': '管理提供商', - } - help_texts = { - 'hosts': '按住 Ctrl / Cmd 可选主机', - 'providers': '按住 Ctrl / Cmd 可多选提供商', - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields['hosts'].queryset = Host.objects.order_by('name') - - provider_users = User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ).order_by('username') - self.fields['providers'].queryset = provider_users diff --git a/apps/hosts/forms_provider.py b/apps/hosts/forms_provider.py deleted file mode 100644 index 000084e..0000000 --- a/apps/hosts/forms_provider.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -主机管理 - 提供商后台表单 - -包含主机创建和编辑表单,复用 Admin 中的密码处理逻辑。 -""" - -import secrets -import string - -from django import forms - -from .models import Host, HostGroup - - -def generate_random_password(length=16): - """ - 生成随机复杂密码 - - 包含大写字母、小写字母、数字和特殊字符,确保密码强度。 - """ - alphabet = string.ascii_letters + string.digits + '!@#$%^&*()_+-=[]{}|;:,.<>?' - while True: - password = ''.join(secrets.choice(alphabet) for _ in range(length)) - has_upper = any(c.isupper() for c in password) - has_lower = any(c.islower() for c in password) - has_digit = any(c.isdigit() for c in password) - has_special = any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?' for c in password) - if has_upper and has_lower and has_digit and has_special: - return password - - -class HostCreateForm(forms.ModelForm): - """ - 主机创建表单 - - 密码字段为必填,可自动生成随机密码。 - 创建时自动设置 created_by 为当前用户。 - """ - - password = forms.CharField( - widget=forms.PasswordInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入密码或留空自动生成', - 'autocomplete': 'new-password', - }), - required=False, - help_text='留空将自动生成随机密码', - label='密码', - ) - - class Meta: - model = Host - fields = [ - 'name', 'hostname', 'connection_type', 'port', 'rdp_port', - 'use_ssl', 'username', 'os_version', 'description', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入主机名称', - }), - 'hostname': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入主机地址', - }), - 'connection_type': forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface appearance-none focus:outline-none focus:ring-2 focus:ring-md-primary transition cursor-pointer', - }), - 'port': forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '5985', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '3389', - }), - 'username': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入连接用户名', - }), - 'os_version': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '例如: Windows Server 2022', - }), - 'description': forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '输入主机描述(可选)', - }), - 'use_ssl': forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 text-md-primary focus:ring-md-primary focus:ring-2 transition', - }), - } - - def __init__(self, *args, **kwargs): - self.generated_password = None - super().__init__(*args, **kwargs) - # 创建时密码可选(自动生成) - self.fields['password'].required = False - - def save(self, commit=True): - instance = super().save(commit=False) - password = self.cleaned_data.get('password') - if password: - instance.password = password - else: - # 自动生成随机密码 - self.generated_password = generate_random_password() - instance.password = self.generated_password - if commit: - instance.save() - return instance - - -class HostUpdateForm(forms.ModelForm): - """ - 主机编辑表单 - - 密码字段可选,留空则不修改。 - 不允许修改 created_by 和 providers 字段。 - """ - - password = forms.CharField( - widget=forms.PasswordInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '留空则不修改密码', - 'autocomplete': 'new-password', - }), - required=False, - help_text='留空则不修改密码。为安全起见,此处不显示原密码。', - label='密码', - ) - - class Meta: - model = Host - fields = [ - 'name', 'hostname', 'connection_type', 'port', 'rdp_port', - 'use_ssl', 'username', 'os_version', 'status', 'description', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入主机名称', - }), - 'hostname': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入主机地址', - }), - 'connection_type': forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface appearance-none focus:outline-none focus:ring-2 focus:ring-md-primary transition cursor-pointer', - }), - 'port': forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '5985', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '3389', - }), - 'username': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入连接用户名', - }), - 'os_version': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '例如: Windows Server 2022', - }), - 'status': forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface appearance-none focus:outline-none focus:ring-2 focus:ring-md-primary transition cursor-pointer', - }), - 'description': forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '输入主机描述(可选)', - }), - 'use_ssl': forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 text-md-primary focus:ring-md-primary focus:ring-2 transition', - }), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance.pk: - self.fields['password'].help_text = '留空则不修改密码。为安全起见,此处不显示原密码。' - - def save(self, commit=True): - instance = super().save(commit=False) - if self.cleaned_data.get('password'): - instance.password = self.cleaned_data['password'] - if commit: - instance.save() - return instance - - -class HostGroupForm(forms.ModelForm): - """ - 主机组表单 - - 提供商只能选择自己可见的主机和提供商。 - hosts 和 providers 字段按当前提供商过滤。 - """ - - class Meta: - model = HostGroup - fields = ['name', 'description', 'hosts', 'providers'] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '输入主机组名称', - }), - 'description': forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '输入主机组描述(可选)', - }), - 'hosts': forms.SelectMultiple(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface focus:outline-none focus:ring-2 focus:ring-md-primary transition min-h-[120px]', - 'size': '8', - }), - 'providers': forms.SelectMultiple(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface focus:outline-none focus:ring-2 focus:ring-md-primary transition min-h-[80px]', - 'size': '5', - }), - } - labels = { - 'name': '组名称', - 'description': '描述', - 'hosts': '主机', - 'providers': '管理提供商', - } - help_texts = { - 'hosts': '按住 Ctrl / Cmd 可多选主机', - 'providers': '按住 Ctrl / Cmd 可多选提供商', - } - - def __init__(self, *args, **kwargs): - self.provider_user = kwargs.pop('provider_user', None) - super().__init__(*args, **kwargs) - - if self.provider_user: - # 过滤 hosts:只显示当前提供商可见的主机 - from utils.provider import get_provider_hosts - self.fields['hosts'].queryset = get_provider_hosts( - self.provider_user - ).order_by('name') - - # 过滤 providers:只显示提供商组的用户 - from django.contrib.auth.models import User - from utils.provider import is_provider, PROVIDER_GROUP_NAME - provider_users = User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ).order_by('username') - self.fields['providers'].queryset = provider_users diff --git a/apps/hosts/forms_wizard.py b/apps/hosts/forms_wizard.py deleted file mode 100644 index 4c5d1ff..0000000 --- a/apps/hosts/forms_wizard.py +++ /dev/null @@ -1,372 +0,0 @@ -""" -主机管理 - 向导式创建表单 - -分步引导超管添加主机,提供智能默认值和逐步验证。 -与 AdminHostForm 不同,此表单专注于创建流程的简化和引导。 -""" - -import os - -from django import forms -from django.contrib.auth import get_user_model -from django.conf import settings - -from utils.provider import PROVIDER_GROUP_NAME -from .models import Host - -User = get_user_model() - -INPUT_CLASS = ( - 'w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 ' - 'rounded px-3 py-2 text-slate-200 placeholder-slate-500 ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition' -) -SELECT_CLASS = ( - 'w-full bg-slate-900/50 backdrop-blur-sm border border-slate-700/50 ' - 'rounded px-3 py-2 text-slate-200 appearance-none ' - 'focus:outline-none focus:ring-1 focus:ring-cyan-500/50 ' - 'focus:border-cyan-500 transition cursor-pointer' -) -CHECKBOX_CLASS = ( - 'w-5 h-5 rounded border-slate-700/50 bg-slate-900/50 ' - 'text-cyan-400 focus:ring-cyan-500 focus:ring-2 transition ' - 'accent-cyan-500 cursor-pointer' -) -FILE_INPUT_CLASS = ( - 'w-full text-sm text-slate-200 file:mr-4 file:py-2 file:px-4 ' - 'file:rounded file:border-0 file:text-sm file:font-medium ' - 'file:bg-cyan-600/20 file:text-cyan-400 hover:file:bg-cyan-600/30 ' - 'file:cursor-pointer cursor-pointer' -) - -CONNECTION_DEFAULT_PORTS = { - 'winrm': 5985, - 'localwinserver': 5985, - 'ssh': 22, - 'tunnel': 5985, -} - -CONNECTION_DEFAULT_SSL = { - 'winrm': False, - 'localwinserver': False, - 'ssh': False, - 'tunnel': False, -} - -CERT_STORAGE_DIR = os.path.join(settings.MEDIA_ROOT, 'certs', 'hosts') - - -def _ensure_cert_dir(host_pk): - d = os.path.join(CERT_STORAGE_DIR, str(host_pk)) - os.makedirs(d, exist_ok=True) - return d - - -def validate_certificate_pem(content: bytes, field_name: str = '证书') -> None: - try: - text = content.decode('utf-8') - except UnicodeDecodeError: - raise forms.ValidationError(f'{field_name}文件编码无效,必须为UTF-8文本格式') - if '-----BEGIN' not in text: - raise forms.ValidationError(f'{field_name}文件格式无效,不是合法的PEM格式') - if '-----END' not in text: - raise forms.ValidationError(f'{field_name}文件格式无效,不是合法的PEM格式') - - -def validate_private_key_pem(content: bytes) -> None: - try: - text = content.decode('utf-8') - except UnicodeDecodeError: - raise forms.ValidationError('私钥文件编码无效,必须为UTF-8文本格式') - if '-----BEGIN' not in text: - raise forms.ValidationError('私钥文件格式无效,不是合法的PEM格式') - if 'PRIVATE KEY' not in text: - raise forms.ValidationError('私钥文件格式无效,未包含私钥标识') - if '-----END' not in text: - raise forms.ValidationError('私钥文件格式无效,不是合法的PEM格式') - try: - from cryptography.hazmat.primitives.serialization import load_pem_private_key - from cryptography.hazmat.backends import default_backend - load_pem_private_key(content, password=None, backend=default_backend()) - except ImportError: - # cryptography 为可选依赖:缺失时仅执行基础 PEM 文本校验, - # 不阻断表单流程,以保持兼容现有部署环境。 - pass - except Exception as e: - raise forms.ValidationError(f'私钥文件无效: {str(e)}') - - -class HostWizardForm(forms.ModelForm): - """ - 主机创建向导表单 - - 分为三步: - - Step 1: 基本信息 (name, os_type, hostname, connection_type) - - Step 2: 连接配置 (port, auth_method, username/password 或 证书) - - Step 3: 分配提供商 (providers, description) - """ - - password = forms.CharField( - widget=forms.PasswordInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入远程主机登录密码', - 'autocomplete': 'new-password', - 'x-model': 'password', - }), - required=False, - label='密码', - ) - - tunnel_token = forms.CharField( - widget=forms.HiddenInput(attrs={ - 'x-model': 'tunnelToken', - }), - required=False, - ) - - init_token = forms.CharField( - widget=forms.HiddenInput(attrs={ - 'x-model': 'initToken', - }), - required=False, - ) - - cert_config_method = forms.CharField( - widget=forms.HiddenInput(attrs={ - 'x-model': 'certConfigMethod', - }), - required=False, - initial='quick', - ) - - cert_pem = forms.FileField( - label='客户端证书(公钥)', - required=False, - widget=forms.ClearableFileInput(attrs={ - 'class': FILE_INPUT_CLASS, - 'accept': '.pem,.cer,.crt,.cert', - }), - help_text='PEM格式的客户端证书文件', - ) - - cert_key = forms.FileField( - label='客户端私钥', - required=False, - widget=forms.ClearableFileInput(attrs={ - 'class': FILE_INPUT_CLASS, - 'accept': '.pem,.key', - }), - help_text='PEM格式的客户端私钥文件', - ) - - class Meta: - model = Host - fields = [ - 'name', 'os_type', 'hostname', 'connection_type', - 'auth_method', 'port', 'rdp_port', 'use_ssl', - 'username', 'password', - 'providers', 'description', - 'tunnel_token', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入主机名称,如: 北京服务器-01', - 'x-model': 'name', - }), - 'os_type': forms.Select(attrs={ - 'class': SELECT_CLASS, - 'x-model': 'osType', - }), - 'hostname': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入主机地址,如: 192.168.1.100', - 'x-model': 'hostname', - }), - 'connection_type': forms.Select(attrs={ - 'class': SELECT_CLASS, - 'x-model': 'connectionType', - 'x-on:change': 'onConnectionTypeChange()', - }), - 'auth_method': forms.Select(attrs={ - 'class': SELECT_CLASS, - 'x-model': 'authMethod', - 'x-on:change': 'onAuthMethodChange()', - }), - 'port': forms.NumberInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '5985', - 'x-model': 'port', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '3389', - 'x-model.number': 'rdpPort', - }), - 'use_ssl': forms.CheckboxInput(attrs={ - 'class': CHECKBOX_CLASS, - 'x-model': 'useSsl', - }), - 'username': forms.TextInput(attrs={ - 'class': INPUT_CLASS, - 'placeholder': '输入连接用户名,如: Administrator', - 'x-model': 'username', - }), - 'description': forms.Textarea(attrs={ - 'class': INPUT_CLASS + ' resize-y', - 'rows': 3, - 'placeholder': '输入主机描述(可选)', - 'x-model': 'description', - }), - 'providers': forms.CheckboxSelectMultiple(), - } - labels = { - 'name': '主机名称', - 'os_type': '主机系统', - 'hostname': '主机地址', - 'connection_type': '连接类型', - 'auth_method': '连接方式', - 'port': 'WinRM端口', - 'rdp_port': 'RDP端口', - 'use_ssl': '使用SSL', - 'username': '用户名', - 'description': '描述', - 'providers': '管理提供商', - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - provider_users = User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ).order_by('username') - self.fields['providers'].queryset = provider_users - - if not self.initial.get('port'): - self.initial['port'] = 5985 - if not self.initial.get('rdp_port'): - self.initial['rdp_port'] = 3389 - - self.fields['os_type'].choices = Host.OS_TYPE_CHOICES - - self.fields['connection_type'].choices = [ - ('winrm', 'WinRM'), - ('localwinserver', '本地WinServer'), - ] - - self.fields['auth_method'].choices = Host.AUTH_METHOD_CHOICES - - def clean(self): - cleaned_data = super().clean() - connection_type = cleaned_data.get('connection_type') - hostname = cleaned_data.get('hostname') - auth_method = cleaned_data.get('auth_method') - - if connection_type == 'tunnel' and not hostname: - cleaned_data['hostname'] = 'tunnel-pending' - - tunnel_token = cleaned_data.get('tunnel_token') - if tunnel_token == '': - cleaned_data['tunnel_token'] = None - - if connection_type == 'winrm' and auth_method == 'certificate': - cert_config_method = cleaned_data.get('cert_config_method', 'quick') - cert_pem = self.files.get('cert_pem') - cert_key = self.files.get('cert_key') - if cert_config_method == 'manual': - if not cert_pem: - self.add_error('cert_pem', '证书认证方式必须上传客户端证书') - if not cert_key: - self.add_error('cert_key', '证书认证方式必须上传客户端私钥') - if cert_pem: - try: - validate_certificate_pem(cert_pem.read()) - except forms.ValidationError as e: - self.add_error('cert_pem', e) - finally: - cert_pem.seek(0) - if cert_key: - try: - validate_private_key_pem(cert_key.read()) - except forms.ValidationError as e: - self.add_error('cert_key', e) - finally: - cert_key.seek(0) - - if connection_type == 'winrm' and auth_method == 'ntlm': - if not cleaned_data.get('username'): - self.add_error('username', 'NTLM认证方式必须填写用户名') - if not cleaned_data.get('password'): - self.add_error('password', 'NTLM认证方式必须填写密码') - - if connection_type == 'localwinserver': - if not cleaned_data.get('username'): - self.add_error('username', '必须填写用户名') - if not cleaned_data.get('password'): - self.add_error('password', '必须填写密码') - - return cleaned_data - - def save(self, commit=True): - instance = super().save(commit=False) - auth_method = self.cleaned_data.get('auth_method') - connection_type = self.cleaned_data.get('connection_type') - - if connection_type == 'winrm' and auth_method == 'ntlm': - instance.password = self.cleaned_data.get('password') - elif connection_type == 'winrm' and auth_method == 'certificate': - instance.cert_pem_path = '' - instance.cert_key_path = '' - elif connection_type == 'localwinserver': - instance.password = self.cleaned_data.get('password') - - if commit: - instance.save() - self.save_m2m() - if connection_type == 'winrm' and auth_method == 'certificate': - self._save_cert_files(instance) - - return instance - - def _save_cert_files(self, host): - cert_pem = self.files.get('cert_pem') - cert_key = self.files.get('cert_key') - if not cert_pem or not cert_key: - return - - cert_dir = _ensure_cert_dir(host.pk) - pem_path = os.path.join(cert_dir, 'client.pem') - key_path = os.path.join(cert_dir, 'client.key') - - with open(pem_path, 'wb') as f: - for chunk in cert_pem.chunks(): - f.write(chunk) - - with open(key_path, 'wb') as f: - for chunk in cert_key.chunks(): - f.write(chunk) - - os.chmod(pem_path, 0o600) - os.chmod(key_path, 0o600) - - host.cert_pem_path = pem_path - host.cert_key_path = key_path - Host.objects.filter(pk=host.pk).update( - cert_pem_path=pem_path, - cert_key_path=key_path, - ) - - def get_providers_with_host_count(self): - providers = self.fields['providers'].queryset - result = [] - for provider in providers: - host_count = provider.provider_hosts.count() - result.append({ - 'id': provider.pk, - 'username': provider.username, - 'host_count': host_count, - }) - return result diff --git a/apps/hosts/management/__init__.py b/apps/hosts/management/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/hosts/management/commands/__init__.py b/apps/hosts/management/commands/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/hosts/management/commands/gateway_listener.py b/apps/hosts/management/commands/gateway_listener.py deleted file mode 100644 index c0eaacf..0000000 --- a/apps/hosts/management/commands/gateway_listener.py +++ /dev/null @@ -1,249 +0,0 @@ -import logging -import signal -import sys - -from django.core.management.base import BaseCommand -from django.conf import settings - -logger = logging.getLogger('2c2a') - - -class Command(BaseCommand): - help = 'Listen for Gateway events via Unix Domain Socket' - - def add_arguments(self, parser): - parser.add_argument( - '--socket', - type=str, - default=None, - help='Unix Domain Socket path (default: from settings)', - ) - - def handle(self, *args, **options): - from utils.gateway_client import is_gateway_enabled - - if not is_gateway_enabled(): - self.stdout.write( - self.style.WARNING( - 'Gateway is not enabled. ' - 'Set GATEWAY_ENABLED=True in environment to enable. ' - 'Exiting.' - ) - ) - return - - from utils.gateway_client import GatewayEventListener - - socket_path = options.get('socket') or getattr( - settings, 'GATEWAY_CONTROL_SOCKET', - '/run/2c2a/control.sock' - ) - - self.stdout.write( - f'Starting Gateway event listener on {socket_path}' - ) - - listener = GatewayEventListener(socket_path) - - listener.register_handler( - 'tunnel_online', self._handle_tunnel_online - ) - listener.register_handler( - 'tunnel_offline', self._handle_tunnel_offline - ) - listener.register_handler( - 'rdp_gateway_connect', self._handle_rdp_gateway_connect - ) - listener.register_handler( - 'rdp_gateway_disconnect', self._handle_rdp_gateway_disconnect - ) - listener.register_handler( - 'remote_exec_result', self._handle_remote_exec_result - ) - - def signal_handler(signum, frame): - self.stdout.write('Shutting down listener...') - listener.stop() - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - listener.start() - except KeyboardInterrupt: - listener.stop() - - def _handle_tunnel_online(self, event_type, payload): - from django.utils import timezone - from apps.hosts.models import Host - from apps.audit.models import AuditLog - - token = payload.get('token', '') - client_ip = payload.get('client_ip', '') - client_ver = payload.get('client_ver', '') - public_key = payload.get('public_key', b'') - - try: - host = Host.objects.get(tunnel_token=token) - now = timezone.now() - host.tunnel_status = 'online' - host.tunnel_connected_at = now - host.tunnel_last_seen_at = now - host.tunnel_client_ip = client_ip - host.tunnel_client_version = client_ver - if public_key: - host.tunnel_public_key = public_key - host.save(update_fields=[ - 'tunnel_status', 'tunnel_connected_at', - 'tunnel_last_seen_at', 'tunnel_client_ip', - 'tunnel_client_version', 'tunnel_public_key', - ]) - - AuditLog.objects.create( - host=host, - action='tunnel_online', - details={ - 'token': token, - 'client_ip': client_ip, - 'client_ver': client_ver, - } - ) - - logger.info( - f'Tunnel online: host={host.name}, ' - f'token={token}, ip={client_ip}' - ) - - except Host.DoesNotExist: - logger.warning( - f'Tunnel online event for unknown token: {token}' - ) - - def _handle_tunnel_offline(self, event_type, payload): - from apps.hosts.models import Host - from apps.audit.models import AuditLog - - token = payload.get('token', '') - - try: - host = Host.objects.get(tunnel_token=token) - host.tunnel_status = 'offline' - host.save(update_fields=['tunnel_status']) - - AuditLog.objects.create( - host=host, - action='tunnel_offline', - details={'token': token} - ) - - logger.info( - f'Tunnel offline: host={host.name}, token={token}' - ) - - except Host.DoesNotExist: - logger.warning( - f'Tunnel offline event for unknown token: {token}' - ) - - def _handle_rdp_gateway_connect(self, event_type, payload): - from apps.audit.models import AuditLog - - token = payload.get('token', '') - session_id = payload.get('session_id', '') - target_host = payload.get('target_host', '') - user = payload.get('user', '') - client_ip = payload.get('client_ip', '') - - try: - from apps.hosts.models import Host - host = Host.objects.get(tunnel_token=token) - - AuditLog.objects.create( - host=host, - action='rdp_gateway_connect', - ip_address=client_ip, - details={ - 'session_id': session_id, - 'token': token, - 'target_host': target_host, - 'user': user, - } - ) - - logger.info( - f'RDP gateway connect: host={host.name}, ' - f'session_id={session_id}, user={user}, ip={client_ip}' - ) - - except Host.DoesNotExist: - logger.warning( - f'RDP gateway connect event for unknown token: {token}' - ) - - def _handle_rdp_gateway_disconnect(self, event_type, payload): - from apps.audit.models import AuditLog - - token = payload.get('token', '') - session_id = payload.get('session_id', '') - target_host = payload.get('target_host', '') - user = payload.get('user', '') - client_ip = payload.get('client_ip', '') - duration = payload.get('duration', 0) - - try: - from apps.hosts.models import Host - host = Host.objects.get(tunnel_token=token) - - AuditLog.objects.create( - host=host, - action='rdp_gateway_disconnect', - ip_address=client_ip, - details={ - 'session_id': session_id, - 'token': token, - 'target_host': target_host, - 'user': user, - 'duration': duration, - } - ) - - logger.info( - f'RDP gateway disconnect: host={host.name}, ' - f'session_id={session_id}, user={user}, duration={duration}' - ) - - except Host.DoesNotExist: - logger.warning( - f'RDP gateway disconnect event for unknown token: {token}' - ) - - def _handle_remote_exec_result(self, event_type, payload): - from apps.audit.models import AuditLog - - token = payload.get('token', '') - req_id = payload.get('req_id', '') - exit_code = payload.get('exit_code', -1) - - try: - from apps.hosts.models import Host - host = Host.objects.get(tunnel_token=token) - - AuditLog.objects.create( - host=host, - action='remote_exec_result', - details={ - 'req_id': req_id, - 'exit_code': exit_code, - } - ) - - logger.info( - f'Remote exec result: host={host.name}, ' - f'req_id={req_id}, exit_code={exit_code}' - ) - - except Host.DoesNotExist: - logger.warning( - f'Remote exec result event for unknown token: {token}' - ) diff --git a/apps/hosts/management/commands/generate_tunnel_token.py b/apps/hosts/management/commands/generate_tunnel_token.py deleted file mode 100644 index 209a332..0000000 --- a/apps/hosts/management/commands/generate_tunnel_token.py +++ /dev/null @@ -1,60 +0,0 @@ -import secrets -from django.core.management.base import BaseCommand -from apps.hosts.models import Host - - -class Command(BaseCommand): - help = '为隧道模式主机生成隧道Token' - - def add_arguments(self, parser): - parser.add_argument( - 'host_id', - type=int, - help='主机ID', - ) - parser.add_argument( - '--force', - action='store_true', - default=False, - help='强制重新生成Token(即使已存在)', - ) - - def handle(self, *args, **options): - host_id = options['host_id'] - force = options['force'] - - try: - host = Host.objects.get(id=host_id) - except Host.DoesNotExist: - self.stderr.write( - self.style.ERROR(f'主机ID {host_id} 不存在') - ) - return - - if host.tunnel_token and not force: - self.stderr.write( - self.style.WARNING( - f'主机 {host.name} 已有Token: {host.tunnel_token}\n' - f'使用 --force 强制重新生成' - ) - ) - return - - token = secrets.token_urlsafe(32) - host.tunnel_token = token - host.connection_type = 'tunnel' - host.tunnel_status = 'offline' - host.save(update_fields=[ - 'tunnel_token', 'connection_type', 'tunnel_status', - ]) - - self.stdout.write( - self.style.SUCCESS( - f'主机 {host.name} 的隧道Token已生成:\n' - f' Token: {token}\n' - f' 连接类型已设置为: tunnel\n' - f'\n' - f'请将此Token配置到边缘端 2c2a-tunnel:\n' - f' 2c2a-tunnel.exe install -token {token} -server wss://:9000' - ) - ) diff --git a/apps/hosts/migrations/0001_initial.py b/apps/hosts/migrations/0001_initial.py deleted file mode 100755 index cc9d7f0..0000000 --- a/apps/hosts/migrations/0001_initial.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:24 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Host', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='主机名称')), - ('hostname', models.CharField(max_length=255, verbose_name='主机地址')), - ('port', models.IntegerField(default=5985, verbose_name='WinRM端口')), - ('rdp_port', models.IntegerField(default=3389, verbose_name='RDP端口')), - ('use_ssl', models.BooleanField(default=False, verbose_name='使用SSL')), - ('username', models.CharField(max_length=100, verbose_name='用户名')), - ('password', models.CharField(max_length=255, verbose_name='密码')), - ('host_type', models.CharField(choices=[('server', '服务器'), ('workstation', '工作站'), ('laptop', '笔记本'), ('desktop', '台式机')], max_length=20, verbose_name='主机类型')), - ('os_version', models.CharField(blank=True, max_length=100, verbose_name='操作系统版本')), - ('status', models.CharField(choices=[('online', '在线'), ('offline', '离线'), ('error', '错误')], default='offline', max_length=20, verbose_name='状态')), - ('description', models.TextField(blank=True, verbose_name='描述')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ], - options={ - 'verbose_name': '主机', - 'verbose_name_plural': '主机', - 'db_table': 'hosts_host', - }, - ), - migrations.CreateModel( - name='HostGroup', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='组名称')), - ('description', models.TextField(blank=True, verbose_name='描述')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('hosts', models.ManyToManyField(blank=True, to='hosts.host', verbose_name='包含主机')), - ], - options={ - 'verbose_name': '主机组', - 'verbose_name_plural': '主机组', - 'db_table': 'hosts_hostgroup', - }, - ), - ] diff --git a/apps/hosts/migrations/0002_alter_hostgroup_hosts.py b/apps/hosts/migrations/0002_alter_hostgroup_hosts.py deleted file mode 100755 index 5fd2eca..0000000 --- a/apps/hosts/migrations/0002_alter_hostgroup_hosts.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='hostgroup', - name='hosts', - field=models.ManyToManyField(blank=True, to='hosts.host', verbose_name='主机'), - ), - ] diff --git a/apps/hosts/migrations/0003_alter_host_password_rename_password_host__password_and_more.py b/apps/hosts/migrations/0003_alter_host_password_rename_password_host__password_and_more.py deleted file mode 100755 index 169eed2..0000000 --- a/apps/hosts/migrations/0003_alter_host_password_rename_password_host__password_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 10:36 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hosts', '0002_alter_hostgroup_hosts'), - ] - - operations = [ - migrations.AlterField( - model_name='host', - name='password', - field=models.CharField(db_column='password', max_length=255, verbose_name='密码'), - ), - migrations.RenameField( - model_name='host', - old_name='password', - new_name='_password', - ), - migrations.AlterField( - model_name='host', - name='port', - field=models.IntegerField(default=5985, verbose_name='WinRM端Port'), - ), - migrations.CreateModel( - name='HostPermission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('can_edit', models.BooleanField(default=False, help_text='是否可以编辑主机信息', verbose_name='编辑权限')), - ('can_manage_products', models.BooleanField(default=False, help_text='是否可以在该主机上管理产品', verbose_name='产品上架权限')), - ('can_review_requests', models.BooleanField(default=False, help_text='是否可以审核该主机相关的开户申请', verbose_name='审核权限')), - ('can_manage_cloud_users', models.BooleanField(default=False, help_text='是否可以管理该主机上的云电脑用户', verbose_name='云电脑用户管理权限')), - ('granted_at', models.DateTimeField(auto_now_add=True, help_text='权限授予的时间', verbose_name='授权时间')), - ('expires_at', models.DateTimeField(blank=True, help_text='权限过期时间,留空表示永久有效', null=True, verbose_name='过期时间')), - ('granted_by', models.ForeignKey(blank=True, help_text='授予此权限的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权人')), - ('host', models.ForeignKey(help_text='被授权的主机', on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='主机')), - ('user', models.ForeignKey(help_text='拥有权限的用户', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '主机权限', - 'verbose_name_plural': '主机权限', - 'indexes': [models.Index(fields=['user'], name='hosts_hostp_user_id_580592_idx'), models.Index(fields=['host'], name='hosts_hostp_host_id_085956_idx'), models.Index(fields=['can_edit'], name='hosts_hostp_can_edi_2a886d_idx'), models.Index(fields=['can_manage_products'], name='hosts_hostp_can_man_97675c_idx'), models.Index(fields=['can_review_requests'], name='hosts_hostp_can_rev_9780ba_idx'), models.Index(fields=['can_manage_cloud_users'], name='hosts_hostp_can_man_ebcd88_idx')], - 'unique_together': {('user', 'host')}, - }, - ), - ] diff --git a/apps/hosts/migrations/0004_alter_host_port_delete_hostpermission.py b/apps/hosts/migrations/0004_alter_host_port_delete_hostpermission.py deleted file mode 100755 index d1871c3..0000000 --- a/apps/hosts/migrations/0004_alter_host_port_delete_hostpermission.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 11:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0003_alter_host_password_rename_password_host__password_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='host', - name='port', - field=models.IntegerField(default=5985, verbose_name='WinRM端口'), - ), - migrations.DeleteModel( - name='HostPermission', - ), - ] diff --git a/apps/hosts/migrations/0005_host_connection_type_alter_host_port.py b/apps/hosts/migrations/0005_host_connection_type_alter_host_port.py deleted file mode 100755 index ec70b3b..0000000 --- a/apps/hosts/migrations/0005_host_connection_type_alter_host_port.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-28 05:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0004_alter_host_port_delete_hostpermission'), - ] - - operations = [ - migrations.AddField( - model_name='host', - name='connection_type', - field=models.CharField(choices=[('winrm', 'WinRM'), ('ssh', 'SSH'), ('localwinserver', '本地WinServer')], default='winrm', max_length=20, verbose_name='连接类型'), - ), - migrations.AlterField( - model_name='host', - name='port', - field=models.IntegerField(default=5985, verbose_name='连接端口'), - ), - ] diff --git a/apps/hosts/migrations/0006_host_administrators.py b/apps/hosts/migrations/0006_host_administrators.py deleted file mode 100644 index d18a667..0000000 --- a/apps/hosts/migrations/0006_host_administrators.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-06 16:13 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hosts", "0005_host_connection_type_alter_host_port"), - ] - - operations = [ - migrations.AddField( - model_name="host", - name="administrators", - field=models.ManyToManyField( - blank=True, - related_name="managed_hosts", - to=settings.AUTH_USER_MODEL, - verbose_name="授权管理员", - ), - ), - ] diff --git a/apps/hosts/migrations/0007_add_hostgroup_created_by.py b/apps/hosts/migrations/0007_add_hostgroup_created_by.py deleted file mode 100644 index 614386b..0000000 --- a/apps/hosts/migrations/0007_add_hostgroup_created_by.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-06 16:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hosts", "0006_host_administrators"), - ] - - operations = [ - migrations.AddField( - model_name="hostgroup", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="created_hostgroups", - to=settings.AUTH_USER_MODEL, - verbose_name="创建者", - ), - ), - ] diff --git a/apps/hosts/migrations/0008_add_providers_to_host_and_hostgroup.py b/apps/hosts/migrations/0008_add_providers_to_host_and_hostgroup.py deleted file mode 100644 index d46c3be..0000000 --- a/apps/hosts/migrations/0008_add_providers_to_host_and_hostgroup.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-07 14:04 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hosts", "0007_add_hostgroup_created_by"), - ] - - operations = [ - migrations.AddField( - model_name="host", - name="providers", - field=models.ManyToManyField( - blank=True, - help_text="由超级管理员分配的提供商用户,提供商可以管理此主机", - related_name="provider_hosts", - to=settings.AUTH_USER_MODEL, - verbose_name="管理提供商", - ), - ), - migrations.AddField( - model_name="hostgroup", - name="providers", - field=models.ManyToManyField( - blank=True, - help_text="由超级管理员分配的提供商用户,提供商可以管理此主机组", - related_name="provider_hostgroups", - to=settings.AUTH_USER_MODEL, - verbose_name="管理提供商", - ), - ), - ] diff --git a/apps/hosts/migrations/0009_host_tunnel_fields.py b/apps/hosts/migrations/0009_host_tunnel_fields.py deleted file mode 100644 index 9c9db7c..0000000 --- a/apps/hosts/migrations/0009_host_tunnel_fields.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0008_add_providers_to_host_and_hostgroup'), - ] - - operations = [ - migrations.AddField( - model_name='host', - name='tunnel_token', - field=models.CharField( - blank=True, max_length=64, null=True, - unique=True, verbose_name='隧道Token' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_status', - field=models.CharField( - choices=[ - ('no_tunnel', '无隧道'), - ('offline', '隧道离线'), - ('online', '隧道在线'), - ('error', '隧道错误'), - ], - default='no_tunnel', max_length=20, - verbose_name='隧道状态' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_connected_at', - field=models.DateTimeField( - blank=True, null=True, verbose_name='隧道连接时间' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_last_seen_at', - field=models.DateTimeField( - blank=True, null=True, verbose_name='隧道最后心跳' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_client_version', - field=models.CharField( - blank=True, max_length=50, - verbose_name='隧道客户端版本' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_client_ip', - field=models.GenericIPAddressField( - blank=True, null=True, - verbose_name='隧道客户端IP' - ), - ), - migrations.AddField( - model_name='host', - name='tunnel_public_key', - field=models.TextField( - blank=True, verbose_name='隧道公钥(Ed25519)' - ), - ), - migrations.AlterField( - model_name='host', - name='connection_type', - field=models.CharField( - choices=[ - ('winrm', 'WinRM'), - ('ssh', 'SSH'), - ('localwinserver', '本地WinServer'), - ('tunnel', '隧道模式(零公网IP)'), - ], - default='winrm', max_length=20, - verbose_name='连接类型' - ), - ), - ] diff --git a/apps/hosts/migrations/0010_remove_host_host_type.py b/apps/hosts/migrations/0010_remove_host_host_type.py deleted file mode 100644 index 6352be4..0000000 --- a/apps/hosts/migrations/0010_remove_host_host_type.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-25 12:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0009_host_tunnel_fields'), - ] - - operations = [ - migrations.RemoveField( - model_name='host', - name='host_type', - ), - ] diff --git a/apps/hosts/migrations/0011_host_auth_method_host_cert_key_path_and_more.py b/apps/hosts/migrations/0011_host_auth_method_host_cert_key_path_and_more.py deleted file mode 100644 index 15da766..0000000 --- a/apps/hosts/migrations/0011_host_auth_method_host_cert_key_path_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-23 05:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0010_remove_host_host_type'), - ] - - operations = [ - migrations.AddField( - model_name='host', - name='auth_method', - field=models.CharField(choices=[('ntlm', '管理员账户密码'), ('certificate', '证书')], default='ntlm', max_length=20, verbose_name='连接方式'), - ), - migrations.AddField( - model_name='host', - name='cert_key_path', - field=models.CharField(blank=True, default='', max_length=512, verbose_name='客户端私钥路径'), - ), - migrations.AddField( - model_name='host', - name='cert_pem_path', - field=models.CharField(blank=True, default='', max_length=512, verbose_name='客户端证书路径'), - ), - migrations.AddField( - model_name='host', - name='os_type', - field=models.CharField(choices=[('windows', 'Windows')], default='windows', max_length=20, verbose_name='主机系统'), - ), - migrations.AlterField( - model_name='host', - name='connection_type', - field=models.CharField(choices=[('winrm', 'WinRM'), ('localwinserver', '本地WinServer'), ('ssh', 'SSH'), ('tunnel', '隧道模式(零公网IP)')], default='winrm', max_length=20, verbose_name='连接类型'), - ), - ] diff --git a/apps/hosts/migrations/0012_host_username_optional.py b/apps/hosts/migrations/0012_host_username_optional.py deleted file mode 100644 index 13e7c07..0000000 --- a/apps/hosts/migrations/0012_host_username_optional.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-23 09:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0011_host_auth_method_host_cert_key_path_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='host', - name='username', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='用户名'), - ), - ] diff --git a/apps/hosts/migrations/0013_add_cert_provision_fields.py b/apps/hosts/migrations/0013_add_cert_provision_fields.py deleted file mode 100644 index 6da3cac..0000000 --- a/apps/hosts/migrations/0013_add_cert_provision_fields.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-29 15:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0012_host_username_optional'), - ] - - operations = [ - migrations.AddField( - model_name='host', - name='_ntlm_fallback_password', - field=models.CharField(blank=True, db_column='ntlm_fallback_password', default='', max_length=255, verbose_name='NTLM回退密码'), - ), - migrations.AddField( - model_name='host', - name='_pfx_password', - field=models.CharField(blank=True, db_column='pfx_password', default='', max_length=255, verbose_name='PFX密码'), - ), - migrations.AddField( - model_name='host', - name='cert_activated_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='证书激活时间'), - ), - migrations.AddField( - model_name='host', - name='cert_provision_status', - field=models.CharField(choices=[('not_started', '未开始'), ('pending', '签发中'), ('ready', '已就绪'), ('configured', '已配置'), ('failed', '失败')], default='not_started', max_length=20, verbose_name='证书配置状态'), - ), - migrations.AddField( - model_name='host', - name='cert_root', - field=models.CharField(blank=True, default='', max_length=2, verbose_name='证书存储根路径'), - ), - migrations.AddField( - model_name='host', - name='cert_sub', - field=models.CharField(blank=True, default='', max_length=2, verbose_name='证书存储子路径'), - ), - migrations.AddField( - model_name='host', - name='ntlm_fallback_user', - field=models.CharField(blank=True, default='', max_length=100, verbose_name='NTLM回退用户名'), - ), - ] diff --git a/apps/hosts/migrations/0014_host_site_group_hostgroup_site_group.py b/apps/hosts/migrations/0014_host_site_group_hostgroup_site_group.py deleted file mode 100644 index f8d6df4..0000000 --- a/apps/hosts/migrations/0014_host_site_group_hostgroup_site_group.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 13:27 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0014_sitegroup_sitegrouphostname_and_more"), - ("hosts", "0013_add_cert_provision_fields"), - ] - - operations = [ - migrations.AddField( - model_name="host", - name="site_group", - field=models.ForeignKey( - blank=True, - help_text="该主机所属的站点组,留空表示默认组", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="hosts", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - migrations.AddField( - model_name="hostgroup", - name="site_group", - field=models.ForeignKey( - blank=True, - help_text="该主机组所属的站点组,留空表示默认组", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="host_groups", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ] diff --git a/apps/hosts/migrations/0015_alter_host_site_group_alter_hostgroup_site_group.py b/apps/hosts/migrations/0015_alter_host_site_group_alter_hostgroup_site_group.py deleted file mode 100644 index 7fb4ad1..0000000 --- a/apps/hosts/migrations/0015_alter_host_site_group_alter_hostgroup_site_group.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-01 13:09 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more"), - ("hosts", "0014_host_site_group_hostgroup_site_group"), - ] - - operations = [ - migrations.AlterField( - model_name="host", - name="site_group", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="hosts", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - migrations.AlterField( - model_name="hostgroup", - name="site_group", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="host_groups", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ] diff --git a/apps/hosts/migrations/__init__.py b/apps/hosts/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/hosts/models.py b/apps/hosts/models.py deleted file mode 100755 index 7e8a1b1..0000000 --- a/apps/hosts/models.py +++ /dev/null @@ -1,595 +0,0 @@ -from django.db import models -from django.conf import settings -from utils.crypto import encrypt_value, decrypt_value -import logging -import os - - -class Host(models.Model): - """ - 主机模型 - """ - OS_TYPE_CHOICES = [ - ('windows', 'Windows'), - ] - - CONNECTION_TYPE_CHOICES = [ - ('winrm', 'WinRM'), - ('localwinserver', '本地WinServer'), - ('ssh', 'SSH'), - ('tunnel', '隧道模式(零公网IP)'), - ] - - AUTH_METHOD_CHOICES = [ - ('ntlm', '管理员账户密码'), - ('certificate', '证书'), - ] - - STATUS_CHOICES = [ - ('online', '在线'), - ('offline', '离线'), - ('error', '错误'), - ] - - TUNNEL_STATUS_CHOICES = [ - ('no_tunnel', '无隧道'), - ('offline', '隧道离线'), - ('online', '隧道在线'), - ('error', '隧道错误'), - ] - - CERT_PROVISION_STATUS_CHOICES = [ - ('not_started', '未开始'), - ('pending', '签发中'), - ('ready', '已就绪'), - ('configured', '已配置'), - ('failed', '失败'), - ] - - name = models.CharField(max_length=100, verbose_name='主机名称') - os_type = models.CharField(max_length=20, choices=OS_TYPE_CHOICES, default='windows', verbose_name='主机系统') - hostname = models.CharField(max_length=255, verbose_name='主机地址') - connection_type = models.CharField(max_length=20, choices=CONNECTION_TYPE_CHOICES, default='winrm', verbose_name='连接类型') - auth_method = models.CharField(max_length=20, choices=AUTH_METHOD_CHOICES, default='ntlm', verbose_name='连接方式') - port = models.IntegerField(default=5985, verbose_name='连接端口') - rdp_port = models.IntegerField(default=3389, verbose_name='RDP端口') - use_ssl = models.BooleanField(default=False, verbose_name='使用SSL') - username = models.CharField(max_length=100, blank=True, default='', verbose_name='用户名') - _password = models.CharField(max_length=255, verbose_name='密码', db_column='password') # 加密存储 - cert_pem_path = models.CharField(max_length=512, blank=True, default='', verbose_name='客户端证书路径') - cert_key_path = models.CharField(max_length=512, blank=True, default='', verbose_name='客户端私钥路径') - os_version = models.CharField(max_length=100, blank=True, verbose_name='操作系统版本') - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='offline', verbose_name='状态') - description = models.TextField(blank=True, verbose_name='描述') - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='创建者') - - # 管理员列表 - 核心字段用于数据隔离 - administrators = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - verbose_name="授权管理员", - related_name='managed_hosts' - ) - - # 管理提供商 - 由超级管理员分配 - providers = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - verbose_name='管理提供商', - related_name='provider_hosts', - help_text='由超级管理员分配的提供商用户,提供商可以管理此主机' - ) - - site_group = models.ForeignKey( - 'dashboard.SiteGroup', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='hosts', - verbose_name='所属站点组', - ) - - created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') - - tunnel_token = models.CharField( - max_length=64, unique=True, null=True, blank=True, - verbose_name='隧道Token' - ) - tunnel_status = models.CharField( - max_length=20, choices=TUNNEL_STATUS_CHOICES, - default='no_tunnel', verbose_name='隧道状态' - ) - tunnel_connected_at = models.DateTimeField( - null=True, blank=True, verbose_name='隧道连接时间' - ) - tunnel_last_seen_at = models.DateTimeField( - null=True, blank=True, verbose_name='隧道最后心跳' - ) - tunnel_client_version = models.CharField( - max_length=50, blank=True, verbose_name='隧道客户端版本' - ) - tunnel_client_ip = models.GenericIPAddressField( - null=True, blank=True, verbose_name='隧道客户端IP' - ) - tunnel_public_key = models.TextField( - blank=True, verbose_name='隧道公钥(Ed25519)' - ) - - cert_root = models.CharField( - max_length=2, blank=True, default='', - verbose_name='证书存储根路径' - ) - cert_sub = models.CharField( - max_length=2, blank=True, default='', - verbose_name='证书存储子路径' - ) - _pfx_password = models.CharField( - max_length=255, blank=True, default='', - db_column='pfx_password', verbose_name='PFX密码' - ) - ntlm_fallback_user = models.CharField( - max_length=100, blank=True, default='', - verbose_name='NTLM回退用户名' - ) - _ntlm_fallback_password = models.CharField( - max_length=255, blank=True, default='', - db_column='ntlm_fallback_password', - verbose_name='NTLM回退密码' - ) - cert_activated_at = models.DateTimeField( - null=True, blank=True, verbose_name='证书激活时间' - ) - cert_provision_status = models.CharField( - max_length=20, - choices=CERT_PROVISION_STATUS_CHOICES, - default='not_started', - verbose_name='证书配置状态' - ) - - class Meta: - verbose_name = '主机' - verbose_name_plural = '主机' - db_table = 'hosts_host' # 与数据库中的实际表名一致 - - def __str__(self): - return self.name - - @property - def password(self): - try: - return decrypt_value(self._password) - except ValueError: - raise ValueError("密码解密失败,数据可能已损坏或密钥已变更") - - @password.setter - def password(self, raw_password): - self._password = encrypt_value(raw_password) - - @property - def pfx_password(self): - try: - return decrypt_value(self._pfx_password) - except ValueError: - raise ValueError("PFX密码解密失败") - - @pfx_password.setter - def pfx_password(self, raw_password): - self._pfx_password = encrypt_value(raw_password) - - @property - def ntlm_fallback_password(self): - try: - return decrypt_value(self._ntlm_fallback_password) - except ValueError: - raise ValueError("NTLM回退密码解密失败") - - @ntlm_fallback_password.setter - def ntlm_fallback_password(self, raw_password): - self._ntlm_fallback_password = encrypt_value(raw_password) - - def save(self, *args, **kwargs): - """ - 重写save方法 - 注意:连接测试由Admin的save_model处理,避免循环调用 - """ - # 先调用父类的save方法保存数据 - super().save(*args, **kwargs) - # 暂时禁用自动连接测试,由Admin处理 - - def get_connection_client(self): - if self.connection_type == 'winrm': - from utils.winrm_client import WinrmClient - kwargs = dict( - hostname=self.hostname, - port=self.port, - use_ssl=self.use_ssl, - ) - if self.auth_method == 'certificate': - return FallbackWinrmClient(self) - else: - kwargs.update( - username=self.username, - password=self.password, - auth_method='ntlm', - ) - return WinrmClient(**kwargs) - elif self.connection_type == 'localwinserver': - from utils.local_winserver_client import LocalWinServerClient - return LocalWinServerClient( - username=self.username, - password=self.password - ) - elif self.connection_type == 'tunnel': - from utils.gateway_client import GatewayClient - return TunnelConnectionAdapter(self, GatewayClient()) - elif self.connection_type == 'ssh': - raise NotImplementedError("SSH连接类型尚未实现") - else: - raise ValueError( - f"不支持的连接类型: {self.connection_type}" - ) - - def test_connection(self): - if os.environ.get('2C2A_DEMO', '').lower() == '1': - Host.objects.filter(pk=self.pk).update(status='online') - return - - if self.connection_type == 'tunnel': - new_status = 'online' if self.tunnel_status == 'online' else 'offline' - Host.objects.filter(pk=self.pk).update(status=new_status) - return - - if (self.auth_method == 'certificate' - and (not self.cert_pem_path - or not os.path.exists(self.cert_pem_path))): - logging.getLogger("2c2a").warning( - f"证书文件不存在,跳过连接测试: {self.name} " - f"(pem={self.cert_pem_path})" - ) - Host.objects.filter(pk=self.pk).update(status='pending') - return - - try: - client = self.get_connection_client() - - if self.connection_type == 'localwinserver': - result = client.execute_command( - 'echo Connection Test OK' - ) - else: - result = client.execute_command('whoami') - - if result.success: - new_status = 'online' - else: - new_status = 'error' - - except Exception as e: - new_status = 'error' - logger = logging.getLogger("2c2a") - logger.error( - f"测试主机连接失败: {self.name}, 错误: {str(e)}" - ) - - Host.objects.filter(pk=self.pk).update(status=new_status) - - -class TunnelConnectionAdapter: - def __init__(self, host, gateway_client): - self.host = host - self.gateway_client = gateway_client - self._fallback_client = None - - def _get_fallback_client(self): - if self._fallback_client is not None: - return self._fallback_client - if self.host.connection_type == 'tunnel' and self.host.hostname: - try: - from utils.winrm_client import WinrmClient - self._fallback_client = WinrmClient( - hostname=self.host.hostname, - port=self.host.port, - username=self.host.username, - password=self.host.password, - use_ssl=self.host.use_ssl, - ) - return self._fallback_client - except Exception: - pass - return None - - @property - def success(self): - return True - - def execute_command(self, command, arguments=None): - return self.execute_powershell(command) - - def execute_powershell(self, script, arguments=None): - script_bytes = script.encode('utf-8') - - result = self.gateway_client.remote_exec( - token=self.host.tunnel_token, - script=script_bytes, - ) - - if result is None: - fallback = self._get_fallback_client() - if fallback: - return fallback.execute_powershell(script) - from utils.winrm_client import WinrmResult - return WinrmResult( - status_code=1, - std_out='', - std_err='Gateway不可用且无备用连接方式' - ) - - from utils.winrm_client import WinrmResult - stdout = '' - stderr = '' - exit_code = 1 - - if result.get('success'): - data = result.get('data', {}) - if isinstance(data, dict): - stdout = data.get('stdout', '') - stderr = data.get('stderr', '') - exit_code = data.get('exit_code', 1) - if isinstance(stdout, bytes): - stdout = stdout.decode('utf-8', errors='ignore') - if isinstance(stderr, bytes): - stderr = stderr.decode('utf-8', errors='ignore') - - return WinrmResult( - status_code=exit_code, - std_out=stdout, - std_err=stderr, - ) - - def create_user(self, username, password, description=None, group=None): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - safe_pass = _escape_ps_string(password) - safe_desc = _escape_ps_string(description or '') - - script = f''' -$pw = ConvertTo-SecureString "{safe_pass}" -AsPlainText -Force -New-LocalUser -Name "{safe_user}" -Password $pw -Description "{safe_desc}" -ErrorAction Stop -Add-LocalGroupMember -Group "Users" -Member "{safe_user}" -ErrorAction Stop -''' - if group: - safe_group = _escape_ps_string(group) - script += f'Add-LocalGroupMember -Group "{safe_group}" -Member "{safe_user}" -ErrorAction Stop\n' - - result = self.execute_powershell(script) - self.add_to_remote_users(username) - return result - - def delete_user(self, username): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - script = f'Remove-LocalUser -Name "{safe_user}" -ErrorAction Stop' - return self.execute_powershell(script) - - def enable_user(self, username): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - script = f'Enable-LocalUser -Name "{safe_user}" -ErrorAction Stop' - return self.execute_powershell(script) - - def disabled_user(self, username): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - script = f'Disable-LocalUser -Name "{safe_user}" -ErrorAction Stop' - return self.execute_powershell(script) - - def reset_password(self, username, password): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - safe_pass = _escape_ps_string(password) - script = f''' -$pw = ConvertTo-SecureString "{safe_pass}" -AsPlainText -Force -Set-LocalUser -Name "{safe_user}" -Password $pw -''' - result = self.execute_powershell(script) - if result.status_code == 0: - self.add_to_remote_users(username) - return result - - def add_to_remote_users(self, username): - from utils.winrm_client import _escape_ps_string - safe_user = _escape_ps_string(username) - script = ( - f'Add-LocalGroupMember -Group "Remote Desktop Users" ' - f'-Member "{safe_user}" -ErrorAction SilentlyContinue' - ) - return self.execute_powershell(script) - - -class FallbackWinrmClient: - _logger = logging.getLogger("2c2a") - - def __init__(self, host): - self.host = host - self._client = None - - def _try_connect(self): - if self._client is not None: - return - from utils.winrm_client import WinrmClient - from utils.cert_storage import get_cert_file_paths - last_exc = None - ca_trust_path = None - if self.host.cert_root and self.host.cert_sub: - paths = get_cert_file_paths(self.host.cert_root, self.host.cert_sub) - if paths['ca_cert'].exists(): - ca_trust_path = str(paths['ca_cert']) - configs = [ - ( - "SSL+Certificate", - dict( - hostname=self.host.hostname, - port=self.host.port, - use_ssl=True, - auth_method='certificate', - cert_pem_path=self.host.cert_pem_path, - cert_key_path=self.host.cert_key_path, - server_cert_validation='ignore', - ca_trust_path=ca_trust_path, - ), - ), - ] - if self.host.ntlm_fallback_user and self.host.ntlm_fallback_password: - configs.append(( - "HTTPS+NTLM", - dict( - hostname=self.host.hostname, - port=self.host.port, - use_ssl=True, - auth_method='ntlm', - username=self.host.ntlm_fallback_user, - password=self.host.ntlm_fallback_password, - server_cert_validation='ignore', - ca_trust_path=ca_trust_path, - ), - )) - configs.append(( - "HTTP+NTLM", - dict( - hostname=self.host.hostname, - port=5985, - use_ssl=False, - auth_method='ntlm', - username=self.host.ntlm_fallback_user, - password=self.host.ntlm_fallback_password, - ), - )) - for label, cfg in configs: - try: - client = WinrmClient(**cfg) - client.execute_command('whoami') - self._client = client - self._logger.info( - f"主机 {self.host.name} 连接成功," - f"使用方式: {label}" - ) - return - except Exception as e: - last_exc = e - self._logger.warning( - f"主机 {self.host.name} 连接方式 {label} " - f"失败: {e}" - ) - Host.objects.filter(pk=self.host.pk).update(status='error') - if last_exc is not None: - raise last_exc - raise RuntimeError("所有连接方式均失败") - - @property - def success(self): - return True - - def execute_command(self, command): - self._try_connect() - return self._client.execute_command(command) - - def execute_powershell(self, script): - self._try_connect() - return self._client.execute_powershell(script) - - def create_user(self, username, password, **kwargs): - self._try_connect() - return self._client.create_user(username, password, **kwargs) - - def delete_user(self, username): - self._try_connect() - return self._client.delete_user(username) - - def enable_user(self, username): - self._try_connect() - return self._client.enable_user(username) - - def disabled_user(self, username): - self._try_connect() - return self._client.disabled_user(username) - - def reset_password(self, username, password): - self._try_connect() - return self._client.reset_password(username, password) - - def op_user(self, username): - self._try_connect() - return self._client.op_user(username) - - def deop_user(self, username): - self._try_connect() - return self._client.deop_user(username) - - def add_to_remote_users(self, username): - self._try_connect() - return self._client.add_to_remote_users(username) - - def check_user_exists(self, username): - self._try_connect() - return self._client.check_user_exists(username) - - def generate_strong_password(self, length=None): - self._try_connect() - return self._client.generate_strong_password(length) - - def get_password_policy(self): - self._try_connect() - return self._client.get_password_policy() - - def create_user_with_reset_password_on_next_login( - self, username, password, description=None, group=None): - self._try_connect() - return self._client.create_user_with_reset_password_on_next_login( - username, password, description=description, group=group) - - -class HostGroup(models.Model): - """ - 主机组模型 - 用于将多个主机分组管理 - """ - name = models.CharField(max_length=100, verbose_name='组名称') - description = models.TextField(blank=True, verbose_name='描述') - hosts = models.ManyToManyField(Host, blank=True, verbose_name='主机') - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name='创建者', - related_name='created_hostgroups' - ) - # 管理提供商 - 由超级管理员分配 - providers = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - verbose_name='管理提供商', - related_name='provider_hostgroups', - help_text='由超级管理员分配的提供商用户,提供商可以管理此主机组' - ) - - site_group = models.ForeignKey( - 'dashboard.SiteGroup', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='host_groups', - verbose_name='所属站点组', - ) - - created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') - - class Meta: - verbose_name = '主机组' - verbose_name_plural = '主机组' - db_table = 'hosts_hostgroup' - - def __str__(self): - return self.name \ No newline at end of file diff --git a/apps/hosts/tasks.py b/apps/hosts/tasks.py deleted file mode 100755 index 5c08848..0000000 --- a/apps/hosts/tasks.py +++ /dev/null @@ -1,381 +0,0 @@ -from celery import shared_task -from django.contrib.auth.models import User -from django.utils import timezone - -from apps.hosts.models import Host -from apps.tasks.models import AsyncTask -import logging -import re - -logger = logging.getLogger(__name__) - -CERT_THUMBPRINT_PATTERN = re.compile(r'^[A-Fa-f0-9]{40}$') -CERT_FILENAME_PATTERN = re.compile(r'^[a-zA-Z0-9_\-\.]{1,255}\.pem$') - - -def validate_cert_thumbprint(thumbprint: str) -> str: - if not thumbprint: - raise ValueError("证书指纹不能为空") - thumbprint = thumbprint.strip().upper() - if not CERT_THUMBPRINT_PATTERN.match(thumbprint): - raise ValueError("证书指纹格式无效,必须是40位十六进制字符") - return thumbprint - - -def validate_cert_filename(filename: str) -> str: - if not filename: - raise ValueError("证书文件名不能为空") - if not CERT_FILENAME_PATTERN.match(filename): - raise ValueError("证书文件名格式无效,只允许字母、数字、下划线、连字符和点,且必须以.pem结尾") - return filename - - -def validate_cert_content(content: str) -> str: - if not content: - raise ValueError("证书内容不能为空") - if '@"' in content or '"@' in content: - raise ValueError("证书内容包含非法字符") - if len(content) > 100000: - raise ValueError("证书内容过长") - return content - - -@shared_task(bind=True) -def configure_winrm_on_host(self, host_id, cert_thumbprint=None, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"配置WinRM - 主机 #{host_id}", - created_by_id=operator_id, - target_object_id=host_id, - target_content_type='hosts.Host', - status='running' - ) - - try: - host = Host.objects.get(id=host_id) - task.start_execution() - - task.progress = 10 - task.save() - - try: - from utils.winrm_client import WinrmClient - - client = WinrmClient( - hostname=host.hostname or host.ip_address, - port=host.port, - username=host.username, - password=host.password, - use_ssl=host.use_ssl - ) - - actual_thumbprint = cert_thumbprint or host.certificate_thumbprint - if actual_thumbprint: - actual_thumbprint = validate_cert_thumbprint(actual_thumbprint) - - ps_script = ''' - Enable-PSRemoting -Force - Set-Service -Name WinRM -StartupType Automatic - ''' - - if actual_thumbprint: - ps_script += f''' - $selectorset = @{{Transport="HTTPS"}} - $resourceset = @{{Port="5986"; CertificateThumbprint="{actual_thumbprint}"}} - Get-WSManInstance -ResourceURI winrm/config/listener -SelectorSet $selectorset -ErrorAction SilentlyContinue | Remove-WSManInstance -ErrorAction SilentlyContinue - New-WSManInstance -ResourceURI winrm/config/listener -SelectorSet $selectorset -ValueSet $resourceset - if (-not (Get-NetFirewallRule -Name "WinRM-HTTPS-In-TCP-Public" -ErrorAction SilentlyContinue)) {{ - New-NetFirewallRule -Name "WinRM-HTTPS-In-TCP-Public" -DisplayName "WinRM HTTPS Inbound" -Enabled True -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Public,Private,Domain - }} - ''' - - ps_script += ''' - Set-Item -Path "WSMan:\\localhost\\Service\\AllowUnencrypted" -Value $false - Set-Item -Path "WSMan:\\localhost\\Service\\Auth\\Basic" -Value $true - Restart-Service WinRM - ''' - - task.progress = 30 - task.save() - - result = client.execute_powershell(ps_script) - - if result.status_code == 0: - task.progress = 80 - task.save() - - from django.utils import timezone - host.init_status = 'ready' - host.initialized_at = timezone.now() - if cert_thumbprint: - host.certificate_thumbprint = cert_thumbprint - host.save() - - task.progress = 100 - task.complete_success({ - 'status_code': result.status_code, - 'stdout': result.std_out, - 'success': True - }) - - return { - 'success': True, - 'status_code': result.status_code, - 'host_id': host_id - } - else: - error_msg = result.std_err if result.std_err else 'Unknown error' - task.complete_failure(f"PowerShell script failed: {error_msg}") - - return { - 'success': False, - 'status_code': result.status_code, - 'error': error_msg - } - - except Exception as conn_error: - logger.error(f"连接主机失败: {str(conn_error)}", exc_info=True) - task.complete_failure(f"无法连接到主机: {str(conn_error)}") - - return { - 'success': False, - 'error': str(conn_error) - } - - except Exception as e: - logger.error(f"配置WinRM失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'error': str(e) - } - - -@shared_task(bind=True) -def test_winrm_connection(self, host_id, use_certificate_auth=False): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"测试WinRM连接 - 主机 #{host_id}", - target_object_id=host_id, - target_content_type='hosts.Host', - status='running' - ) - - try: - host = Host.objects.get(id=host_id) - task.start_execution() - - old_status = host.status - host.test_connection() - host.refresh_from_db() - new_status = host.status - success = new_status == 'online' - - if success: - if host.auth_method == 'certificate' and host.cert_provision_status in ('pending', 'ready'): - Host.objects.filter(pk=host.pk).update( - cert_provision_status='configured', - cert_activated_at=timezone.now(), - ) - status_display = dict(Host.STATUS_CHOICES).get(new_status, new_status) - task.progress = 100 - task.complete_success({ - 'connected': True, - 'status': new_status, - 'status_display': status_display, - 'old_status': old_status, - 'message': f'连接成功,主机状态: {status_display}', - }) - - return { - 'success': True, - 'connected': True, - 'status': new_status, - 'status_display': status_display, - } - else: - if host.auth_method == 'certificate' and host.cert_provision_status in ('pending', 'ready'): - Host.objects.filter(pk=host.pk).update(cert_provision_status='failed') - status_display = dict(Host.STATUS_CHOICES).get(new_status, new_status) - task.complete_failure(f"连接测试失败,主机状态: {status_display}") - return { - 'success': False, - 'connected': False, - 'status': new_status, - 'status_display': status_display, - 'error': f'连接失败,主机状态: {status_display}', - } - - except Exception as e: - logger.error(f"测试WinRM连接失败: {str(e)}", exc_info=True) - try: - host = Host.objects.get(id=host_id) - if host.auth_method == 'certificate' and host.cert_provision_status in ('pending', 'ready'): - Host.objects.filter(pk=host.pk).update(cert_provision_status='failed') - Host.objects.filter(pk=host.pk).update(status='error') - except Host.DoesNotExist: - logger.warning( - "测试WinRM连接失败后的清理阶段:主机 #%s 不存在,跳过证书状态更新", - host_id - ) - task.complete_failure(str(e)) - - return { - 'success': False, - 'connected': False, - 'error': str(e) - } - - -@shared_task(bind=True) -def test_winrm_connection_raw(self, connection_type, hostname, port, use_ssl, auth_method, username, password): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"测试WinRM连接 - {hostname}", - status='running' - ) - - try: - task.start_execution() - - if connection_type == 'localwinserver': - from utils.local_winserver_client import LocalWinServerClient - client = LocalWinServerClient( - username=username, - password=password, - ) - result = client.execute_command('echo Connection Test OK') - elif connection_type == 'winrm' and auth_method == 'ntlm': - from utils.winrm_client import WinrmClient - client = WinrmClient( - hostname=hostname, - port=int(port), - username=username, - password=password, - use_ssl=bool(use_ssl), - auth_method='ntlm', - ) - result = client.execute_command('whoami') - else: - raise ValueError(f'不支持的连接类型: {connection_type}') - - if result.success: - output = result.std_out.strip() if result.std_out else '' - task.progress = 100 - task.complete_success({ - 'connected': True, - 'output': output, - 'message': f'连接成功{f" ({output})" if output else ""}', - }) - - return { - 'success': True, - 'connected': True, - 'output': output, - 'message': f'连接成功{f" ({output})" if output else ""}', - } - else: - error_detail = result.std_err.strip() if result.std_err else f'命令执行返回非零状态码: {result.status_code}' - task.complete_failure(f"连接失败: {error_detail}") - return { - 'success': False, - 'connected': False, - 'error': f'连接失败: {error_detail}', - } - - except Exception as e: - logger.error(f"测试WinRM连接失败: {hostname}, 错误: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'connected': False, - 'error': f'连接测试失败: {str(e)}', - } - - -@shared_task(bind=True) -def install_certificates_on_host(self, host_id, cert_pem, cert_filename, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"安装证书 - 主机 #{host_id}", - created_by_id=operator_id, - target_object_id=host_id, - target_content_type='hosts.Host', - status='running' - ) - - try: - host = Host.objects.get(id=host_id) - task.start_execution() - - cert_filename = validate_cert_filename(cert_filename) - cert_pem = validate_cert_content(cert_pem) - - from utils.winrm_client import WinrmClient, _escape_for_here_string - - client = WinrmClient( - hostname=host.hostname or host.ip_address, - port=host.port, - username=host.username, - password=host.password, - use_ssl=host.use_ssl - ) - - safe_cert_content = _escape_for_here_string(cert_pem) - safe_filename = cert_filename.replace('"', '').replace("'", '').replace(';', '') - - ps_script = f''' - $tempDir = "$env:TEMP\\2c2a_Certs" - if (!(Test-Path $tempDir)) {{ - New-Item -ItemType Directory -Path $tempDir -Force - }} - - $certContent = @" -{safe_cert_content} -"@ - - $certPath = Join-Path $tempDir "{safe_filename}" - $certContent | Out-File -FilePath $certPath -Encoding UTF8 - - Import-Certificate -FilePath $certPath -CertStoreLocation Cert:\\LocalMachine\\Root - Import-Certificate -FilePath $certPath -CertStoreLocation Cert:\\LocalMachine\\My - - Write-Output "Certificate installed successfully" - - Remove-Item $tempDir -Recurse -Force - ''' - - result = client.execute_powershell(ps_script) - - if result.status_code == 0: - task.progress = 100 - task.complete_success({ - 'installed': True, - 'cert_filename': cert_filename, - 'output': result.std_out - }) - - return { - 'success': True, - 'installed': True - } - else: - error_msg = result.std_err if result.std_err else 'Unknown error' - task.complete_failure(f"Certificate installation failed: {error_msg}") - - return { - 'success': False, - 'installed': False, - 'error': error_msg - } - - except Exception as e: - logger.error(f"安装证书失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'error': str(e) - } diff --git a/apps/hosts/templates/hosts/hostgroup_list.html b/apps/hosts/templates/hosts/hostgroup_list.html deleted file mode 100755 index 72052e6..0000000 --- a/apps/hosts/templates/hosts/hostgroup_list.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}主机组列表{% endblock %} - -{% block content %} -
-

主机组列表

- -
-
-
- -
-
- - -
-
-
- -
- - - - - - - - - - - - {% for group in hostgroups %} - - - - - - - - {% empty %} - - - - {% endfor %} - -
名称描述主机数量创建时间操作
{{ group.name }}{{ group.description|truncatechars:100 }}{{ group.hosts.count }}{{ group.created_at|date:"Y-m-d H:i:s" }} - 编辑 - 详情 -
暂无主机组
-
- - - {% if hostgroups.has_other_pages %} - - {% endif %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/apps/hosts/urls_admin.py b/apps/hosts/urls_admin.py deleted file mode 100644 index 8b43a45..0000000 --- a/apps/hosts/urls_admin.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -超管后台 - 主机管理 URL 配置 - -命名空间: admin_hosts (通过 admin: 命名空间访问) -超管可查看所有主机和主机组,无数据隔离。 -""" - -from django.urls import path - -from . import views_admin - -app_name = 'admin_hosts' - -urlpatterns = [ - # 主机管理 - path( - '', - views_admin.AdminHostListView.as_view(), - name='host_list' - ), - path( - 'wizard/', - views_admin.admin_host_wizard, - name='host_wizard' - ), - path( - 'wizard/generate-token/', - views_admin.admin_host_wizard_generate_token, - name='host_wizard_generate_token' - ), - path( - 'wizard/test-connection/', - views_admin.admin_host_wizard_test_connection, - name='host_wizard_test_connection' - ), - path( - 'wizard/generate-init-command/', - views_admin.admin_host_wizard_generate_init_command, - name='host_wizard_generate_init_command' - ), - path( - 'create/', - views_admin.AdminHostCreateView.as_view(), - name='host_create' - ), - path( - '/', - views_admin.AdminHostDetailView.as_view(), - name='host_detail' - ), - path( - '/edit/', - views_admin.AdminHostUpdateView.as_view(), - name='host_edit' - ), - path( - '/delete/', - views_admin.AdminHostDeleteView.as_view(), - name='host_delete' - ), - path( - '/test/', - views_admin.admin_host_test_connection, - name='host_test' - ), - path( - '/generate-init-command/', - views_admin.admin_host_generate_init_command, - name='host_generate_init_command' - ), - path( - 'generate-cert-command/', - views_admin.admin_host_generate_cert_command, - name='admin_host_generate_cert_command' - ), - - # 主机组管理 - path( - 'groups/', - views_admin.AdminHostGroupListView.as_view(), - name='hostgroup_list' - ), - path( - 'groups/create/', - views_admin.AdminHostGroupCreateView.as_view(), - name='hostgroup_create' - ), - path( - 'groups//edit/', - views_admin.AdminHostGroupUpdateView.as_view(), - name='hostgroup_edit' - ), - path( - 'groups//delete/', - views_admin.AdminHostGroupDeleteView.as_view(), - name='hostgroup_delete' - ), -] diff --git a/apps/hosts/urls_provider.py b/apps/hosts/urls_provider.py deleted file mode 100644 index 3640620..0000000 --- a/apps/hosts/urls_provider.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -主机管理 - 提供商后台 URL 配置 - -所有 URL 以 /provider/hosts/ 为前缀,命名空间为 'provider_hosts'。 -完整命名空间为 'provider:provider_hosts:'。 -""" - -from django.urls import path - -from . import views_provider - -app_name = 'provider_hosts' - -urlpatterns = [ - path( - '', - views_provider.HostListView.as_view(), - name='host_list' - ), - path( - 'create/', - views_provider.HostCreateView.as_view(), - name='host_create' - ), - path( - '/', - views_provider.HostDetailView.as_view(), - name='host_detail' - ), - path( - '/edit/', - views_provider.HostUpdateView.as_view(), - name='host_edit' - ), - path( - '/delete/', - views_provider.HostDeleteView.as_view(), - name='host_delete' - ), - path( - '/deploy/', - views_provider.HostDeployCommandView.as_view(), - name='host_deploy' - ), - path( - '/toggle/', - views_provider.HostToggleActiveView.as_view(), - name='host_toggle' - ), - - # 主机组管理 - path( - 'groups/', - views_provider.HostGroupListView.as_view(), - name='hostgroup_list' - ), - path( - 'groups/create/', - views_provider.HostGroupCreateView.as_view(), - name='hostgroup_create' - ), - path( - 'groups//edit/', - views_provider.HostGroupUpdateView.as_view(), - name='hostgroup_edit' - ), - path( - 'groups//delete/', - views_provider.HostGroupDeleteView.as_view(), - name='hostgroup_delete' - ), -] diff --git a/apps/hosts/views_admin.py b/apps/hosts/views_admin.py deleted file mode 100644 index eb18ea7..0000000 --- a/apps/hosts/views_admin.py +++ /dev/null @@ -1,1152 +0,0 @@ -""" -主机管理 - 超管后台视图 - -所有视图均使用 @admin_required 装饰器保护。 -超管可查看所有主机和主机组;提供商仅可查看自己创建或分配给自己的数据。 -""" - -import json -import logging -import os -import platform -import secrets -from datetime import timedelta - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.core.paginator import Paginator -from django.db.models import Q -from django.http import Http404, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone -from django.utils.decorators import method_decorator -from django.views import View -from django.views.generic import DetailView, TemplateView - -from django.contrib.auth.decorators import login_required - -from apps.accounts.provider_decorators import admin_required -from utils.provider import get_provider_hosts, PROVIDER_GROUP_NAME - -from .forms_admin import AdminHostForm, AdminHostGroupForm -from .forms_wizard import ( - HostWizardForm, - CONNECTION_DEFAULT_PORTS, - CONNECTION_DEFAULT_SSL, -) -from .models import Host, HostGroup - -User = get_user_model() -logger = logging.getLogger(__name__) - - -def _is_local_winserver(): - return platform.system() == "Windows" and "server" in platform.version().lower() - - -def _generate_init_command_data(request, host): - from apps.bootstrap.models import InitialToken - import base64 - - token = secrets.token_urlsafe(32) - expires_at = timezone.now() + timedelta(hours=24) - - initial_token = InitialToken.objects.create( - token=token, - host=host, - expires_at=expires_at, - status="ISSUED", - ) - - pairing_code = initial_token.generate_pairing_code() - - config_data = { - "c_side_url": request.build_absolute_uri("/").rstrip("/"), - "token": initial_token.token, - "host_id": str(host.id), - "expires_at": initial_token.expires_at.isoformat(), - } - config_json = json.dumps(config_data) - encoded_config = base64.b64encode(config_json.encode("utf-8")).decode("utf-8") - - one_liner = ( - "& ([ScriptBlock]::Create(" - "(irm https://static.2c2a.cc.cd/hostinitbash.ps1)" - f")) -Secret '{encoded_config}'" - ) - - fallback_command = ( - '$e = "$env:TEMP\\h_side_init.exe"; ' - "irm https://2c2a.cc.cd/hostinitbash.exe " - f"-OutFile $e; & $e '{encoded_config}'" - ) - - return { - "pairing_code": pairing_code, - "one_liner": one_liner, - "fallback_command": fallback_command, - "expires_at": initial_token.expires_at.isoformat(), - "host_id": host.id, - "hostname": host.hostname, - } - - -def _get_host_form_context(): - return { - "default_ports": json.dumps(CONNECTION_DEFAULT_PORTS), - "default_ssl": json.dumps(CONNECTION_DEFAULT_SSL), - "is_local_winserver": json.dumps(_is_local_winserver()), - } - - -def _get_permission_context(form, host=None): - provider_users = User.objects.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ).order_by("username") - - all_groups = Group.objects.all().order_by("name") - - provider_users_json = json.dumps( - [{"id": u.id, "username": u.username} for u in provider_users] - ) - groups_json = json.dumps( - [ - { - "id": g.id, - "name": g.name, - "member_ids": list( - g.user_set.filter( - groups__name=PROVIDER_GROUP_NAME, - is_staff=True, - is_superuser=False, - ) - .values_list("id", flat=True) - .distinct() - ), - } - for g in all_groups - ] - ) - - provider_user_ids = list(provider_users.values_list("id", flat=True)) - - initial_provider_ids = [] - - if form.is_bound: - initial_provider_ids = [int(x) for x in form.data.getlist("providers")] - elif host and host.pk: - initial_provider_ids = list(host.providers.values_list("id", flat=True)) - - initial_permissions = [] - key_counter = 0 - for uid in initial_provider_ids: - u = provider_users.filter(id=uid).first() - if u: - initial_permissions.append( - { - "key": key_counter, - "type": "member", - "targetId": u.id, - "name": u.username, - "userIds": [u.id], - } - ) - key_counter += 1 - - return { - "provider_users": provider_users, - "all_groups": all_groups, - "provider_users_json": provider_users_json, - "groups_json": groups_json, - "provider_user_ids_json": json.dumps(provider_user_ids), - "initial_permissions_json": json.dumps(initial_permissions), - "initial_provider_ids_json": json.dumps(initial_provider_ids), - } - - -# ========== 主机管理 ========== - - -@method_decorator(admin_required, name="dispatch") -class AdminHostListView(TemplateView): - """ - 超管主机列表视图 - - 显示所有主机,支持搜索和按连接类型/状态筛选。 - 包含提供商列显示每个主机关联的提供商。 - """ - - template_name = "admin_base/hosts/host_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - hosts_qs = Host.objects.all().order_by("-created_at") - elif self.request.user.is_site_group_admin(site_group): - if site_group: - hosts_qs = Host.objects.filter(site_group=site_group).order_by( - "-created_at" - ) - else: - hosts_qs = Host.objects.none() - else: - hosts_qs = get_provider_hosts( - self.request.user, site_group=site_group - ).order_by("-created_at") - - # 搜索过滤 - search = self.request.GET.get("search", "").strip() - if search: - hosts_qs = hosts_qs.filter( - Q(name__icontains=search) - | Q(hostname__icontains=search) - | Q(username__icontains=search) - ) - - # 状态过滤 - status_filter = self.request.GET.get("status", "").strip() - if status_filter: - hosts_qs = hosts_qs.filter(status=status_filter) - - # 连接类型过滤 - conn_filter = self.request.GET.get("connection_type", "").strip() - if conn_filter: - hosts_qs = hosts_qs.filter(connection_type=conn_filter) - - # 分页 - paginator = Paginator(hosts_qs, 15) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context.update( - { - "page_obj": page_obj, - "hosts": page_obj, - "search": search, - "status_filter": status_filter, - "connection_type_filter": conn_filter, - "status_choices": Host.STATUS_CHOICES, - "connection_type_choices": Host.CONNECTION_TYPE_CHOICES, - "page_title": "主机管理", - "active_nav": "hosts", - } - ) - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminHostDetailView(DetailView): - """ - 超管主机详情视图 - - 显示主机基本信息、提供商列表、管理员列表。 - """ - - template_name = "admin_base/hosts/host_detail.html" - model = Host - context_object_name = "host" - pk_url_kwarg = "pk" - - def get_queryset(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return Host.objects.all() - if self.request.user.is_site_group_admin(site_group): - if site_group: - return Host.objects.filter(site_group=site_group) - return Host.objects.none() - return get_provider_hosts(self.request.user, site_group=site_group) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.object - - # 获取关联产品 - from apps.operations.models import Product - - products = Product.objects.filter(host=host) - - # 获取关联用户数 - from apps.operations.models import CloudComputerUser - - user_count = CloudComputerUser.objects.filter(product__in=products).count() - - # 检查 session 中是否有生成的密码 - generated_password = None - if self.request.session.get("generated_password_host_id") == host.pk: - generated_password = self.request.session.get("generated_password") - self.request.session.pop("generated_password", None) - self.request.session.pop("generated_password_host_id", None) - - context.update( - { - "products": products, - "user_count": user_count, - "generated_password": generated_password, - "page_title": f"主机 - {host.name}", - "active_nav": "hosts", - } - ) - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminHostCreateView(TemplateView): - """ - 超管主机创建视图 - - 处理 GET 和 POST 请求,创建新主机。 - 自动设置 created_by 为当前用户。 - """ - - template_name = "admin_base/hosts/host_form.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - form = kwargs.get("form", AdminHostForm()) - context.update( - { - "form": form, - "page_title": "添加主机", - "active_nav": "hosts", - "is_create": True, - } - ) - context.update(_get_permission_context(form)) - context.update(_get_host_form_context()) - return context - - def post(self, request, *args, **kwargs): - form = AdminHostForm(request.POST, request.FILES) - if form.is_valid(): - init_token_value = form.cleaned_data.get("init_token", "") - logger.info( - f"Wizard save: init_token={'yes' if init_token_value else 'no'}, value={init_token_value[:8] if init_token_value else 'N/A'}" - ) - existing_host = None - cert_token_obj = None - if init_token_value: - from apps.bootstrap.models import CertProvisionToken - - try: - cert_token_obj = CertProvisionToken.objects.get( - token=init_token_value - ) - if cert_token_obj.host: - existing_host = cert_token_obj.host - except CertProvisionToken.DoesNotExist: - pass - - if existing_host: - for field in [ - "name", - "os_type", - "hostname", - "connection_type", - "auth_method", - "port", - "rdp_port", - "use_ssl", - "username", - "description", - ]: - if field in form.cleaned_data: - setattr(existing_host, field, form.cleaned_data[field]) - pwd = form.cleaned_data.get("password", "") - if pwd: - existing_host.password = pwd - existing_host.save() - form.instance = existing_host - form.save_m2m() - # 保存证书文件 - if existing_host.auth_method == "certificate": - form._save_cert_files(existing_host) - host = existing_host - else: - host = form.save(commit=False) - host.created_by = request.user - site_group = getattr(request, "site_group", None) - if site_group: - host.site_group = site_group - host.save() - form.save_m2m() - # 保存证书文件 - if host.auth_method == "certificate": - form._save_cert_files(host) - - if cert_token_obj and not existing_host: - cert_token_obj.host = host - if cert_token_obj.cert_data: - cd = cert_token_obj.cert_data - host.cert_root = cd.get("cert_root", "") - host.cert_sub = cd.get("cert_sub", "") - host.pfx_password = cd.get("pfx_password", "") - host.ntlm_fallback_user = cd.get("ntlm_user", "") - host.ntlm_fallback_password = cd.get("ntlm_password", "") - host.cert_provision_status = "ready" - # 根据 cert_root 和 cert_sub 计算证书路径 - if host.cert_root and host.cert_sub: - from utils.cert_storage import get_cert_dir - - cert_dir = get_cert_dir(host.cert_root, host.cert_sub) - host.cert_pem_path = str(cert_dir / "client.crt") - host.cert_key_path = str(cert_dir / "client.key") - host.save() - cert_token_obj.cert_data = None - cert_token_obj.save() - - # 测试连接(异步) - from apps.hosts.tasks import test_winrm_connection - - test_winrm_connection.delay(host.pk) - messages.success( - request, f"主机 {host.name} 创建成功," f"连接测试正在后台执行" - ) - - # 如果自动生成了密码,提示用户 - if hasattr(form, "generated_password") and form.generated_password: - messages.info( - request, f"已为主机 {host.name} 自动生成密码," f"请妥善保存。" - ) - request.session["generated_password"] = form.generated_password - request.session["generated_password_host_id"] = host.pk - - init_token = request.POST.get("init_token") - if host.auth_method == "certificate" and init_token and not cert_token_obj: - try: - from apps.bootstrap.models import CertProvisionToken - - CertProvisionToken.objects.filter( - token=init_token, - host__isnull=True, - ).update(host=host) - except Exception: - pass - - return redirect("admin:admin_hosts:host_detail", pk=host.pk) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminHostUpdateView(TemplateView): - """ - 超管主机编辑视图 - - 处理 GET 和 POST 请求,编辑主机信息。 - 密码字段可选,留空则不修改。 - """ - - template_name = "admin_base/hosts/host_form.html" - - def get_host(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(Host, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - Host.objects.filter(site_group=site_group), pk=self.kwargs["pk"] - ) - raise Http404 - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=site_group), - pk=self.kwargs["pk"], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.get_host() - form = kwargs.get("form", AdminHostForm(instance=host)) - context.update( - { - "form": form, - "host": host, - "page_title": f"编辑主机 - {host.name}", - "active_nav": "hosts", - "is_create": False, - } - ) - context.update(_get_permission_context(form, host)) - context.update(_get_host_form_context()) - return context - - def post(self, request, *args, **kwargs): - host = self.get_host() - form = AdminHostForm(request.POST, request.FILES, instance=host) - if form.is_valid(): - host = form.save() - - # 如果密码被修改,异步测试连接 - if "password" in form.changed_data and form.cleaned_data.get("password"): - from apps.hosts.tasks import test_winrm_connection - - test_winrm_connection.delay(host.pk) - messages.success( - request, f"主机 {host.name} 更新成功," f"连接测试正在后台执行" - ) - else: - messages.success(request, f"主机 {host.name} 更新成功") - - return redirect("admin:admin_hosts:host_detail", pk=host.pk) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminHostDeleteView(TemplateView): - """ - 超管主机删除视图 - - 显示确认页面,处理删除请求。 - 删除前清理关联的产品和公共主机信息。 - """ - - template_name = "admin_base/hosts/host_confirm_delete.html" - - def get_host(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(Host, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - Host.objects.filter(site_group=site_group), pk=self.kwargs["pk"] - ) - raise Http404 - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=site_group), - pk=self.kwargs["pk"], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.get_host() - - from apps.operations.models import Product - - products = Product.objects.filter(host=host) - - context.update( - { - "host": host, - "product_count": products.count(), - "page_title": f"删除主机 - {host.name}", - "active_nav": "hosts", - } - ) - return context - - def post(self, request, *args, **kwargs): - host = self.get_host() - - # 删除关联的产品和公共主机信息 - from apps.operations.models import Product, PublicHostInfo - - Product.objects.filter(host=host).delete() - PublicHostInfo.objects.filter(internal_host=host).delete() - - host_name = host.name - host.delete() - - messages.success(request, f"主机 {host_name} 已删除") - return redirect("admin:admin_hosts:host_list") - - -@admin_required -def admin_host_test_connection(request, pk): - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - host = get_object_or_404(Host, pk=pk) - elif request.user.is_site_group_admin(site_group): - if site_group: - host = get_object_or_404(Host.objects.filter(site_group=site_group), pk=pk) - else: - raise Http404 - else: - host = get_object_or_404( - get_provider_hosts(request.user, site_group=site_group), pk=pk - ) - - from apps.hosts.tasks import test_winrm_connection - - result = test_winrm_connection.delay(host.pk) - - return JsonResponse( - { - "success": True, - "task_id": result.id, - "message": "连接测试已提交,正在后台执行", - } - ) - - -# ========== 主机组管理 ========== - - -@method_decorator(admin_required, name="dispatch") -class AdminHostGroupListView(TemplateView): - """ - 超管主机组列表视图 - - 显示所有主机组,支持搜索。 - 包含提供商列和主机数量。 - """ - - template_name = "admin_base/hosts/hostgroup_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - hostgroups_qs = HostGroup.objects.all().order_by("-created_at") - elif self.request.user.is_site_group_admin(site_group): - if site_group: - hostgroups_qs = HostGroup.objects.filter( - site_group=site_group - ).order_by("-created_at") - else: - hostgroups_qs = HostGroup.objects.none() - else: - hostgroups_qs = HostGroup.objects.filter( - created_by=self.request.user - ).order_by("-created_at") - - # 搜索过滤 - search = self.request.GET.get("search", "").strip() - if search: - hostgroups_qs = hostgroups_qs.filter( - Q(name__icontains=search) | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(hostgroups_qs, 15) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context.update( - { - "page_obj": page_obj, - "hostgroups": page_obj, - "search": search, - "page_title": "主机组管理", - "active_nav": "hostgroups", - } - ) - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminHostGroupCreateView(TemplateView): - """ - 超管主机组创建视图 - - 处理 GET 和 POST 请求,创建新主机组。 - 自动设置 created_by 为当前用户。 - """ - - template_name = "admin_base/hosts/hostgroup_form.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update( - { - "form": kwargs.get("form", AdminHostGroupForm()), - "page_title": "创建主机组", - "active_nav": "hostgroups", - "is_create": True, - } - ) - return context - - def post(self, request, *args, **kwargs): - form = AdminHostGroupForm(request.POST) - if form.is_valid(): - hostgroup = form.save(commit=False) - hostgroup.created_by = request.user - site_group = getattr(request, "site_group", None) - if site_group: - hostgroup.site_group = site_group - hostgroup.save() - form.save_m2m() - - messages.success(request, f"主机组 {hostgroup.name} 创建成功") - return redirect("admin:admin_hosts:hostgroup_list") - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminHostGroupUpdateView(TemplateView): - """ - 超管主机组编辑视图 - - 处理 GET 和 POST 请求,编辑主机组信息。 - """ - - template_name = "admin_base/hosts/hostgroup_form.html" - - def get_hostgroup(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(HostGroup, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - HostGroup.objects.filter(site_group=site_group), - pk=self.kwargs["pk"], - ) - raise Http404 - return get_object_or_404( - HostGroup, - pk=self.kwargs["pk"], - created_by=self.request.user, - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - hostgroup = self.get_hostgroup() - form = kwargs.get("form", AdminHostGroupForm(instance=hostgroup)) - context.update( - { - "form": form, - "hostgroup": hostgroup, - "page_title": f"编辑主机组 - {hostgroup.name}", - "active_nav": "hostgroups", - "is_create": False, - } - ) - return context - - def post(self, request, *args, **kwargs): - hostgroup = self.get_hostgroup() - form = AdminHostGroupForm(request.POST, instance=hostgroup) - if form.is_valid(): - hostgroup = form.save() - messages.success(request, f"主机组 {hostgroup.name} 更新成功") - return redirect("admin:admin_hosts:hostgroup_list") - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminHostGroupDeleteView(TemplateView): - """ - 超管主机组删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = "admin_base/hosts/hostgroup_confirm_delete.html" - - def get_hostgroup(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(HostGroup, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - HostGroup.objects.filter(site_group=site_group), - pk=self.kwargs["pk"], - ) - raise Http404 - return get_object_or_404( - HostGroup, - pk=self.kwargs["pk"], - created_by=self.request.user, - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - hostgroup = self.get_hostgroup() - - context.update( - { - "hostgroup": hostgroup, - "host_count": hostgroup.hosts.count(), - "page_title": f"删除主机组 - {hostgroup.name}", - "active_nav": "hostgroups", - } - ) - return context - - def post(self, request, *args, **kwargs): - hostgroup = self.get_hostgroup() - hostgroup_name = hostgroup.name - hostgroup.delete() - - messages.success(request, f"主机组 {hostgroup_name} 已删除") - return redirect("admin:admin_hosts:hostgroup_list") - - -# ========== 主机创建向导 ========== - - -@admin_required -def admin_host_wizard(request): - """ - 主机创建向导视图 - - 引导超管分步添加主机: - - Step 1: 基本信息 (名称、地址、连接类型) - - Step 2: 连接配置 (端口、SSL、认证) - - Step 3: 分配提供商 (提供商、描述) - - 使用 Alpine.js 在客户端管理步骤切换, - 最终一次性提交表单创建主机。 - """ - if request.method == "POST": - form = HostWizardForm(request.POST, request.FILES) - if form.is_valid(): - host = form.save(commit=False) - host.created_by = request.user - site_group = getattr(request, "site_group", None) - if site_group: - host.site_group = site_group - host.save() - form.save_m2m() - # 保存证书文件 - if host.auth_method == "certificate": - form._save_cert_files(host) - - init_token_value = form.cleaned_data.get("init_token", "") - if init_token_value: - from apps.bootstrap.models import CertProvisionToken - - try: - cert_token_obj = CertProvisionToken.objects.get( - token=init_token_value - ) - if not cert_token_obj.host_id: - cert_token_obj.host = host - if cert_token_obj.cert_data: - cd = cert_token_obj.cert_data - host.cert_root = cd.get("cert_root", "") - host.cert_sub = cd.get("cert_sub", "") - host.pfx_password = cd.get("pfx_password", "") - host.ntlm_fallback_user = cd.get("ntlm_user", "") - host.ntlm_fallback_password = cd.get("ntlm_password", "") - host.cert_provision_status = "ready" - # 根据 cert_root 和 cert_sub 计算证书路径 - if host.cert_root and host.cert_sub: - from utils.cert_storage import get_cert_dir - - cert_dir = get_cert_dir(host.cert_root, host.cert_sub) - host.cert_pem_path = str(cert_dir / "client.crt") - host.cert_key_path = str(cert_dir / "client.key") - host.save() - cert_token_obj.cert_data = None - cert_token_obj.save() - host.refresh_from_db() - except CertProvisionToken.DoesNotExist: - pass - - from apps.hosts.tasks import test_winrm_connection - - test_winrm_connection.delay(host.pk) - messages.success( - request, f"主机 {host.name} 创建成功," f"连接测试正在后台执行" - ) - - # 如果自动生成了密码,提示用户 - if hasattr(form, "generated_password") and form.generated_password: - messages.info( - request, f"已为主机 {host.name} 自动生成密码," f"请妥善保存。" - ) - # 将生成的密码存入 session 以便在详情页展示 - request.session["generated_password"] = form.generated_password - request.session["generated_password_host_id"] = host.pk - - return redirect("admin:admin_hosts:host_detail", pk=host.pk) - else: - form = HostWizardForm() - - # 获取提供商列表及主机数量(用于向导第三步) - providers_with_count = form.get_providers_with_host_count() - - gateway_url = os.environ.get("TUNNEL_GATEWAY_URL", "wss://gateway.2c2a.com:9000") - server_base_url = os.environ.get( - "TUNNEL_SERVER_BASE_URL", request.build_absolute_uri("/").rstrip("/") - ) - - context = { - "form": form, - "providers_with_count": providers_with_count, - "connection_type_choices": [ - c - for c in Host.CONNECTION_TYPE_CHOICES - if c[0] in ("winrm", "localwinserver") - ], - "os_type_choices": Host.OS_TYPE_CHOICES, - "auth_method_choices": Host.AUTH_METHOD_CHOICES, - "default_ports": json.dumps(CONNECTION_DEFAULT_PORTS), - "default_ssl": json.dumps(CONNECTION_DEFAULT_SSL), - "is_local_winserver": json.dumps(_is_local_winserver()), - "gateway_url": gateway_url, - "server_base_url": server_base_url, - "page_title": "添加主机", - "active_nav": "hosts", - } - - return render( - request, - "admin_base/hosts/host_wizard.html", - context, - ) - - -@admin_required -def admin_host_wizard_generate_init_command(request): - if request.method != "POST": - return JsonResponse( - {"success": False, "error": "Method not allowed"}, - status=405, - ) - - import base64, json - from apps.bootstrap.models import CertProvisionToken - from apps.bootstrap.token_utils import encode_provision_token - - token_str = secrets.token_hex(32) - server_host = request.get_host() - scheme = "https" if request.is_secure() else "http" - - ip_address = "" - try: - body = json.loads(request.body) - ip_address = body.get("ip_address", "") - except (json.JSONDecodeError, ValueError): - ip_address = request.POST.get("ip_address", "") - - CertProvisionToken.objects.create( - token=token_str, - host=None, - server_host=server_host, - ip_address=ip_address, - expires_at=timezone.now() + timedelta(minutes=60), - status="ISSUED", - created_by=request.user, - ) - - encoded = encode_provision_token(token_str, scheme, server_host) - script_url = f"{scheme}://{server_host}/static/scripts/init.ps1" - one_liner = ( - f"$script = [Text.Encoding]::UTF8.GetString((iwr '{script_url}' -UseBasicParsing).RawContentStream.ToArray()); " - f"& ([ScriptBlock]::Create($script)) {encoded}" - ) - fallback_command = ( - f"$script = [Text.Encoding]::UTF8.GetString((iwr '{script_url}' -UseBasicParsing).RawContentStream.ToArray()); " - f"& ([ScriptBlock]::Create($script)) {encoded} debug" - ) - - return JsonResponse( - { - "success": True, - "data": { - "token": token_str, - "one_liner": one_liner, - "fallback_command": fallback_command, - }, - } - ) - - -@admin_required -def admin_host_wizard_generate_token(request): - if request.method != "POST": - return JsonResponse( - {"success": False, "error": "Method not allowed"}, status=405 - ) - - try: - token = secrets.token_urlsafe(32) - gateway_url = os.environ.get( - "TUNNEL_GATEWAY_URL", "wss://gateway.2c2a.com:9000" - ) - server_base_url = os.environ.get( - "TUNNEL_SERVER_BASE_URL", request.build_absolute_uri("/").rstrip("/") - ) - - return JsonResponse( - { - "success": True, - "tunnel_token": token, - "gateway_url": gateway_url, - "server_base_url": server_base_url, - } - ) - except Exception as e: - logger.error(f"Error generating tunnel token: {str(e)}", exc_info=True) - return JsonResponse( - { - "success": False, - "error": "Failed to generate tunnel token", - }, - status=500, - ) - - -@admin_required -def admin_host_wizard_test_connection(request): - if request.method != "POST": - return JsonResponse( - {"success": False, "error": "Method not allowed"}, status=405 - ) - - try: - data = json.loads(request.body) - except (json.JSONDecodeError, ValueError): - return JsonResponse({"success": False, "error": "请求数据格式无效"}, status=400) - - connection_type = data.get("connection_type", "winrm") - hostname = data.get("hostname", "").strip() - port = data.get("port", 5985) - use_ssl = data.get("use_ssl", False) - auth_method = data.get("auth_method", "ntlm") - username = data.get("username", "").strip() - password = data.get("password", "") - - if not hostname: - return JsonResponse({"success": False, "error": "主机地址不能为空"}, status=400) - - if auth_method == "ntlm": - if not username: - return JsonResponse( - {"success": False, "error": "用户名不能为空"}, status=400 - ) - if not password: - return JsonResponse({"success": False, "error": "密码不能为空"}, status=400) - - if auth_method == "certificate": - return JsonResponse( - { - "success": False, - "error": "证书认证方式请先保存主机后再测试连接", - } - ) - - from apps.hosts.tasks import test_winrm_connection_raw - - result = test_winrm_connection_raw.delay( - connection_type, - hostname, - port, - use_ssl, - auth_method, - username, - password, - ) - - return JsonResponse( - { - "success": True, - "task_id": result.id, - "message": "连接测试已提交,正在后台执行", - } - ) - - -@admin_required -def admin_host_generate_init_command(request, pk): - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - host = get_object_or_404(Host, pk=pk) - elif request.user.is_site_group_admin(site_group): - if site_group: - host = get_object_or_404(Host.objects.filter(site_group=site_group), pk=pk) - else: - raise Http404 - else: - host = get_object_or_404( - get_provider_hosts(request.user, site_group=site_group), pk=pk - ) - - if request.method != "POST": - return JsonResponse( - {"success": False, "error": "Method not allowed"}, - status=405, - ) - - try: - init_data = _generate_init_command_data(request, host) - return JsonResponse({"success": True, "data": init_data}) - except Exception as e: - return JsonResponse( - {"success": False, "error": str(e)}, - status=500, - ) - - -@login_required -def admin_host_generate_cert_command(request): - if request.method != "POST": - return JsonResponse( - {"success": False, "error": "Method not allowed"}, status=405 - ) - - host_id = request.POST.get("host_id") - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - host = get_object_or_404(Host, pk=host_id) - elif request.user.is_site_group_admin(site_group): - if site_group: - host = get_object_or_404( - Host.objects.filter(site_group=site_group), pk=host_id - ) - else: - raise Http404 - else: - host = get_object_or_404( - get_provider_hosts(request.user, site_group=site_group), pk=host_id - ) - - if host.auth_method != "certificate": - return JsonResponse( - {"success": False, "error": "Host is not configured for certificate auth"}, - status=400, - ) - - from apps.bootstrap.models import CertProvisionToken - from apps.bootstrap.token_utils import encode_provision_token - - token_str = secrets.token_hex(32) - server_host = request.get_host() - scheme = "https" if request.is_secure() else "http" - - provision_token = CertProvisionToken.objects.create( - token=token_str, - host=host, - server_host=server_host, - ip_address=host.hostname or "", - expires_at=timezone.now() + timedelta(minutes=60), - status="ISSUED", - created_by=request.user, - ) - - encoded = encode_provision_token(token_str, scheme, server_host) - script_url = f"{scheme}://{server_host}/static/scripts/init.ps1" - command = ( - f"$script = [Text.Encoding]::UTF8.GetString((iwr '{script_url}' -UseBasicParsing).RawContentStream.ToArray()); " - f"& ([ScriptBlock]::Create($script)) {encoded}" - ) - debug_command = ( - f"$script = [Text.Encoding]::UTF8.GetString((iwr '{script_url}' -UseBasicParsing).RawContentStream.ToArray()); " - f"& ([ScriptBlock]::Create($script)) {encoded} debug" - ) - - return JsonResponse( - { - "success": True, - "command": command, - "debug_command": debug_command, - "token": token_str, - "expires_at": provision_token.expires_at.isoformat(), - } - ) diff --git a/apps/hosts/views_provider.py b/apps/hosts/views_provider.py deleted file mode 100644 index 3199e17..0000000 --- a/apps/hosts/views_provider.py +++ /dev/null @@ -1,639 +0,0 @@ -""" -主机管理 - 提供商后台视图 - -所有视图均使用 @provider_required 装饰器保护,确保只有提供商用户可以访问。 -数据隔离通过 utils.provider 中的 get_provider_hosts 函数实现。 -""" - -import base64 -import json -import logging -import secrets - -from datetime import timedelta - -from django.contrib import messages -from django.core.paginator import Paginator -from django.db.models import Q -from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect -from django.utils import timezone -from django.utils.decorators import method_decorator -from django.views import View -from django.views.generic import DetailView, TemplateView - -from apps.accounts.provider_decorators import provider_required -from apps.provider.context_mixin import ProviderContextMixin -from utils.provider import get_provider_hosts - -from .forms_provider import HostCreateForm, HostUpdateForm, HostGroupForm -from .models import Host, HostGroup - -logger = logging.getLogger(__name__) - - -@method_decorator(provider_required, name='dispatch') -class HostListView(ProviderContextMixin, TemplateView): - """ - 主机列表视图 - - 提供分页、搜索和筛选功能,仅显示当前提供商可见的主机。 - """ - - template_name = 'admin_base/hosts/host_list.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - # 获取提供商可见的主机 - hosts_qs = get_provider_hosts(user, site_group=getattr(self.request, 'site_group', None)).order_by('-created_at') - - # 搜索过滤 - search = self.request.GET.get('search', '').strip() - if search: - hosts_qs = hosts_qs.filter( - Q(name__icontains=search) - | Q(hostname__icontains=search) - | Q(username__icontains=search) - ) - - # 状态过滤 - status_filter = self.request.GET.get('status', '').strip() - if status_filter: - hosts_qs = hosts_qs.filter(status=status_filter) - - # 连接类型过滤 - conn_filter = self.request.GET.get('connection_type', '').strip() - if conn_filter: - hosts_qs = hosts_qs.filter(connection_type=conn_filter) - - # 分页 - paginator = Paginator(hosts_qs, 15) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'hosts': page_obj, - 'search': search, - 'status_filter': status_filter, - 'connection_type_filter': conn_filter, - 'status_choices': Host.STATUS_CHOICES, - 'connection_type_choices': Host.CONNECTION_TYPE_CHOICES, - 'page_title': '主机管理', - 'active_nav': 'hosts', - }) - return context - - -@method_decorator(provider_required, name='dispatch') -class HostDetailView(ProviderContextMixin, DetailView): - """ - 主机详情视图 - - 显示主机基本信息、隧道状态、关联产品等。 - """ - - template_name = 'admin_base/hosts/host_detail.html' - model = Host - context_object_name = 'host' - pk_url_kwarg = 'pk' - provider_url_namespace = 'provider:provider_hosts' - - def get_queryset(self): - """确保提供商只能查看自己的主机""" - return get_provider_hosts(self.request.user, site_group=getattr(self.request, 'site_group', None)) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.object - - # 获取关联产品 - from apps.operations.models import Product - products = Product.objects.filter(host=host) - - # 获取关联用户数 - from apps.operations.models import CloudComputerUser - user_count = CloudComputerUser.objects.filter( - product__in=products - ).count() - - # 检查 session 中是否有生成的密码 - generated_password = None - if ( - self.request.session.get('generated_password_host_id') - == host.pk - ): - generated_password = self.request.session.get( - 'generated_password' - ) - # 一次性读取后清除 - self.request.session.pop('generated_password', None) - self.request.session.pop('generated_password_host_id', None) - - context.update({ - 'products': products, - 'user_count': user_count, - 'generated_password': generated_password, - 'page_title': f'主机 - {host.name}', - 'active_nav': 'hosts', - }) - return context - - -@method_decorator(provider_required, name='dispatch') -class HostCreateView(ProviderContextMixin, TemplateView): - """ - 主机创建视图 - - 处理 GET 和 POST 请求,创建新主机。 - 自动设置 created_by 为当前用户,并将用户添加到 administrators。 - """ - - template_name = 'admin_base/hosts/host_form.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'form': kwargs.get('form', HostCreateForm()), - 'page_title': '添加主机', - 'active_nav': 'hosts', - 'is_create': True, - }) - return context - - def post(self, request, *args, **kwargs): - form = HostCreateForm(request.POST) - if form.is_valid(): - host = form.save(commit=False) - host.created_by = request.user - host.save() - # 将创建者添加到管理员列表 - host.administrators.add(request.user) - - # 测试连接(异步) - from apps.hosts.tasks import test_winrm_connection - test_winrm_connection.delay(host.pk) - messages.success( - request, - f'主机 {host.name} 创建成功,' - f'连接测试正在后台执行' - ) - - # 如果自动生成了密码,提示用户 - if form.generated_password: - messages.info( - request, - f'已为主机 {host.name} 自动生成密码,请妥善保存。' - ) - # 将生成的密码存入 session 以便在详情页展示 - request.session['generated_password'] = ( - form.generated_password - ) - request.session['generated_password_host_id'] = host.pk - - return redirect( - 'provider:provider_hosts:host_detail', pk=host.pk - ) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(provider_required, name='dispatch') -class HostUpdateView(ProviderContextMixin, TemplateView): - """ - 主机编辑视图 - - 处理 GET 和 POST 请求,编辑主机信息。 - 密码字段可选,留空则不修改。 - """ - - template_name = 'admin_base/hosts/host_form.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_host(self): - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.get_host() - form = kwargs.get('form', HostUpdateForm(instance=host)) - context.update({ - 'form': form, - 'host': host, - 'page_title': f'编辑主机 - {host.name}', - 'active_nav': 'hosts', - 'is_create': False, - }) - return context - - def post(self, request, *args, **kwargs): - host = self.get_host() - form = HostUpdateForm(request.POST, instance=host) - if form.is_valid(): - host = form.save() - - # 如果密码被修改,异步测试连接 - if ( - 'password' in form.changed_data - and form.cleaned_data.get('password') - ): - from apps.hosts.tasks import test_winrm_connection - test_winrm_connection.delay(host.pk) - messages.success( - request, - f'主机 {host.name} 更新成功,' - f'连接测试正在后台执行' - ) - else: - messages.success( - request, f'主机 {host.name} 更新成功' - ) - - return redirect( - 'provider:provider_hosts:host_detail', pk=host.pk - ) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(provider_required, name='dispatch') -class HostDeleteView(ProviderContextMixin, TemplateView): - """ - 主机删除视图 - - 显示确认页面,处理删除请求。 - 删除前清理关联的产品和公共主机信息。 - """ - - template_name = 'admin_base/hosts/host_confirm_delete.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_host(self): - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - host = self.get_host() - - from apps.operations.models import Product - products = Product.objects.filter(host=host) - - context.update({ - 'host': host, - 'product_count': products.count(), - 'page_title': f'删除主机 - {host.name}', - 'active_nav': 'hosts', - }) - return context - - def post(self, request, *args, **kwargs): - host = self.get_host() - - # 删除关联的产品和公共主机信息 - from apps.operations.models import Product, PublicHostInfo - Product.objects.filter(host=host).delete() - PublicHostInfo.objects.filter(internal_host=host).delete() - - host_name = host.name - host.delete() - - messages.success(request, f'主机 {host_name} 已删除') - return redirect('provider:provider_hosts:host_list') - - -@method_decorator(provider_required, name='dispatch') -class HostDeployCommandView(View): - """ - 生成部署命令视图 - - 为主机生成部署命令,复用 Admin 中的部署命令生成逻辑。 - 返回 JSON 响应,供前端 AJAX 调用。 - """ - - def get_host(self): - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def get(self, request, *args, **kwargs): - host = self.get_host() - try: - from apps.bootstrap.models import InitialToken - - current_time = timezone.now() - valid_tokens = InitialToken.objects.filter( - host=host, - status__in=['ISSUED', 'PAIRED'], - expires_at__gt=current_time - ).order_by('-created_at') - - if valid_tokens.exists(): - existing_token = valid_tokens.first() - initial_token = existing_token - created = False - token_message = '复用现有引导令牌' - pairing_code = initial_token.generate_pairing_code() - else: - token = secrets.token_urlsafe(32) - expires_at = current_time + timedelta(hours=24) - - initial_token, created = ( - InitialToken.objects.get_or_create( - token=token, - defaults={ - 'host': host, - 'expires_at': expires_at, - 'status': 'ISSUED' - } - ) - ) - token_message = '新引导令牌已生成' - pairing_code = initial_token.generate_pairing_code() - - current_site = request.build_absolute_uri('/') - - secret_data = { - 'c_side_url': current_site.rstrip('/'), - 'token': initial_token.token, - 'host_id': str(host.id), - 'hostname': host.hostname, - 'generated_at': current_time.isoformat(), - 'expires_at': initial_token.expires_at.isoformat() - } - - json_str = json.dumps(secret_data, ensure_ascii=False) - encoded_bytes = base64.b64encode(json_str.encode('utf-8')) - encoded_str = encoded_bytes.decode('utf-8') - - deploy_command = ( - f'.\\h_side_init.exe "{encoded_str}"' - ) - - return JsonResponse({ - 'success': True, - 'deploy_command': deploy_command, - 'secret': encoded_str, - 'pairing_code': pairing_code, - 'pairing_code_expiry': ( - initial_token.pairing_code_expires_at.isoformat() - if initial_token.pairing_code_expires_at else None - ), - 'expires_at': initial_token.expires_at.isoformat(), - 'message': f'{token_message},将在24小时后过期', - 'token_id': initial_token.token, - 'token_status': initial_token.status, - 'created_new': created - }) - - except Exception as e: - logger.error( - f'生成部署命令时发生错误: {str(e)}', exc_info=True - ) - return JsonResponse({ - 'success': False, - 'error': '生成部署命令失败' - }, status=500) - - -@method_decorator(provider_required, name='dispatch') -class HostToggleActiveView(View): - """ - 切换主机活跃状态视图 - - AJAX 端点,用于快速切换主机的在线/离线状态。 - """ - - def get_host(self): - return get_object_or_404( - get_provider_hosts(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def post(self, request, *args, **kwargs): - host = self.get_host() - - if host.status == 'online': - new_status = 'offline' - Host.objects.filter(pk=host.pk).update(status=new_status) - else: - from apps.hosts.tasks import test_winrm_connection - test_winrm_connection.delay(host.pk) - new_status = 'pending' - - status_display = dict(Host.STATUS_CHOICES).get( - new_status, new_status - ) - return JsonResponse({ - 'success': True, - 'status': new_status, - 'status_display': status_display, - }) - - -# ========== 主机组管理 ========== - - -def get_provider_hostgroups(user, site_group=None): - qs = HostGroup.objects.filter( - Q(created_by=user) | Q(providers=user) - ).distinct() - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - -@method_decorator(provider_required, name='dispatch') -class HostGroupListView(ProviderContextMixin, TemplateView): - """ - 主机组列表视图 - - 提供分页和搜索功能,仅显示当前提供商可见的主机组。 - """ - - template_name = 'admin_base/hosts/hostgroup_list.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - # 获取提供商可见的主机组 - hostgroups_qs = get_provider_hostgroups(user, site_group=getattr(self.request, 'site_group', None)).order_by( - '-created_at' - ) - - # 搜索过滤 - search = self.request.GET.get('search', '').strip() - if search: - hostgroups_qs = hostgroups_qs.filter( - Q(name__icontains=search) - | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(hostgroups_qs, 15) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'hostgroups': page_obj, - 'search': search, - 'page_title': '主机组管理', - 'active_nav': 'hosts', - }) - return context - - -@method_decorator(provider_required, name='dispatch') -class HostGroupCreateView(ProviderContextMixin, TemplateView): - """ - 主机组创建视图 - - 处理 GET 和 POST 请求,创建新主机组。 - 自动设置 created_by 为当前用户。 - """ - - template_name = 'admin_base/hosts/hostgroup_form.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'form': kwargs.get( - 'form', - HostGroupForm(provider_user=self.request.user) - ), - 'page_title': '创建主机组', - 'active_nav': 'hosts', - 'is_create': True, - }) - return context - - def post(self, request, *args, **kwargs): - form = HostGroupForm( - request.POST, provider_user=request.user - ) - if form.is_valid(): - hostgroup = form.save(commit=False) - hostgroup.created_by = request.user - hostgroup.save() - form.save_m2m() - - messages.success( - request, - f'主机组 {hostgroup.name} 创建成功' - ) - return redirect( - 'provider:provider_hosts:hostgroup_list' - ) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(provider_required, name='dispatch') -class HostGroupUpdateView(ProviderContextMixin, TemplateView): - """ - 主机组编辑视图 - - 处理 GET 和 POST 请求,编辑主机组信息。 - """ - - template_name = 'admin_base/hosts/hostgroup_form.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_hostgroup(self): - return get_object_or_404( - get_provider_hostgroups(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - hostgroup = self.get_hostgroup() - form = kwargs.get( - 'form', - HostGroupForm( - instance=hostgroup, - provider_user=self.request.user - ) - ) - context.update({ - 'form': form, - 'hostgroup': hostgroup, - 'page_title': f'编辑主机组 - {hostgroup.name}', - 'active_nav': 'hosts', - 'is_create': False, - }) - return context - - def post(self, request, *args, **kwargs): - hostgroup = self.get_hostgroup() - form = HostGroupForm( - request.POST, - instance=hostgroup, - provider_user=request.user - ) - if form.is_valid(): - hostgroup = form.save() - messages.success( - request, - f'主机组 {hostgroup.name} 更新成功' - ) - return redirect( - 'provider:provider_hosts:hostgroup_list' - ) - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(provider_required, name='dispatch') -class HostGroupDeleteView(ProviderContextMixin, TemplateView): - """ - 主机组删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = 'admin_base/hosts/hostgroup_confirm_delete.html' - provider_url_namespace = 'provider:provider_hosts' - - def get_hostgroup(self): - return get_object_or_404( - get_provider_hostgroups(self.request.user, site_group=getattr(self.request, 'site_group', None)), - pk=self.kwargs['pk'] - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - hostgroup = self.get_hostgroup() - - context.update({ - 'hostgroup': hostgroup, - 'host_count': hostgroup.hosts.count(), - 'page_title': f'删除主机组 - {hostgroup.name}', - 'active_nav': 'hosts', - }) - return context - - def post(self, request, *args, **kwargs): - hostgroup = self.get_hostgroup() - hostgroup_name = hostgroup.name - hostgroup.delete() - - messages.success( - request, - f'主机组 {hostgroup_name} 已删除' - ) - return redirect('provider:provider_hosts:hostgroup_list') diff --git a/apps/operations/__init__.py b/apps/operations/__init__.py deleted file mode 100755 index 6723564..0000000 --- a/apps/operations/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -2c2a操作记录应用 -""" -default_app_config = 'apps.operations.apps.OperationsConfig' - -# 导入模型中的信号,以便可以从apps.operations直接导入 -try: - # 防止在Django应用初始化期间导入模型导致的AppRegistryNotReady错误 - from django.apps import apps - if apps.ready: - from .models import account_opening_request_pre_submit, account_opening_request_post_submit -except: - # 如果应用尚未准备好,不导出信号 - pass \ No newline at end of file diff --git a/apps/operations/admin.py b/apps/operations/admin.py deleted file mode 100755 index 9752161..0000000 --- a/apps/operations/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.operations.views_admin) 和提供商后台 (apps.operations.views_provider) diff --git a/apps/operations/apps.py b/apps/operations/apps.py deleted file mode 100755 index 040f2ea..0000000 --- a/apps/operations/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -操作记录应用配置 -""" -from django.apps import AppConfig - - -class OperationsConfig(AppConfig): - """操作记录应用配置类""" - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.operations' - verbose_name = '操作记录' diff --git a/apps/operations/forms.py b/apps/operations/forms.py deleted file mode 100755 index 9bc4a2a..0000000 --- a/apps/operations/forms.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -操作记录表单 -""" - -import json -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import SystemTask, AccountOpeningRequest, CloudComputerUser -from apps.hosts.models import Host - - -class SystemTaskFilterForm(forms.Form): - """ - 系统任务过滤表单 - """ - - task_type = forms.CharField( - required=False, - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": "任务类型"} - ), - label=_("任务类型"), - ) - - status = forms.ChoiceField( - required=False, - choices=[("", "全部")] + SystemTask._meta.get_field("status").choices, - widget=forms.Select(attrs={"class": "form-control"}), - label=_("状态"), - ) - - start_date = forms.DateField( - required=False, - widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}), - label=_("开始日期"), - ) - - end_date = forms.DateField( - required=False, - widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}), - label=_("结束日期"), - ) - - -class AccountOpeningRequestForm(forms.ModelForm): - """ - 用户开户申请表单 - """ - - # 重写username字段以符合新需求 - username = forms.CharField( - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": "请输入主机连接用户名"} - ), - label=_("主机连接用户名"), - help_text=_("将在云电脑主机上创建的连接用户名"), - ) - - # 重写user_fullname字段以符合新需求 - user_fullname = forms.CharField( - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": "请输入显示用户名"} - ), - label=_("主机显示用户名"), - help_text=_("用于在系统中显示的用户名"), - ) - - # 重写user_description字段以符合新需求 - user_description = forms.CharField( - widget=forms.Textarea( - attrs={"class": "form-control", "rows": 4, "placeholder": "请输入申请理由"} - ), - label=_("申请理由"), - help_text=_("请说明申请云电脑主机的用途和理由"), - ) - - target_product = forms.ModelChoiceField( - queryset=None, # 动态设置 - widget=forms.Select(attrs={"class": "form-control"}), - label=_("目标产品"), - help_text=_("请选择您要申请的产品"), - ) - - requested_disk_capacity = forms.CharField( - required=False, - widget=forms.HiddenInput(), - label=_("需求磁盘容量"), - help_text=_("额外申请的磁盘容量(MB)"), - ) - - class Meta: - model = AccountOpeningRequest - fields = [ - "username", - "user_fullname", - "user_description", - "target_product", - "requested_disk_capacity", - ] - - def __init__(self, *args, **kwargs): - # 从视图传入的产品查询集 - products_qs = kwargs.pop("products_qs", None) - site_group = kwargs.pop("site_group", None) - super().__init__(*args, **kwargs) - - if products_qs is not None: - self.fields["target_product"].queryset = products_qs - else: - # 默认按 site_group 过滤可用产品 - from .models import Product - - qs = Product.objects.filter(is_available=True) - if site_group: - qs = qs.filter(site_group=site_group) - else: - qs = qs.filter(site_group__isnull=True) - self.fields["target_product"].queryset = qs - - # 如果只有一个产品选项,将其设为初始值并隐藏 - if len(self.fields["target_product"].queryset) == 1: - target_product = self.fields["target_product"].queryset.first() - self.fields["target_product"].initial = target_product - # 将字段设为隐藏 - self.fields["target_product"].widget = forms.HiddenInput() - - def clean_requested_disk_capacity(self): - data = self.cleaned_data.get("requested_disk_capacity", "{}") - if not data: - return {} - if isinstance(data, dict): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError("磁盘容量格式无效") - if not isinstance(parsed, dict): - raise forms.ValidationError("磁盘容量必须为字典格式") - for disk, value in parsed.items(): - try: - val = int(value) - if val < 0: - raise forms.ValidationError(f"磁盘 {disk} 的容量不能为负数") - except (ValueError, TypeError): - raise forms.ValidationError(f"磁盘 {disk} 的容量必须为数字") - return parsed - - -class AccountOpeningRequestFilterForm(forms.Form): - """ - 开户申请过滤表单 - """ - - status = forms.ChoiceField( - required=False, - choices=[("", "全部")] - + AccountOpeningRequest._meta.get_field("status").choices, - widget=forms.Select(attrs={"class": "form-control"}), - label=_("状态"), - ) - - host = forms.ModelChoiceField( - required=False, - queryset=Host.objects.none(), - widget=forms.Select(attrs={"class": "form-control"}), - label=_("主机"), - ) - - search = forms.CharField( - required=False, - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": "搜索用户名、姓名或邮箱"} - ), - label=_("搜索"), - ) - - def __init__(self, *args, **kwargs): - site_group = kwargs.pop("site_group", None) - super().__init__(*args, **kwargs) - if site_group: - self.fields["host"].queryset = Host.objects.filter(site_group=site_group) - else: - self.fields["host"].queryset = Host.objects.filter(site_group__isnull=True) - - -class CloudComputerUserFilterForm(forms.Form): - """ - 云电脑用户过滤表单 - """ - - status = forms.ChoiceField( - required=False, - choices=[("", "全部")] + CloudComputerUser._meta.get_field("status").choices, - widget=forms.Select(attrs={"class": "form-control"}), - label=_("状态"), - ) - - product = forms.ModelChoiceField( - required=False, - queryset=None, - widget=forms.Select(attrs={"class": "form-control"}), - label=_("产品"), - ) - - search = forms.CharField( - required=False, - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": "搜索用户名、姓名或邮箱"} - ), - label=_("搜索"), - ) - - def __init__(self, *args, **kwargs): - site_group = kwargs.pop("site_group", None) - super().__init__(*args, **kwargs) - from .models import Product - - if site_group: - self.fields["product"].queryset = Product.objects.filter( - site_group=site_group - ) - else: - self.fields["product"].queryset = Product.objects.filter( - site_group__isnull=True - ) diff --git a/apps/operations/forms_admin.py b/apps/operations/forms_admin.py deleted file mode 100644 index 605b98e..0000000 --- a/apps/operations/forms_admin.py +++ /dev/null @@ -1,323 +0,0 @@ -""" -运营管理 - 超级管理员后台表单 - -包含产品、产品组、开户申请驳回等表单。 -超管可查看所有记录;站点组管理员仅可查看当前站点组记录; -提供商仅可查看自己相关的记录。 -""" - -import json -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import Product, ProductGroup - - -# MD3 风格的通用 CSS 类 -_INPUT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface placeholder-md-outline ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary transition' -) -_SELECT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer' -) -_CHECKBOX_CLASS = ( - 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary' -) -_TEXTAREA_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface placeholder-md-outline ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition resize-y' -) - - -class AdminProductForm(forms.ModelForm): - """ - 超管产品管理表单 - - 与提供商表单类似,但不做数据隔离: - - 所有主机均可选择 - - 所有产品组均可选择 - """ - - default_disk_quota = forms.CharField( - label=_('默认磁盘配额'), - required=False, - widget=forms.Textarea(attrs={ - 'rows': 3, - 'placeholder': '{"C:": 10240, "D:": 20480}', - 'class': _TEXTAREA_CLASS + ' font-mono text-sm', - }), - help_text=_( - '每个磁盘的默认配额大小(MB),' - 'JSON 格式,如 {"C:": 10240, "D:": 20480}' - ), - ) - - allow_extra_quota_disks = forms.CharField( - label=_('允许额外申请容量的磁盘'), - required=False, - widget=forms.Textarea(attrs={ - 'rows': 2, - 'placeholder': '["C:", "D:"]', - 'class': _TEXTAREA_CLASS + ' font-mono text-sm', - }), - help_text=_( - '允许用户在申请时额外申请容量的磁盘列表,' - 'JSON 数组格式,如 ["C:", "D:"]' - ), - ) - - class Meta: - model = Product - fields = [ - 'display_name', 'display_description', - 'product_group', - 'host', 'is_available', 'auto_approval', 'visibility', - 'enable_host_protection', - 'display_hostname', 'rdp_port', - 'enable_disk_quota', - ] - widgets = { - 'display_name': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入产品显示名称', - }), - 'display_description': forms.Textarea(attrs={ - 'class': _TEXTAREA_CLASS, - 'rows': 3, - 'placeholder': '输入产品显示描述(可选)', - }), - 'product_group': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'host': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'is_available': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'auto_approval': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'visibility': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'enable_host_protection': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'display_hostname': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入显示地址', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '3389', - }), - 'enable_disk_quota': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - } - labels = { - 'display_name': _('显示名称'), - 'display_description': _('显示描述'), - 'product_group': _('产品组'), - 'host': _('关联主机'), - 'is_available': _('是否可用'), - 'auto_approval': _('自动审核'), - 'visibility': _('可见性'), - 'enable_host_protection': _('启用主机保护(Gateway)'), - 'display_hostname': _('显示地址'), - 'rdp_port': _('RDP端口'), - 'enable_disk_quota': _('启用磁盘配额管理'), - } - help_texts = { - 'host': _('此产品运行所在的主机'), - 'is_available': _('是否在前端展示此产品'), - 'auto_approval': _('是否自动批准针对此产品的开户申请'), - 'visibility': _( - '公开对所有用户可见,' - '邀请访问仅对已授权用户可见' - ), - 'enable_host_protection': _( - '启用后,用户只能通过Gateway隧道访问RDP,' - '主机不暴露公网IP。需部署Gateway服务。' - ), - 'enable_disk_quota': _( - '是否启用磁盘配额管理,' - '启用后将自动为新用户设置磁盘配额' - ), - } - - def __init__(self, *args, user=None, site_group=None, **kwargs): - super().__init__(*args, **kwargs) - - from apps.hosts.models import Host - from utils.provider import get_provider_hosts - - if user and user.is_superuser: - host_qs = Host.objects.all() - pg_qs = ProductGroup.objects.all() - elif user and site_group and user.is_site_group_admin(site_group): - host_qs = Host.objects.filter(site_group=site_group) - pg_qs = ProductGroup.objects.filter(site_group=site_group) - elif user: - host_qs = get_provider_hosts(user, site_group=site_group) - pg_qs = ProductGroup.objects.filter(created_by=user) - else: - host_qs = Host.objects.all() - pg_qs = ProductGroup.objects.all() - - self.fields['host'].queryset = host_qs.order_by('name') - self.fields['product_group'].queryset = pg_qs.order_by('name') - - # 初始化 JSON 字段的显示值 - if self.instance and self.instance.pk: - if self.instance.default_disk_quota: - self.initial['default_disk_quota'] = json.dumps( - self.instance.default_disk_quota, - ensure_ascii=False, - indent=2, - ) - if self.instance.allow_extra_quota_disks: - self.initial['allow_extra_quota_disks'] = json.dumps( - self.instance.allow_extra_quota_disks, - ensure_ascii=False, - ) - - def clean_default_disk_quota(self): - data = self.cleaned_data.get('default_disk_quota', '') - if not data or not data.strip(): - return {} - if isinstance(data, dict): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('磁盘配额格式无效,请输入有效的 JSON') - if not isinstance(parsed, dict): - raise forms.ValidationError( - '磁盘配额必须为字典格式,如 {"C:": 10240}' - ) - for disk, value in parsed.items(): - try: - val = int(value) - if val < 0: - raise forms.ValidationError( - f'磁盘 {disk} 的配额不能为负数' - ) - except (ValueError, TypeError): - raise forms.ValidationError( - f'磁盘 {disk} 的配额必须为数字' - ) - return parsed - - def clean_allow_extra_quota_disks(self): - data = self.cleaned_data.get('allow_extra_quota_disks', '') - if not data or not data.strip(): - return [] - if isinstance(data, list): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError( - '磁盘列表格式无效,请输入有效的 JSON 数组' - ) - if not isinstance(parsed, list): - raise forms.ValidationError( - '磁盘列表必须为数组格式,如 ["C:", "D:"]' - ) - return parsed - - def save(self, commit=True): - instance = super().save(commit=False) - instance.default_disk_quota = self.cleaned_data.get( - 'default_disk_quota', {} - ) - instance.allow_extra_quota_disks = self.cleaned_data.get( - 'allow_extra_quota_disks', [] - ) - if commit: - instance.save() - return instance - - -class AdminProductGroupForm(forms.ModelForm): - """ - 超管产品组管理表单 - - 包含所有字段,不做数据隔离。 - """ - - class Meta: - model = ProductGroup - fields = [ - 'name', 'description', - 'display_order', 'is_active', 'visibility', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入产品组名称', - }), - 'description': forms.Textarea(attrs={ - 'class': _TEXTAREA_CLASS, - 'rows': 3, - 'placeholder': '输入产品组描述(可选)', - }), - 'display_order': forms.NumberInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '0', - 'min': '0', - }), - 'is_active': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'visibility': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - } - labels = { - 'name': _('产品组名称'), - 'description': _('描述'), - 'display_order': _('显示顺序'), - 'is_active': _('是否启用'), - 'visibility': _('可见性'), - } - help_texts = { - 'display_order': _( - '产品组在前端展示的顺序,数字越小越靠前' - ), - 'is_active': _('是否在前端展示此产品组'), - 'visibility': _( - '公开对所有用户可见,' - '邀请访问仅对已授权用户可见' - ), - } - - -class AdminRequestRejectForm(forms.Form): - """ - 超管驳回开户申请表单 - - 用于输入驳回原因 - """ - - rejection_reason = forms.CharField( - label='驳回原因', - required=True, - widget=forms.Textarea(attrs={ - 'rows': 4, - 'placeholder': '请输入驳回原因...', - 'class': _TEXTAREA_CLASS, - }), - help_text='驳回原因将作为审核备注发送给申请人', - ) diff --git a/apps/operations/forms_provider.py b/apps/operations/forms_provider.py deleted file mode 100644 index 0e62245..0000000 --- a/apps/operations/forms_provider.py +++ /dev/null @@ -1,480 +0,0 @@ -""" -运营管理 - 提供商后台表单 - -包含开户申请、云电脑用户管理、邀请令牌、产品、产品组相关的表单。 -""" - -import json -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import Product, ProductGroup, ProductInvitationToken - - -class AccountOpeningRequestRejectForm(forms.Form): - """ - 驳回开户申请表单 - - 用于输入驳回原因 - """ - - rejection_reason = forms.CharField( - label='驳回原因', - required=True, - widget=forms.Textarea(attrs={ - 'rows': 4, - 'placeholder': '请输入驳回原因...', - 'class': 'w-full px-4 py-3 bg-md-surface-container-high/50 border border-white/10 ' - 'rounded-md text-md-on-surface placeholder-md-on-surface-variant/50 ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary focus:border-transparent ' - 'transition resize-none', - }), - help_text='驳回原因将作为审核备注发送给申请人', - ) - - -class CloudComputerUserDiskQuotaForm(forms.Form): - """ - 磁盘配额设置表单 - - 用于设置用户的磁盘配额(MB),格式为 {"C:": 10240, "D:": 20480} - """ - - disk_quota = forms.CharField( - label=_('磁盘配额'), - help_text=_('JSON 格式,例如: {"C:": 10240, "D:": 20480}'), - widget=forms.Textarea(attrs={ - 'rows': 4, - 'placeholder': '{"C:": 10240, "D:": 20480}', - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition resize-y font-mono text-sm', - }), - ) - - def clean_disk_quota(self): - data = self.cleaned_data.get('disk_quota', '{}') - if not data or not data.strip(): - return {} - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('磁盘配额格式无效,请输入有效的 JSON 格式') - if not isinstance(parsed, dict): - raise forms.ValidationError('磁盘配额必须为字典格式,例如: {"C:": 10240}') - for disk, value in parsed.items(): - try: - val = int(value) - if val < 0: - raise forms.ValidationError(f'磁盘 {disk} 的配额不能为负数') - except (ValueError, TypeError): - raise forms.ValidationError(f'磁盘 {disk} 的配额必须为数字') - return parsed - - -class CloudComputerUserResetPasswordForm(forms.Form): - """ - 重置密码表单 - - 用于重置云电脑用户的 Windows 密码 - """ - - new_password = forms.CharField( - label=_('新密码'), - help_text=_('设置用户的新密码,建议使用复杂密码'), - min_length=8, - max_length=128, - widget=forms.PasswordInput(attrs={ - 'autocomplete': 'new-password', - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '请输入新密码', - }), - ) - - confirm_password = forms.CharField( - label=_('确认密码'), - help_text=_('再次输入新密码以确认'), - widget=forms.PasswordInput(attrs={ - 'autocomplete': 'new-password', - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '请再次输入新密码', - }), - ) - - def clean(self): - cleaned_data = super().clean() - new_password = cleaned_data.get('new_password') - confirm_password = cleaned_data.get('confirm_password') - - if new_password and confirm_password and new_password != confirm_password: - raise forms.ValidationError('两次输入的密码不一致') - - return cleaned_data - - -class ProductInvitationTokenForm(forms.ModelForm): - """ - 产品邀请令牌创建表单 - - 提供商只能选择自己创建的产品/产品组 - """ - - class Meta: - model = ProductInvitationToken - fields = ['product', 'product_group', 'max_uses', 'expires_at', 'is_active'] - widgets = { - 'product': forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface appearance-none focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition cursor-pointer', - }), - 'product_group': forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface appearance-none focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition cursor-pointer', - }), - 'max_uses': forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition', - 'placeholder': '0 表示无限制', - 'min': '0', - }), - 'expires_at': forms.DateTimeInput(attrs={ - 'type': 'datetime-local', - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition', - }), - 'is_active': forms.CheckboxInput(attrs={ - 'class': 'rounded border-md-outline bg-md-surface-container-high text-md-primary ' - 'focus:ring-md-primary focus:ring-2 cursor-pointer', - }), - } - labels = { - 'product': _('关联产品'), - 'product_group': _('关联产品组'), - 'max_uses': _('最大使用次数'), - 'expires_at': _('过期时间'), - 'is_active': _('是否启用'), - } - help_texts = { - 'product': _('此令牌关联的产品(与产品组至少选一个)'), - 'product_group': _('此令牌关联的产品组(与产品至少选一个)'), - 'max_uses': _('令牌最大可使用次数,0表示无限制'), - 'expires_at': _('令牌过期时间,留空表示永不过期'), - } - - def __init__(self, *args, **kwargs): - self.provider_user = kwargs.pop('provider_user', None) - super().__init__(*args, **kwargs) - - if self.provider_user: - from apps.operations.models import Product, ProductGroup - self.fields['product'].queryset = Product.objects.filter( - created_by=self.provider_user - ) - self.fields['product_group'].queryset = ProductGroup.objects.filter( - created_by=self.provider_user - ) - - def clean(self): - cleaned_data = super().clean() - product = cleaned_data.get('product') - product_group = cleaned_data.get('product_group') - if not product and not product_group: - raise forms.ValidationError( - _('关联产品和关联产品组至少需要选择一个。') - ) - return cleaned_data - - -# ========== 产品管理表单 ========== - - -# MD3 风格的通用 CSS 类 -_INPUT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface placeholder-md-outline ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary transition' -) -_SELECT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer' -) -_CHECKBOX_CLASS = ( - 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary' -) -_TEXTAREA_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md ' - 'px-4 py-3 text-md-on-surface placeholder-md-outline ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition resize-y' -) - - -class ProductForm(forms.ModelForm): - """ - 产品管理表单 - - 复用 Admin 中 ProductAdminForm 的逻辑,使用 MD3 风格的 Widget。 - 包含磁盘配额配置和主机保护开关。 - 提供商只能选择自己可见的主机。 - """ - - default_disk_quota = forms.CharField( - label=_('默认磁盘配额'), - required=False, - widget=forms.Textarea(attrs={ - 'rows': 3, - 'placeholder': '{"C:": 10240, "D:": 20480}', - 'class': _TEXTAREA_CLASS + ' font-mono text-sm', - }), - help_text=_( - '每个磁盘的默认配额大小(MB),' - 'JSON 格式,如 {"C:": 10240, "D:": 20480}' - ), - ) - - allow_extra_quota_disks = forms.CharField( - label=_('允许额外申请容量的磁盘'), - required=False, - widget=forms.Textarea(attrs={ - 'rows': 2, - 'placeholder': '["C:", "D:"]', - 'class': _TEXTAREA_CLASS + ' font-mono text-sm', - }), - help_text=_( - '允许用户在申请时额外申请容量的磁盘列表,' - 'JSON 数组格式,如 ["C:", "D:"]' - ), - ) - - class Meta: - model = Product - fields = [ - 'display_name', 'display_description', - 'product_group', - 'host', 'is_available', 'auto_approval', 'visibility', - 'enable_host_protection', - 'display_hostname', 'rdp_port', - 'enable_disk_quota', - ] - widgets = { - 'display_name': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入产品显示名称', - }), - 'display_description': forms.Textarea(attrs={ - 'class': _TEXTAREA_CLASS, - 'rows': 3, - 'placeholder': '输入产品显示描述(可选)', - }), - 'product_group': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'host': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'is_available': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'auto_approval': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'visibility': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - 'enable_host_protection': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'display_hostname': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入显示地址', - }), - 'rdp_port': forms.NumberInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '3389', - }), - 'enable_disk_quota': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - } - labels = { - 'display_name': _('显示名称'), - 'display_description': _('显示描述'), - 'product_group': _('产品组'), - 'host': _('关联主机'), - 'is_available': _('是否可用'), - 'auto_approval': _('自动审核'), - 'visibility': _('可见性'), - 'enable_host_protection': _('启用主机保护(Gateway)'), - 'display_hostname': _('显示地址'), - 'rdp_port': _('RDP端口'), - 'enable_disk_quota': _('启用磁盘配额管理'), - } - help_texts = { - 'host': _('此产品运行所在的主机'), - 'is_available': _('是否在前端展示此产品'), - 'auto_approval': _('是否自动批准针对此产品的开户申请'), - 'visibility': _( - '公开对所有用户可见,' - '邀请访问仅对已授权用户可见' - ), - 'enable_host_protection': _( - '启用后,用户只能通过Gateway隧道访问RDP,' - '主机不暴露公网IP。需部署Gateway服务。' - ), - 'enable_disk_quota': _( - '是否启用磁盘配额管理,' - '启用后将自动为新用户设置磁盘配额' - ), - } - - def __init__(self, *args, **kwargs): - self.provider_user = kwargs.pop('provider_user', None) - super().__init__(*args, **kwargs) - - # 提供商只能选择自己可见的主机 - if self.provider_user: - from utils.provider import get_provider_hosts - self.fields['host'].queryset = get_provider_hosts( - self.provider_user - ).order_by('name') - - # 提供商只能选择自己创建的产品组 - self.fields['product_group'].queryset = ( - ProductGroup.objects.filter( - created_by=self.provider_user - ).order_by('name') - ) - - # 初始化 JSON 字段的显示值 - if self.instance and self.instance.pk: - if self.instance.default_disk_quota: - self.initial['default_disk_quota'] = json.dumps( - self.instance.default_disk_quota, - ensure_ascii=False, - indent=2, - ) - if self.instance.allow_extra_quota_disks: - self.initial['allow_extra_quota_disks'] = json.dumps( - self.instance.allow_extra_quota_disks, - ensure_ascii=False, - ) - - def clean_default_disk_quota(self): - data = self.cleaned_data.get('default_disk_quota', '') - if not data or not data.strip(): - return {} - if isinstance(data, dict): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('磁盘配额格式无效,请输入有效的 JSON') - if not isinstance(parsed, dict): - raise forms.ValidationError( - '磁盘配额必须为字典格式,如 {"C:": 10240}' - ) - for disk, value in parsed.items(): - try: - val = int(value) - if val < 0: - raise forms.ValidationError( - f'磁盘 {disk} 的配额不能为负数' - ) - except (ValueError, TypeError): - raise forms.ValidationError( - f'磁盘 {disk} 的配额必须为数字' - ) - return parsed - - def clean_allow_extra_quota_disks(self): - data = self.cleaned_data.get('allow_extra_quota_disks', '') - if not data or not data.strip(): - return [] - if isinstance(data, list): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError( - '磁盘列表格式无效,请输入有效的 JSON 数组' - ) - if not isinstance(parsed, list): - raise forms.ValidationError( - '磁盘列表必须为数组格式,如 ["C:", "D:"]' - ) - return parsed - - def save(self, commit=True): - instance = super().save(commit=False) - instance.default_disk_quota = self.cleaned_data.get( - 'default_disk_quota', {} - ) - instance.allow_extra_quota_disks = self.cleaned_data.get( - 'allow_extra_quota_disks', [] - ) - if commit: - instance.save() - return instance - - -# ========== 产品组管理表单 ========== - - -class ProductGroupForm(forms.ModelForm): - """ - 产品组管理表单 - - 提供商创建产品组时自动设置 created_by。 - """ - - class Meta: - model = ProductGroup - fields = [ - 'name', 'description', - 'display_order', 'is_active', 'visibility', - ] - widgets = { - 'name': forms.TextInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '输入产品组名称', - }), - 'description': forms.Textarea(attrs={ - 'class': _TEXTAREA_CLASS, - 'rows': 3, - 'placeholder': '输入产品组描述(可选)', - }), - 'display_order': forms.NumberInput(attrs={ - 'class': _INPUT_CLASS, - 'placeholder': '0', - 'min': '0', - }), - 'is_active': forms.CheckboxInput(attrs={ - 'class': _CHECKBOX_CLASS, - }), - 'visibility': forms.Select(attrs={ - 'class': _SELECT_CLASS, - }), - } - labels = { - 'name': _('产品组名称'), - 'description': _('描述'), - 'display_order': _('显示顺序'), - 'is_active': _('是否启用'), - 'visibility': _('可见性'), - } - help_texts = { - 'display_order': _( - '产品组在前端展示的顺序,数字越小越靠前' - ), - 'is_active': _('是否在前端展示此产品组'), - 'visibility': _( - '公开对所有用户可见,' - '邀请访问仅对已授权用户可见' - ), - } diff --git a/apps/operations/forms_wizard.py b/apps/operations/forms_wizard.py deleted file mode 100644 index f5f9fe5..0000000 --- a/apps/operations/forms_wizard.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -运营管理 - 产品向导式创建表单 - -分步引导超管创建产品,提供智能默认值和逐步验证。 -与 AdminProductForm 不同,此表单专注于创建流程的简化和引导。 -""" - -import json - -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import Product, ProductGroup - - -_INPUT_CLASS = ( - "w-full bg-white/5 backdrop-blur-xl border border-white/10 rounded-md " - "px-4 py-3 text-white placeholder-slate-500 " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 focus:border-cyan-500 transition" -) -_SELECT_CLASS = ( - "w-full bg-white/5 backdrop-blur-xl border border-white/10 rounded-md " - "px-4 py-3 text-white appearance-none " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 focus:border-cyan-500 " - "transition cursor-pointer" -) -_CHECKBOX_CLASS = ( - "w-5 h-5 rounded border-slate-700/50 bg-slate-900/50 " - "text-cyan-400 focus:ring-cyan-500 focus:ring-2 transition cursor-pointer accent-md-primary" -) -_TEXTAREA_CLASS = ( - "w-full bg-white/5 backdrop-blur-xl border border-white/10 rounded-md " - "px-4 py-3 text-white placeholder-slate-500 " - "focus:outline-none focus:ring-1 focus:ring-cyan-500/50 focus:border-cyan-500 " - "transition resize-y" -) - - -class ProductWizardForm(forms.ModelForm): - """ - 产品创建向导表单 - - 分为三步: - - Step 1: 基本信息 (display_name, display_description, product_group) - - Step 2: 主机关联与配置 (host, display_hostname, rdp_port, visibility, is_available, auto_approval) - - Step 3: 高级设置 (enable_host_protection, enable_disk_quota, default_disk_quota, allow_extra_quota_disks) - - 智能默认值: - - 选择主机后自动填充 display_hostname 和 rdp_port - - 可见性默认为公开 - """ - - default_disk_quota = forms.CharField( - label=_("默认磁盘配额"), - required=False, - widget=forms.Textarea( - attrs={ - "rows": 3, - "placeholder": '{"C:": 10240, "D:": 20480}', - "class": _TEXTAREA_CLASS + " font-mono text-sm", - "x-model": "defaultDiskQuota", - } - ), - help_text=_( - "每个磁盘的默认配额大小(MB)," 'JSON 格式,如 {"C:": 10240, "D:": 20480}' - ), - ) - - allow_extra_quota_disks = forms.CharField( - label=_("允许额外申请容量的磁盘"), - required=False, - widget=forms.Textarea( - attrs={ - "rows": 2, - "placeholder": '["C:", "D:"]', - "class": _TEXTAREA_CLASS + " font-mono text-sm", - "x-model": "allowExtraQuotaDisks", - } - ), - help_text=_( - "允许用户在申请时额外申请容量的磁盘列表," 'JSON 数组格式,如 ["C:", "D:"]' - ), - ) - - class Meta: - model = Product - fields = [ - "display_name", - "display_description", - "product_group", - "host", - "is_available", - "auto_approval", - "visibility", - "enable_host_protection", - "display_hostname", - "rdp_port", - "enable_disk_quota", - "limit_one_per_user", - ] - widgets = { - "display_name": forms.TextInput( - attrs={ - "class": _INPUT_CLASS, - "placeholder": "输入产品显示名称", - "x-model": "displayName", - "required": "", - } - ), - "display_description": forms.Textarea( - attrs={ - "class": _TEXTAREA_CLASS, - "rows": 3, - "placeholder": "输入产品显示描述(可选)", - "x-model": "displayDescription", - } - ), - "product_group": forms.Select( - attrs={ - "class": _SELECT_CLASS, - "x-model": "productGroup", - } - ), - "host": forms.Select( - attrs={ - "class": _SELECT_CLASS, - "x-model": "hostId", - "x-on:change": "onHostChange()", - } - ), - "is_available": forms.CheckboxInput( - attrs={ - "class": _CHECKBOX_CLASS, - "x-model": "isAvailable", - } - ), - "auto_approval": forms.CheckboxInput( - attrs={ - "class": _CHECKBOX_CLASS, - "x-model": "autoApproval", - } - ), - "visibility": forms.Select( - attrs={ - "class": _SELECT_CLASS, - "x-model": "visibility", - } - ), - "enable_host_protection": forms.CheckboxInput( - attrs={ - "class": _CHECKBOX_CLASS, - "x-model": "enableHostProtection", - } - ), - "display_hostname": forms.TextInput( - attrs={ - "class": _INPUT_CLASS, - "placeholder": "输入显示地址", - "x-model": "displayHostname", - "required": "", - } - ), - "rdp_port": forms.NumberInput( - attrs={ - "class": _INPUT_CLASS, - "placeholder": "3389", - "x-model.number": "rdpPort", - } - ), - "enable_disk_quota": forms.CheckboxInput( - attrs={ - "class": _CHECKBOX_CLASS, - "x-model": "enableDiskQuota", - } - ), - "limit_one_per_user": forms.CheckboxInput( - attrs={ - "class": _CHECKBOX_CLASS, - "x-model": "limitOnePerUser", - } - ), - } - labels = { - "display_name": _("显示名称"), - "display_description": _("显示描述"), - "product_group": _("产品组"), - "host": _("关联主机"), - "is_available": _("是否可用"), - "auto_approval": _("自动审核"), - "visibility": _("可见性"), - "enable_host_protection": _("启用主机保护(Gateway)"), - "display_hostname": _("显示地址"), - "rdp_port": _("RDP端口"), - "enable_disk_quota": _("启用磁盘配额管理"), - "limit_one_per_user": _("每人限购一个"), - } - help_texts = { - "host": _("此产品运行所在的主机"), - "is_available": _("是否在前端展示此产品"), - "auto_approval": _("是否自动批准针对此产品的开户申请"), - "visibility": _("公开对所有用户可见," "邀请访问仅对已授权用户可见"), - "enable_host_protection": _( - "启用后,用户只能通过Gateway隧道访问RDP," - "主机不暴露公网IP。需部署Gateway服务。" - ), - "enable_disk_quota": _( - "是否启用磁盘配额管理," "启用后将自动为新用户设置磁盘配额" - ), - "limit_one_per_user": _("启用后,每个用户只能拥有一个此产品"), - } - - def __init__(self, *args, user=None, site_group=None, **kwargs): - super().__init__(*args, **kwargs) - - from apps.hosts.models import Host - from utils.provider import get_provider_hosts - from django.db.models import Q - - if user and user.is_superuser: - host_qs = Host.objects.all() - pg_qs = ProductGroup.objects.all() - elif user and site_group and user.is_site_group_admin(site_group): - host_qs = Host.objects.filter(site_group=site_group) - pg_qs = ProductGroup.objects.filter(site_group=site_group) - elif user: - host_qs = get_provider_hosts(user, site_group=site_group) - pg_qs = ProductGroup.objects.filter(created_by=user) - else: - host_qs = Host.objects.all() - pg_qs = ProductGroup.objects.all() - - self.fields["host"].queryset = host_qs.order_by("name") - self.fields["product_group"].queryset = pg_qs.order_by("name") - - if not self.initial.get("rdp_port"): - self.initial["rdp_port"] = 3389 - if not self.initial.get("visibility"): - self.initial["visibility"] = "public" - - def get_hosts_info(self): - """ - 返回主机列表信息,用于向导第二步的智能填充。 - 包含主机ID、名称、地址、RDP端口等。 - """ - hosts = self.fields["host"].queryset - result = [] - for host in hosts: - result.append( - { - "id": host.pk, - "name": host.name, - "hostname": host.hostname, - "rdp_port": host.rdp_port or 3389, - "connection_type": host.connection_type, - "status": host.status, - } - ) - return result - - def clean_default_disk_quota(self): - data = self.cleaned_data.get("default_disk_quota", "") - if not data or not data.strip(): - return {} - if isinstance(data, dict): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError("磁盘配额格式无效,请输入有效的 JSON") - if not isinstance(parsed, dict): - raise forms.ValidationError('磁盘配额必须为字典格式,如 {"C:": 10240}') - for disk, value in parsed.items(): - try: - val = int(value) - if val < 0: - raise forms.ValidationError(f"磁盘 {disk} 的配额不能为负数") - except (ValueError, TypeError): - raise forms.ValidationError(f"磁盘 {disk} 的配额必须为数字") - return parsed - - def clean_allow_extra_quota_disks(self): - data = self.cleaned_data.get("allow_extra_quota_disks", "") - if not data or not data.strip(): - return [] - if isinstance(data, list): - return data - try: - parsed = json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError("磁盘列表格式无效,请输入有效的 JSON 数组") - if not isinstance(parsed, list): - raise forms.ValidationError('磁盘列表必须为数组格式,如 ["C:", "D:"]') - return parsed - - def save(self, commit=True): - instance = super().save(commit=False) - instance.default_disk_quota = self.cleaned_data.get("default_disk_quota", {}) - instance.allow_extra_quota_disks = self.cleaned_data.get( - "allow_extra_quota_disks", [] - ) - if commit: - instance.save() - return instance diff --git a/apps/operations/management/__init__.py b/apps/operations/management/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/operations/management/commands/__init__.py b/apps/operations/management/commands/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/operations/management/commands/create_public_host_info.py b/apps/operations/management/commands/create_public_host_info.py deleted file mode 100755 index 66f83e2..0000000 --- a/apps/operations/management/commands/create_public_host_info.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.core.management.base import BaseCommand -from apps.hosts.models import Host -from apps.operations.models import PublicHostInfo - - -class Command(BaseCommand): - help = '为现有主机创建公开主机信息' - - def add_arguments(self, parser): - parser.add_argument( - '--force', - action='store_true', - help='强制为所有主机创建公开信息,即使已存在', - ) - - def handle(self, *args, **options): - force = options['force'] - hosts = Host.objects.all() - created_count = 0 - skipped_count = 0 - - for host in hosts: - # 检查是否已存在对应的 PublicHostInfo - if not force and PublicHostInfo.objects.filter(internal_host=host).exists(): - self.stdout.write( - self.style.WARNING(f'Skipping {host.name} - PublicHostInfo already exists') - ) - skipped_count += 1 - continue - - # 创建 PublicHostInfo - public_info, created = PublicHostInfo.objects.get_or_create( - internal_host=host, - defaults={ - 'display_name': host.name, - 'display_description': host.description, - 'display_hostname': host.hostname, - 'display_rdp_port': host.rdp_port, - 'is_available': True, - } - ) - - if created: - created_count += 1 - self.stdout.write( - self.style.SUCCESS(f'Created PublicHostInfo for {host.name}') - ) - else: - # 如果记录已存在但在强制模式下,更新它 - if force: - public_info.display_name = host.name - public_info.display_description = host.description - public_info.display_hostname = host.hostname - public_info.display_rdp_port = host.rdp_port - public_info.is_available = True - public_info.save() - self.stdout.write( - self.style.WARNING(f'Updated PublicHostInfo for {host.name}') - ) - - self.stdout.write( - self.style.NOTICE( - f'Completed! Created: {created_count}, Skipped: {skipped_count}' - ) - ) \ No newline at end of file diff --git a/apps/operations/migrations/0001_initial.py b/apps/operations/migrations/0001_initial.py deleted file mode 100755 index e51756e..0000000 --- a/apps/operations/migrations/0001_initial.py +++ /dev/null @@ -1,164 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:24 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hosts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='AccountOpeningRequest', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('contact_email', models.EmailField(help_text='申请人联系方式', max_length=254, verbose_name='联系邮箱')), - ('contact_phone', models.CharField(blank=True, help_text='申请人联系电话', max_length=20, null=True, verbose_name='联系电话')), - ('username', models.CharField(help_text='希望在云电脑上创建的用户名', max_length=150, verbose_name='用户名')), - ('user_fullname', models.CharField(help_text='用户真实姓名', max_length=200, verbose_name='用户姓名')), - ('user_email', models.EmailField(help_text='用户邮箱地址', max_length=254, verbose_name='用户邮箱')), - ('user_description', models.TextField(blank=True, help_text='关于该用户的附加信息', verbose_name='用户描述')), - ('requested_password', models.CharField(blank=True, help_text='用户希望设置的初始密码,留空则系统生成', max_length=128, null=True, verbose_name='用户指定密码')), - ('status', models.CharField(choices=[('pending', '待审核'), ('approved', '已批准'), ('rejected', '已拒绝'), ('processing', '处理中'), ('completed', '已完成'), ('failed', '失败')], default='pending', help_text='开户申请的当前状态', max_length=20, verbose_name='申请状态')), - ('approval_date', models.DateTimeField(blank=True, help_text='申请被审核的时间', null=True, verbose_name='审核时间')), - ('approval_notes', models.TextField(blank=True, help_text='审核时的备注信息', verbose_name='审核备注')), - ('cloud_user_id', models.CharField(blank=True, help_text='在云电脑上实际创建的用户ID', max_length=255, verbose_name='云电脑用户ID')), - ('cloud_user_password', models.CharField(blank=True, help_text='为用户设置的初始密码', max_length=255, verbose_name='云电脑用户密码')), - ('result_message', models.TextField(blank=True, help_text='开户操作的结果信息', verbose_name='结果信息')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='申请创建时间', verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='申请信息最后更新时间', verbose_name='更新时间')), - ('applicant', models.ForeignKey(help_text='提交开户申请的用户', on_delete=django.db.models.deletion.CASCADE, related_name='account_opening_requests', to=settings.AUTH_USER_MODEL, verbose_name='申请人')), - ('approved_by', models.ForeignKey(blank=True, help_text='批准此申请的管理员', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_account_requests', to=settings.AUTH_USER_MODEL, verbose_name='审核人')), - ], - options={ - 'verbose_name': '开户申请', - 'verbose_name_plural': '开户申请', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='面向用户展示的产品名称', max_length=200, verbose_name='产品名称')), - ('description', models.TextField(blank=True, help_text='产品的详细描述,支持Markdown格式', verbose_name='产品描述')), - ('display_name', models.CharField(help_text='在前端展示的产品名称', max_length=200, verbose_name='显示名称')), - ('display_description', models.TextField(blank=True, help_text='在前端展示的产品描述,支持Markdown格式', verbose_name='显示描述')), - ('rdp_port', models.IntegerField(default=3389, help_text='用户连接时使用的RDP端口', verbose_name='RDP端口')), - ('display_hostname', models.CharField(help_text='在前端展示的产品访问地址', max_length=255, verbose_name='显示地址')), - ('is_available', models.BooleanField(default=True, help_text='是否在前端展示此产品', verbose_name='是否可用')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('host', models.ForeignKey(help_text='此产品运行所在的主机', on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='关联主机')), - ], - options={ - 'verbose_name': '产品', - 'verbose_name_plural': '产品', - }, - ), - migrations.CreateModel( - name='CloudComputerUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(help_text='在云电脑上的用户名', max_length=150, verbose_name='用户名')), - ('fullname', models.CharField(help_text='用户真实姓名', max_length=200, verbose_name='用户姓名')), - ('email', models.EmailField(help_text='用户邮箱地址', max_length=254, verbose_name='用户邮箱')), - ('description', models.TextField(blank=True, help_text='关于该用户的附加信息', verbose_name='用户描述')), - ('status', models.CharField(choices=[('active', '激活'), ('inactive', '未激活'), ('disabled', '已禁用'), ('deleted', '已删除')], default='active', help_text='用户在云电脑上的状态', max_length=20, verbose_name='用户状态')), - ('is_admin', models.BooleanField(default=False, help_text='是否具有管理员权限', verbose_name='管理员权限')), - ('groups', models.TextField(blank=True, help_text='用户所属的组(逗号分隔)', verbose_name='用户组')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='用户在云电脑上创建的时间', verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='信息最后更新时间', verbose_name='更新时间')), - ('created_from_request', models.ForeignKey(blank=True, help_text='创建此用户的开户申请', null=True, on_delete=django.db.models.deletion.SET_NULL, to='operations.accountopeningrequest', verbose_name='来源申请')), - ('product', models.ForeignKey(help_text='该用户所属的云电脑产品', on_delete=django.db.models.deletion.CASCADE, to='operations.product', verbose_name='所属产品')), - ], - options={ - 'verbose_name': '云电脑用户', - 'verbose_name_plural': '云电脑用户', - 'ordering': ['-created_at'], - }, - ), - migrations.AddField( - model_name='accountopeningrequest', - name='target_product', - field=models.ForeignKey(blank=True, help_text='要在哪个产品上创建用户', null=True, on_delete=django.db.models.deletion.CASCADE, to='operations.product', verbose_name='目标产品'), - ), - migrations.CreateModel( - name='SystemTask', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='任务的名称', max_length=200, verbose_name='任务名称')), - ('task_type', models.CharField(help_text='任务的类型,如batch_create_user等', max_length=100, verbose_name='任务类型')), - ('description', models.TextField(blank=True, help_text='任务的详细描述', null=True, verbose_name='任务描述')), - ('status', models.CharField(choices=[('pending', '等待中'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('cancelled', '已取消')], default='pending', help_text='任务的执行状态', max_length=20, verbose_name='任务状态')), - ('progress', models.IntegerField(default=0, help_text='任务执行进度,0-100', verbose_name='执行进度')), - ('result', models.TextField(blank=True, help_text='任务执行的结果信息', null=True, verbose_name='执行结果')), - ('error_message', models.TextField(blank=True, help_text='任务执行失败时的错误信息', null=True, verbose_name='错误信息')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='任务创建时间', verbose_name='创建时间')), - ('started_at', models.DateTimeField(blank=True, help_text='任务开始执行的时间', null=True, verbose_name='开始时间')), - ('completed_at', models.DateTimeField(blank=True, help_text='任务完成的时间', null=True, verbose_name='完成时间')), - ('created_by', models.ForeignKey(blank=True, help_text='创建该任务的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tasks', to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ], - options={ - 'verbose_name': '系统任务', - 'verbose_name_plural': '系统任务', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['status'], name='operations__status_4c55d9_idx'), models.Index(fields=['task_type'], name='operations__task_ty_240239_idx'), models.Index(fields=['created_at'], name='operations__created_b8e18a_idx')], - }, - ), - migrations.AddIndex( - model_name='product', - index=models.Index(fields=['is_available'], name='operations__is_avai_fe927c_idx'), - ), - migrations.AddIndex( - model_name='product', - index=models.Index(fields=['host'], name='operations__host_id_e9faab_idx'), - ), - migrations.AddIndex( - model_name='product', - index=models.Index(fields=['created_at'], name='operations__created_8529c5_idx'), - ), - migrations.AddIndex( - model_name='cloudcomputeruser', - index=models.Index(fields=['product'], name='operations__product_684d9f_idx'), - ), - migrations.AddIndex( - model_name='cloudcomputeruser', - index=models.Index(fields=['username'], name='operations__usernam_fe9d86_idx'), - ), - migrations.AddIndex( - model_name='cloudcomputeruser', - index=models.Index(fields=['status'], name='operations__status_27a9ba_idx'), - ), - migrations.AddIndex( - model_name='cloudcomputeruser', - index=models.Index(fields=['created_at'], name='operations__created_98027d_idx'), - ), - migrations.AlterUniqueTogether( - name='cloudcomputeruser', - unique_together={('product', 'username')}, - ), - migrations.AddIndex( - model_name='accountopeningrequest', - index=models.Index(fields=['applicant'], name='operations__applica_1b5fd9_idx'), - ), - migrations.AddIndex( - model_name='accountopeningrequest', - index=models.Index(fields=['status'], name='operations__status_c941d7_idx'), - ), - migrations.AddIndex( - model_name='accountopeningrequest', - index=models.Index(fields=['target_product'], name='operations__target__a69322_idx'), - ), - migrations.AddIndex( - model_name='accountopeningrequest', - index=models.Index(fields=['created_at'], name='operations__created_9035ca_idx'), - ), - ] diff --git a/apps/operations/migrations/0002_publichostinfo.py b/apps/operations/migrations/0002_publichostinfo.py deleted file mode 100755 index 81a70a0..0000000 --- a/apps/operations/migrations/0002_publichostinfo.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-26 03:39 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('hosts', '0002_alter_hostgroup_hosts'), - ('operations', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='PublicHostInfo', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('display_name', models.CharField(help_text='在前端展示的主机名称', max_length=200, verbose_name='显示名称')), - ('display_description', models.TextField(blank=True, help_text='在前端展示的主机描述,支持Markdown格式', verbose_name='显示描述')), - ('display_hostname', models.CharField(help_text='在前端展示的主机地址', max_length=255, verbose_name='显示地址')), - ('display_rdp_port', models.IntegerField(default=3389, help_text='在前端展示的RDP端口', verbose_name='显示RDP端口')), - ('is_available', models.BooleanField(default=True, help_text='是否在前端展示此主机', verbose_name='是否可用')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('internal_host', models.OneToOneField(help_text='关联的内部主机', on_delete=django.db.models.deletion.CASCADE, to='hosts.host', verbose_name='内部主机')), - ], - options={ - 'verbose_name': '公开主机信息', - 'verbose_name_plural': '公开主机信息', - 'indexes': [models.Index(fields=['is_available'], name='operations__is_avai_9e6881_idx'), models.Index(fields=['internal_host'], name='operations__interna_acf3ff_idx')], - }, - ), - ] diff --git a/apps/operations/migrations/0003_manual_fix.py b/apps/operations/migrations/0003_manual_fix.py deleted file mode 100755 index 180817f..0000000 --- a/apps/operations/migrations/0003_manual_fix.py +++ /dev/null @@ -1,15 +0,0 @@ -# 数据库结构修复迁移 -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0002_publichostinfo'), - ] - - operations = [ - # 我们不删除不存在的字段,而是什么都不做 - # 这样可以标记迁移为已应用,避免错误 - migrations.RunSQL('SELECT 1;', reverse_sql='SELECT 1;'), - ] diff --git a/apps/operations/migrations/0004_remove_accountopeningrequest_requested_password_and_more.py b/apps/operations/migrations/0004_remove_accountopeningrequest_requested_password_and_more.py deleted file mode 100755 index 82e02d0..0000000 --- a/apps/operations/migrations/0004_remove_accountopeningrequest_requested_password_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 11:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0003_manual_fix'), - ] - - operations = [ - migrations.RemoveField( - model_name='accountopeningrequest', - name='requested_password', - ), - migrations.AddField( - model_name='cloudcomputeruser', - name='initial_password', - field=models.CharField(blank=True, help_text='用户的初始密码,查看后将被清除', max_length=255, verbose_name='初始密码'), - ), - migrations.AddField( - model_name='cloudcomputeruser', - name='password_viewed', - field=models.BooleanField(default=False, help_text='指示初始密码是否已被查看', verbose_name='密码已查看'), - ), - migrations.AddField( - model_name='cloudcomputeruser', - name='password_viewed_at', - field=models.DateTimeField(blank=True, help_text='初始密码被查看的时间', null=True, verbose_name='密码查看时间'), - ), - migrations.AddField( - model_name='product', - name='auto_approval', - field=models.BooleanField(default=False, help_text='是否自动批准针对此产品的开户申请', verbose_name='自动审核'), - ), - ] diff --git a/apps/operations/migrations/0005_cloudcomputeruser_owner.py b/apps/operations/migrations/0005_cloudcomputeruser_owner.py deleted file mode 100644 index c1c8be4..0000000 --- a/apps/operations/migrations/0005_cloudcomputeruser_owner.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 15:15 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('operations', '0004_remove_accountopeningrequest_requested_password_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='cloudcomputeruser', - name='owner', - field=models.ForeignKey(blank=True, help_text='拥有此云电脑账户的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cloud_users', to=settings.AUTH_USER_MODEL, verbose_name='所有者'), - ), - ] diff --git a/apps/operations/migrations/0006_add_product_created_by.py b/apps/operations/migrations/0006_add_product_created_by.py deleted file mode 100644 index c5ac101..0000000 --- a/apps/operations/migrations/0006_add_product_created_by.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-06 16:32 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("operations", "0005_cloudcomputeruser_owner"), - ] - - operations = [ - migrations.AddField( - model_name="product", - name="created_by", - field=models.ForeignKey( - blank=True, - help_text="创建此产品的用户", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="created_products", - to=settings.AUTH_USER_MODEL, - verbose_name="创建者", - ), - ), - migrations.AddIndex( - model_name="product", - index=models.Index( - fields=["created_by"], name="operations__created_4da3a5_idx" - ), - ), - ] diff --git a/apps/operations/migrations/0007_add_product_group.py b/apps/operations/migrations/0007_add_product_group.py deleted file mode 100644 index 88e60a1..0000000 --- a/apps/operations/migrations/0007_add_product_group.py +++ /dev/null @@ -1,117 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-08 10:20 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("operations", "0006_add_product_created_by"), - ] - - operations = [ - migrations.CreateModel( - name="ProductGroup", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField( - help_text="产品组的名称", - max_length=200, - verbose_name="产品组名称", - ), - ), - ( - "description", - models.TextField( - blank=True, - help_text="产品组的详细描述,支持Markdown格式", - verbose_name="产品组描述", - ), - ), - ( - "display_order", - models.IntegerField( - default=0, - help_text="产品组在前端展示的顺序,数字越小越靠前", - verbose_name="显示顺序", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="是否在前端展示此产品组", - verbose_name="是否启用", - ), - ), - ( - "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), - ), - ( - "updated_at", - models.DateTimeField(auto_now=True, verbose_name="更新时间"), - ), - ], - options={ - "verbose_name": "产品组", - "verbose_name_plural": "产品组", - "ordering": ["display_order", "name"], - }, - ), - migrations.AddField( - model_name="productgroup", - name="auto_assign_providers", - field=models.ManyToManyField( - blank=True, - help_text="这些提供商创建的产品将自动加入此产品组", - related_name="auto_product_groups", - to=settings.AUTH_USER_MODEL, - verbose_name="自动分配提供商", - ), - ), - migrations.AddField( - model_name="product", - name="product_group", - field=models.ForeignKey( - blank=True, - help_text="产品所属的产品组", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="products", - to="operations.productgroup", - verbose_name="产品组", - ), - ), - migrations.AddIndex( - model_name="product", - index=models.Index( - fields=["product_group"], name="operations__product_981a27_idx" - ), - ), - migrations.AddIndex( - model_name="productgroup", - index=models.Index( - fields=["is_active"], name="operations__is_acti_e01f52_idx" - ), - ), - migrations.AddIndex( - model_name="productgroup", - index=models.Index( - fields=["display_order"], name="operations__display_2722d2_idx" - ), - ), - ] diff --git a/apps/operations/migrations/0008_accountopeningrequest_requested_disk_capacity_and_more.py b/apps/operations/migrations/0008_accountopeningrequest_requested_disk_capacity_and_more.py deleted file mode 100644 index 73bcf16..0000000 --- a/apps/operations/migrations/0008_accountopeningrequest_requested_disk_capacity_and_more.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-17 13:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("operations", "0007_add_product_group"), - ] - - operations = [ - migrations.AddField( - model_name="accountopeningrequest", - name="requested_disk_capacity", - field=models.JSONField( - blank=True, - default=dict, - help_text='用户额外申请的磁盘容量(MB),如 {"C:": 20480, "D:": 40960}', - verbose_name="需求磁盘容量", - ), - ), - migrations.AddField( - model_name="cloudcomputeruser", - name="disk_quota", - field=models.JSONField( - blank=True, - default=dict, - help_text='用户的磁盘配额配置(MB),如 {"C:": 10240, "D:": 20480}', - verbose_name="磁盘配额", - ), - ), - migrations.AddField( - model_name="product", - name="allow_extra_quota_disks", - field=models.JSONField( - blank=True, - default=list, - help_text='允许用户在申请时额外申请容量的磁盘列表,如 ["C:", "D:"]', - verbose_name="允许额外申请容量的磁盘", - ), - ), - migrations.AddField( - model_name="product", - name="default_disk_quota", - field=models.JSONField( - blank=True, - default=dict, - help_text='每个磁盘的默认配额大小(MB),如 {"C:": 10240, "D:": 20480}', - verbose_name="默认磁盘配额", - ), - ), - migrations.AddField( - model_name="product", - name="enable_disk_quota", - field=models.BooleanField( - default=False, - help_text="是否启用磁盘配额管理,启用后将自动为新用户设置磁盘配额", - verbose_name="启用磁盘配额管理", - ), - ), - ] diff --git a/apps/operations/migrations/0009_rdpdomainroute.py b/apps/operations/migrations/0009_rdpdomainroute.py deleted file mode 100644 index 98185a7..0000000 --- a/apps/operations/migrations/0009_rdpdomainroute.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('operations', '0008_accountopeningrequest_requested_disk_capacity_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='RdpDomainRoute', - fields=[ - ('id', models.BigAutoField( - auto_created=True, primary_key=True, - serialize=False, verbose_name='ID' - )), - ('domain', models.CharField( - max_length=255, unique=True, - verbose_name='RDP域名', - help_text='分配给用户的临时RDP访问域名' - )), - ('tunnel_token', models.CharField( - max_length=64, verbose_name='隧道Token', - help_text='关联主机的隧道Token' - )), - ('is_active', models.BooleanField( - default=True, verbose_name='是否有效', - help_text='域名是否仍然有效' - )), - ('expires_at', models.DateTimeField( - verbose_name='过期时间', - help_text='域名过期时间,10分钟无连接后过期' - )), - ('last_activity_at', models.DateTimeField( - auto_now=True, verbose_name='最后活动时间', - help_text='最后一次RDP连接活动时间' - )), - ('created_at', models.DateTimeField( - auto_now_add=True, verbose_name='创建时间' - )), - ('assigned_to', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - verbose_name='分配用户', - help_text='被分配此RDP域名的用户' - )), - ('product', models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='operations.product', - verbose_name='关联产品', - help_text='此域名关联的云电脑产品' - )), - ], - options={ - 'verbose_name': 'RDP域名路由', - 'verbose_name_plural': 'RDP域名路由', - 'ordering': ['-created_at'], - }, - ), - migrations.AddIndex( - model_name='rdpdomainroute', - index=models.Index(fields=['domain'], name='operations_rdp_domain_idx'), - ), - migrations.AddIndex( - model_name='rdpdomainroute', - index=models.Index(fields=['is_active'], name='operations_rdp_active_idx'), - ), - migrations.AddIndex( - model_name='rdpdomainroute', - index=models.Index(fields=['assigned_to'], name='operations_rdp_user_idx'), - ), - migrations.AddIndex( - model_name='rdpdomainroute', - index=models.Index(fields=['expires_at'], name='operations_rdp_expires_idx'), - ), - migrations.AddIndex( - model_name='rdpdomainroute', - index=models.Index(fields=['product'], name='operations_rdp_product_idx'), - ), - ] diff --git a/apps/operations/migrations/0010_product_enable_host_protection.py b/apps/operations/migrations/0010_product_enable_host_protection.py deleted file mode 100644 index 11c3f28..0000000 --- a/apps/operations/migrations/0010_product_enable_host_protection.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0009_rdpdomainroute'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='enable_host_protection', - field=models.BooleanField( - default=False, - help_text=( - '启用后,用户只能通过Gateway隧道访问该产品的RDP,' - '主机不暴露公网IP。需要部署Gateway服务。' - ), - verbose_name='启用主机保护(通过Gateway)' - ), - ), - ] diff --git a/apps/operations/migrations/0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more.py b/apps/operations/migrations/0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more.py deleted file mode 100644 index bc49a89..0000000 --- a/apps/operations/migrations/0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-25 12:35 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0010_product_enable_host_protection'), - ] - - operations = [ - migrations.RenameIndex( - model_name='rdpdomainroute', - new_name='operations__domain_5c01f5_idx', - old_name='operations_rdp_domain_idx', - ), - migrations.RenameIndex( - model_name='rdpdomainroute', - new_name='operations__is_acti_ba9cb3_idx', - old_name='operations_rdp_active_idx', - ), - migrations.RenameIndex( - model_name='rdpdomainroute', - new_name='operations__assigne_ecc986_idx', - old_name='operations_rdp_user_idx', - ), - migrations.RenameIndex( - model_name='rdpdomainroute', - new_name='operations__expires_8306ea_idx', - old_name='operations_rdp_expires_idx', - ), - migrations.RenameIndex( - model_name='rdpdomainroute', - new_name='operations__product_ccd619_idx', - old_name='operations_rdp_product_idx', - ), - ] diff --git a/apps/operations/migrations/0012_product_visibility_productgroup_visibility_and_more.py b/apps/operations/migrations/0012_product_visibility_productgroup_visibility_and_more.py deleted file mode 100644 index 179aa57..0000000 --- a/apps/operations/migrations/0012_product_visibility_productgroup_visibility_and_more.py +++ /dev/null @@ -1,111 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-29 10:10 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('operations', '0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='visibility', - field=models.CharField(choices=[('public', '公开'), ('invite_only', '邀请访问')], default='public', help_text='产品的可见性:公开对所有用户可见,邀请访问仅对已授权用户可见', max_length=20, verbose_name='可见性'), - ), - migrations.AddField( - model_name='productgroup', - name='visibility', - field=models.CharField(choices=[('public', '公开'), ('invite_only', '邀请访问')], default='public', help_text='产品组的可见性:公开对所有用户可见,邀请访问仅对已授权用户可见', max_length=20, verbose_name='可见性'), - ), - migrations.CreateModel( - name='ProductInvitationToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(help_text='用于邀请链接的唯一令牌', max_length=64, unique=True, verbose_name='邀请令牌')), - ('max_uses', models.IntegerField(default=0, help_text='令牌最大可使用次数,0表示无限制', verbose_name='最大使用次数')), - ('used_count', models.IntegerField(default=0, help_text='令牌已被使用的次数', verbose_name='已使用次数')), - ('expires_at', models.DateTimeField(blank=True, help_text='令牌过期时间,留空表示永不过期', null=True, verbose_name='过期时间')), - ('is_active', models.BooleanField(default=True, help_text='令牌是否有效', verbose_name='是否启用')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('created_by', models.ForeignKey(help_text='创建此邀请令牌的用户', on_delete=django.db.models.deletion.CASCADE, related_name='created_invitation_tokens', to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ('product', models.ForeignKey(blank=True, help_text='此令牌关联的产品(与产品组至少选一个)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitation_tokens', to='operations.product', verbose_name='关联产品')), - ('product_group', models.ForeignKey(blank=True, help_text='此令牌关联的产品组(与产品至少选一个)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitation_tokens', to='operations.productgroup', verbose_name='关联产品组')), - ], - options={ - 'verbose_name': '产品邀请令牌', - 'verbose_name_plural': '产品邀请令牌', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='ProductAccessGrant', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), - ('expires_at', models.DateTimeField(blank=True, help_text='访问权限过期时间,留空表示永久有效', null=True, verbose_name='授权过期时间')), - ('is_revoked', models.BooleanField(default=False, help_text='权限是否已被撤销', verbose_name='是否已撤销')), - ('revoked_at', models.DateTimeField(blank=True, null=True, verbose_name='撤销时间')), - ('granted_by_token', models.ForeignKey(blank=True, help_text='通过哪个邀请令牌获得的权限', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='access_grants', to='operations.productinvitationtoken', verbose_name='来源邀请令牌')), - ('product', models.ForeignKey(blank=True, help_text='授权访问的产品', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_grants', to='operations.product', verbose_name='关联产品')), - ('product_group', models.ForeignKey(blank=True, help_text='授权访问的产品组', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_grants', to='operations.productgroup', verbose_name='关联产品组')), - ('revoked_by', models.ForeignKey(blank=True, help_text='撤销此权限的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='revoked_access_grants', to=settings.AUTH_USER_MODEL, verbose_name='撤销人')), - ('user', models.ForeignKey(help_text='获得访问权限的用户', on_delete=django.db.models.deletion.CASCADE, related_name='product_access_grants', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '产品访问授权', - 'verbose_name_plural': '产品访问授权', - 'ordering': ['-granted_at'], - }, - ), - migrations.AddIndex( - model_name='productinvitationtoken', - index=models.Index(fields=['token'], name='operations__token_43deaf_idx'), - ), - migrations.AddIndex( - model_name='productinvitationtoken', - index=models.Index(fields=['is_active'], name='operations__is_acti_8900c8_idx'), - ), - migrations.AddIndex( - model_name='productinvitationtoken', - index=models.Index(fields=['expires_at'], name='operations__expires_b456ac_idx'), - ), - migrations.AddIndex( - model_name='productinvitationtoken', - index=models.Index(fields=['created_by'], name='operations__created_a0e391_idx'), - ), - migrations.AddIndex( - model_name='productaccessgrant', - index=models.Index(fields=['user'], name='operations__user_id_2e640b_idx'), - ), - migrations.AddIndex( - model_name='productaccessgrant', - index=models.Index(fields=['product'], name='operations__product_f577f6_idx'), - ), - migrations.AddIndex( - model_name='productaccessgrant', - index=models.Index(fields=['product_group'], name='operations__product_691717_idx'), - ), - migrations.AddIndex( - model_name='productaccessgrant', - index=models.Index(fields=['is_revoked'], name='operations__is_revo_0fe57f_idx'), - ), - migrations.AddIndex( - model_name='productaccessgrant', - index=models.Index(fields=['granted_at'], name='operations__granted_d3fe5b_idx'), - ), - migrations.AddConstraint( - model_name='productaccessgrant', - constraint=models.UniqueConstraint(condition=models.Q(('product__isnull', False)), fields=('user', 'product'), name='unique_user_product_grant'), - ), - migrations.AddConstraint( - model_name='productaccessgrant', - constraint=models.UniqueConstraint(condition=models.Q(('product_group__isnull', False)), fields=('user', 'product_group'), name='unique_user_productgroup_grant'), - ), - ] diff --git a/apps/operations/migrations/0013_productgroup_created_by.py b/apps/operations/migrations/0013_productgroup_created_by.py deleted file mode 100644 index ea51bb4..0000000 --- a/apps/operations/migrations/0013_productgroup_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-29 10:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('operations', '0012_product_visibility_productgroup_visibility_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='productgroup', - name='created_by', - field=models.ForeignKey(blank=True, help_text='创建此产品组的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_product_groups', to=settings.AUTH_USER_MODEL, verbose_name='创建者'), - ), - ] diff --git a/apps/operations/migrations/0014_encrypt_initial_password.py b/apps/operations/migrations/0014_encrypt_initial_password.py deleted file mode 100644 index d54220f..0000000 --- a/apps/operations/migrations/0014_encrypt_initial_password.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.db import migrations, models -import hashlib -import base64 - - -def _get_fernet(): - from django.conf import settings - from cryptography.fernet import Fernet - key = hashlib.sha256(settings.SECRET_KEY.encode()).digest() - return Fernet(base64.urlsafe_b64encode(key)) - - -def encrypt_existing_passwords(apps, schema_editor): - CloudComputerUser = apps.get_model('operations', 'CloudComputerUser') - fernet = _get_fernet() - for user in CloudComputerUser.objects.filter( - _initial_password__isnull=False - ).exclude(_initial_password=''): - try: - fernet.decrypt(user._initial_password.encode()) - except Exception: - encrypted = fernet.encrypt(user._initial_password.encode()).decode() - CloudComputerUser.objects.filter(pk=user.pk).update( - _initial_password=encrypted - ) - - -def decrypt_passwords_back(apps, schema_editor): - CloudComputerUser = apps.get_model('operations', 'CloudComputerUser') - fernet = _get_fernet() - for user in CloudComputerUser.objects.filter( - _initial_password__isnull=False - ).exclude(_initial_password=''): - try: - decrypted = fernet.decrypt(user._initial_password.encode()).decode() - CloudComputerUser.objects.filter(pk=user.pk).update( - _initial_password=decrypted - ) - except Exception: - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0013_productgroup_created_by'), - ] - - operations = [ - migrations.RemoveField( - model_name='cloudcomputeruser', - name='initial_password', - ), - migrations.AddField( - model_name='cloudcomputeruser', - name='_initial_password', - field=models.CharField(blank=True, db_column='initial_password', help_text='用户的初始密码(加密存储),查看后将被清除', max_length=512, verbose_name='初始密码(加密)'), - ), - migrations.RunPython(encrypt_existing_passwords, decrypt_passwords_back), - ] diff --git a/apps/operations/migrations/0015_alter_rdpdomainroute_domain_and_more.py b/apps/operations/migrations/0015_alter_rdpdomainroute_domain_and_more.py deleted file mode 100644 index 6145bf1..0000000 --- a/apps/operations/migrations/0015_alter_rdpdomainroute_domain_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.30 on 2026-05-21 16:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0014_encrypt_initial_password'), - ] - - operations = [ - migrations.AlterField( - model_name='rdpdomainroute', - name='domain', - field=models.CharField(help_text='分配给用户的临时RDP访问域名(仅用于显示/追踪,不再用于SNI路由)', max_length=255, unique=True, verbose_name='RDP域名'), - ), - migrations.AlterField( - model_name='rdpdomainroute', - name='tunnel_token', - field=models.CharField(blank=True, help_text='关联主机的隧道Token,用于RD Gateway路由', max_length=64, verbose_name='隧道Token'), - ), - ] diff --git a/apps/operations/migrations/0016_add_product_limit_one_per_user.py b/apps/operations/migrations/0016_add_product_limit_one_per_user.py deleted file mode 100644 index 0d687d5..0000000 --- a/apps/operations/migrations/0016_add_product_limit_one_per_user.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0015_alter_rdpdomainroute_domain_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='product', - name='limit_one_per_user', - field=models.BooleanField(default=False, help_text='是否限制每个用户只能拥有一个此产品', verbose_name='每人限购一个'), - ), - ] diff --git a/apps/operations/migrations/0017_accountopeningrequest_retry_count.py b/apps/operations/migrations/0017_accountopeningrequest_retry_count.py deleted file mode 100644 index 0c7fc19..0000000 --- a/apps/operations/migrations/0017_accountopeningrequest_retry_count.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 04:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("operations", "0016_add_product_limit_one_per_user"), - ] - - operations = [ - migrations.AddField( - model_name="accountopeningrequest", - name="retry_count", - field=models.IntegerField( - default=0, help_text="管理员重试开户的次数", verbose_name="重试次数" - ), - ), - ] diff --git a/apps/operations/migrations/0018_product_site_group_productgroup_site_group.py b/apps/operations/migrations/0018_product_site_group_productgroup_site_group.py deleted file mode 100644 index 8b36e96..0000000 --- a/apps/operations/migrations/0018_product_site_group_productgroup_site_group.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-31 13:27 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0014_sitegroup_sitegrouphostname_and_more"), - ("operations", "0017_accountopeningrequest_retry_count"), - ] - - operations = [ - migrations.AddField( - model_name="product", - name="site_group", - field=models.ForeignKey( - blank=True, - help_text="该产品所属的站点组,留空表示默认组", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="products", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - migrations.AddField( - model_name="productgroup", - name="site_group", - field=models.ForeignKey( - blank=True, - help_text="该产品组所属的站点组,留空表示默认组", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="product_groups", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ] diff --git a/apps/operations/migrations/0019_alter_product_site_group_and_more.py b/apps/operations/migrations/0019_alter_product_site_group_and_more.py deleted file mode 100644 index 7d53b9a..0000000 --- a/apps/operations/migrations/0019_alter_product_site_group_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-01 13:09 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("dashboard", "0015_remove_sitegroup_dashboard_s_slug_5cb7f5_idx_and_more"), - ("operations", "0018_product_site_group_productgroup_site_group"), - ] - - operations = [ - migrations.AlterField( - model_name="product", - name="site_group", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="products", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - migrations.AlterField( - model_name="productgroup", - name="site_group", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="product_groups", - to="dashboard.sitegroup", - verbose_name="所属站点组", - ), - ), - ] diff --git a/apps/operations/migrations/__init__.py b/apps/operations/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/operations/models.py b/apps/operations/models.py deleted file mode 100755 index ecb9256..0000000 --- a/apps/operations/models.py +++ /dev/null @@ -1,1572 +0,0 @@ -""" -操作记录模型 -""" -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.contrib.auth import get_user_model -from django.conf import settings -from django.utils import timezone -from django.dispatch import Signal -from utils.crypto import encrypt_value, decrypt_value -import logging - -User = get_user_model() - -logger = logging.getLogger(__name__) - - -# 定义开户申请提交前的信号 -account_opening_request_pre_submit = Signal() -# 定义开户申请提交后的信号 -account_opening_request_post_submit = Signal() - - -class PublicHostInfo(models.Model): - """ - 公开主机信息模型 - - 用于在前端展示主机信息,而不暴露敏感信息 - """ - # 内部主机关联 - internal_host = models.OneToOneField( - 'hosts.Host', - on_delete=models.CASCADE, - verbose_name=_('内部主机'), - help_text=_('关联的内部主机') - ) - - # 显示信息 - display_name = models.CharField( - max_length=200, - verbose_name=_('显示名称'), - help_text=_('在前端展示的主机名称') - ) - display_description = models.TextField( - blank=True, - verbose_name=_('显示描述'), - help_text=_('在前端展示的主机描述,支持Markdown格式') - ) - - # 连接信息(对外公开的部分) - display_hostname = models.CharField( - max_length=255, - verbose_name=_('显示地址'), - help_text=_('在前端展示的主机地址') - ) - display_rdp_port = models.IntegerField( - default=3389, - verbose_name=_('显示RDP端口'), - help_text=_('在前端展示的RDP端口') - ) - - # 可用性 - is_available = models.BooleanField( - default=True, - verbose_name=_('是否可用'), - help_text=_('是否在前端展示此主机') - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('公开主机信息') - verbose_name_plural = _('公开主机信息') - indexes = [ - models.Index(fields=['is_available']), - models.Index(fields=['internal_host']), - ] - - def __str__(self): - return self.display_name - - -class SystemTask(models.Model): - """ - 系统任务模型 - - 记录系统中的异步任务,如批量操作、定时任务等 - """ - # 任务信息 - name = models.CharField( - max_length=200, - verbose_name=_('任务名称'), - help_text=_('任务的名称') - ) - task_type = models.CharField( - max_length=100, - verbose_name=_('任务类型'), - help_text=_('任务的类型,如batch_create_user等') - ) - description = models.TextField( - blank=True, - null=True, - verbose_name=_('任务描述'), - help_text=_('任务的详细描述') - ) - - # 执行信息 - status = models.CharField( - max_length=20, - choices=[ - ('pending', _('等待中')), - ('running', _('执行中')), - ('success', _('成功')), - ('failed', _('失败')), - ('cancelled', _('已取消')), - ], - default='pending', - verbose_name=_('任务状态'), - help_text=_('任务的执行状态') - ) - progress = models.IntegerField( - default=0, - verbose_name=_('执行进度'), - help_text=_('任务执行进度,0-100') - ) - result = models.TextField( - blank=True, - null=True, - verbose_name=_('执行结果'), - help_text=_('任务执行的结果信息') - ) - error_message = models.TextField( - blank=True, - null=True, - verbose_name=_('错误信息'), - help_text=_('任务执行失败时的错误信息') - ) - - # 关联信息 - created_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='created_tasks', - verbose_name=_('创建者'), - help_text=_('创建该任务的用户') - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间'), - help_text=_('任务创建时间') - ) - started_at = models.DateTimeField( - blank=True, - null=True, - verbose_name=_('开始时间'), - help_text=_('任务开始执行的时间') - ) - completed_at = models.DateTimeField( - blank=True, - null=True, - verbose_name=_('完成时间'), - help_text=_('任务完成的时间') - ) - - class Meta: - verbose_name = _('系统任务') - verbose_name_plural = _('系统任务') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['status']), - models.Index(fields=['task_type']), - models.Index(fields=['created_at']), - ] - - def __str__(self): - """返回任务名称""" - return self.name - - def update_progress(self, progress): - """ - 更新任务进度 - - Args: - progress: 进度值,0-100 - """ - self.progress = min(max(progress, 0), 100) - self.save(update_fields=['progress']) - - def start(self): - """开始执行任务""" - from django.utils import timezone - self.status = 'running' - self.started_at = timezone.now() - self.save(update_fields=['status', 'started_at']) - - def complete(self, result=None): - """ - 完成任务 - - Args: - result: 执行结果 - """ - from django.utils import timezone - self.status = 'success' - self.completed_at = timezone.now() - self.progress = 100 - if result: - self.result = result - self.save(update_fields=['status', 'completed_at', 'progress', 'result']) - - def fail(self, error_message): - """ - 任务失败 - - Args: - error_message: 错误信息 - """ - from django.utils import timezone - self.status = 'failed' - self.completed_at = timezone.now() - self.error_message = error_message - self.save(update_fields=['status', 'completed_at', 'error_message']) - - def cancel(self): - """取消任务""" - from django.utils import timezone - self.status = 'cancelled' - self.completed_at = timezone.now() - self.save(update_fields=['status', 'completed_at']) - - -class ProductGroup(models.Model): - """ - 产品组模型 - - 用于对产品进行分组管理 - """ - name = models.CharField( - max_length=200, - verbose_name=_('产品组名称'), - help_text=_('产品组的名称') - ) - description = models.TextField( - blank=True, - verbose_name=_('产品组描述'), - help_text=_('产品组的详细描述,支持Markdown格式') - ) - display_order = models.IntegerField( - default=0, - verbose_name=_('显示顺序'), - help_text=_('产品组在前端展示的顺序,数字越小越靠前') - ) - is_active = models.BooleanField( - default=True, - verbose_name=_('是否启用'), - help_text=_('是否在前端展示此产品组') - ) - visibility = models.CharField( - max_length=20, - choices=[ - ('public', _('公开')), - ('invite_only', _('邀请访问')), - ], - default='public', - verbose_name=_('可见性'), - help_text=_('产品组的可见性:公开对所有用户可见,邀请访问仅对已授权用户可见') - ) - auto_assign_providers = models.ManyToManyField( - User, - blank=True, - related_name='auto_product_groups', - verbose_name=_('自动分配提供商'), - help_text=_('这些提供商创建的产品将自动加入此产品组') - ) - - site_group = models.ForeignKey( - 'dashboard.SiteGroup', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='product_groups', - verbose_name=_('所属站点组'), - ) - - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='created_product_groups', - verbose_name=_('创建者'), - help_text=_('创建此产品组的用户') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('产品组') - verbose_name_plural = _('产品组') - ordering = ['display_order', 'name'] - indexes = [ - models.Index(fields=['is_active']), - models.Index(fields=['display_order']), - ] - - def __str__(self): - return self.name - - -class Product(models.Model): - """ - 产品模型 - - 代表面向用户的产品,一个主机可以对应多个产品 - """ - name = models.CharField( - max_length=200, - verbose_name=_('产品名称'), - help_text=_('面向用户展示的产品名称') - ) - description = models.TextField( - blank=True, - verbose_name=_('产品描述'), - help_text=_('产品的详细描述,支持Markdown格式') - ) - display_name = models.CharField( - max_length=200, - verbose_name=_('显示名称'), - help_text=_('在前端展示的产品名称') - ) - display_description = models.TextField( - blank=True, - verbose_name=_('显示描述'), - help_text=_('在前端展示的产品描述,支持Markdown格式') - ) - - product_group = models.ForeignKey( - ProductGroup, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='products', - verbose_name=_('产品组'), - help_text=_('产品所属的产品组') - ) - - # 关联主机 - host = models.ForeignKey( - 'hosts.Host', - on_delete=models.CASCADE, - verbose_name=_('关联主机'), - help_text=_('此产品运行所在的主机') - ) - - site_group = models.ForeignKey( - 'dashboard.SiteGroup', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='products', - verbose_name=_('所属站点组'), - ) - - # 产品配置 - rdp_port = models.IntegerField( - default=3389, - verbose_name=_('RDP端口'), - help_text=_('用户连接时使用的RDP端口') - ) - display_hostname = models.CharField( - max_length=255, - verbose_name=_('显示地址'), - help_text=_('在前端展示的产品访问地址') - ) - - # 产品状态 - is_available = models.BooleanField( - default=True, - verbose_name=_('是否可用'), - help_text=_('是否在前端展示此产品') - ) - auto_approval = models.BooleanField( - default=False, - verbose_name=_('自动审核'), - help_text=_('是否自动批准针对此产品的开户申请') - ) - visibility = models.CharField( - max_length=20, - choices=[ - ('public', _('公开')), - ('invite_only', _('邀请访问')), - ], - default='public', - verbose_name=_('可见性'), - help_text=_('产品的可见性:公开对所有用户可见,邀请访问仅对已授权用户可见') - ) - - limit_one_per_user = models.BooleanField( - default=False, - verbose_name=_('每人限购一个'), - help_text=_('是否限制每个用户只能拥有一个此产品') - ) - - enable_disk_quota = models.BooleanField( - default=False, - verbose_name=_('启用磁盘配额管理'), - help_text=_('是否启用磁盘配额管理,启用后将自动为新用户设置磁盘配额') - ) - enable_host_protection = models.BooleanField( - default=False, - verbose_name=_('启用主机保护(通过Gateway)'), - help_text=_( - '启用后,用户只能通过Gateway隧道访问该产品的RDP,' - '主机不暴露公网IP。需要部署Gateway服务。' - ) - ) - default_disk_quota = models.JSONField( - default=dict, - blank=True, - verbose_name=_('默认磁盘配额'), - help_text=_('每个磁盘的默认配额大小(MB),如 {"C:": 10240, "D:": 20480}') - ) - allow_extra_quota_disks = models.JSONField( - default=list, - blank=True, - verbose_name=_('允许额外申请容量的磁盘'), - help_text=_('允许用户在申请时额外申请容量的磁盘列表,如 ["C:", "D:"]') - ) - - # 创建者 - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name=_('创建者'), - help_text=_('创建此产品的用户'), - related_name='created_products' - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('产品') - verbose_name_plural = _('产品') - indexes = [ - models.Index(fields=['is_available']), - models.Index(fields=['host']), - models.Index(fields=['created_at']), - models.Index(fields=['created_by']), - models.Index(fields=['product_group'], name='operations__product_981a27_idx'), - ] - - def __str__(self): - return self.display_name - - @property - def status(self): - """ - 产品状态,继承自主机状态 - """ - return self.host.status - - @property - def hostname(self): - """ - 产品主机名,使用显示地址 - """ - return self.display_hostname - - -class AccountOpeningRequest(models.Model): - """ - 用户开户申请模型 - - 用于记录用户提交的开户申请信息 - """ - # 申请人信息 - applicant = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='account_opening_requests', - verbose_name=_('申请人'), - help_text=_('提交开户申请的用户') - ) - contact_email = models.EmailField( - verbose_name=_('联系邮箱'), - help_text=_('申请人联系方式') - ) - contact_phone = models.CharField( - max_length=20, - blank=True, - null=True, - verbose_name=_('联系电话'), - help_text=_('申请人联系电话') - ) - - # 开户信息 - username = models.CharField( - max_length=150, - verbose_name=_('用户名'), - help_text=_('希望在云电脑上创建的用户名') - ) - user_fullname = models.CharField( - max_length=200, - verbose_name=_('用户姓名'), - help_text=_('用户真实姓名') - ) - user_email = models.EmailField( - verbose_name=_('用户邮箱'), - help_text=_('用户邮箱地址') - ) - user_description = models.TextField( - blank=True, - verbose_name=_('用户描述'), - help_text=_('关于该用户的附加信息') - ) - - # 目标产品(替代原来的target_host) - target_product = models.ForeignKey( - Product, - on_delete=models.CASCADE, - verbose_name=_('目标产品'), - help_text=_('要在哪个产品上创建用户'), - null=True, - blank=True - ) - - requested_disk_capacity = models.JSONField( - default=dict, - blank=True, - verbose_name=_('需求磁盘容量'), - help_text=_('用户额外申请的磁盘容量(MB),如 {"C:": 20480, "D:": 40960}') - ) - - # 审核信息 - status = models.CharField( - max_length=20, - choices=[ - ('pending', _('待审核')), - ('approved', _('已批准')), - ('rejected', _('已拒绝')), - ('processing', _('处理中')), - ('completed', _('已完成')), - ('failed', _('失败')), - ], - default='pending', - verbose_name=_('申请状态'), - help_text=_('开户申请的当前状态') - ) - approved_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='approved_account_requests', - verbose_name=_('审核人'), - help_text=_('批准此申请的管理员') - ) - approval_date = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('审核时间'), - help_text=_('申请被审核的时间') - ) - approval_notes = models.TextField( - blank=True, - verbose_name=_('审核备注'), - help_text=_('审核时的备注信息') - ) - - # 结果信息 - cloud_user_id = models.CharField( - max_length=255, - blank=True, - verbose_name=_('云电脑用户ID'), - help_text=_('在云电脑上实际创建的用户ID') - ) - cloud_user_password = models.CharField( - max_length=255, - blank=True, - verbose_name=_('云电脑用户密码'), - help_text=_('为用户设置的初始密码') - ) - result_message = models.TextField( - blank=True, - verbose_name=_('结果信息'), - help_text=_('开户操作的结果信息') - ) - retry_count = models.IntegerField( - default=0, - verbose_name=_('重试次数'), - help_text=_('管理员重试开户的次数') - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间'), - help_text=_('申请创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间'), - help_text=_('申请信息最后更新时间') - ) - - class Meta: - verbose_name = _('开户申请') - verbose_name_plural = _('开户申请') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['applicant']), - models.Index(fields=['status']), - models.Index(fields=['target_product']), - models.Index(fields=['created_at']), - ] - - def __str__(self): - product_name = self.target_product.display_name if self.target_product else 'Unknown Product' - return f'{self.username} - {product_name}' - - def approve(self, approver, notes=''): - """ - 批准开户申请 - - Args: - approver: 批准申请的管理员 - notes: 审核备注 - """ - self.status = 'approved' - self.approved_by = approver - self.approval_date = timezone.now() - self.approval_notes = notes - # 不直接调用save,而是通过super().save()让重写的save方法处理后续操作 - super().save() - - # 状态从pending变为approved时,通过Celery异步处理用户创建 - # (save()中的逻辑也会检测此条件并派发任务,但approve()绕过了save(), - # 所以需要在此处显式派发) - try: - from apps.operations.tasks import process_account_creation - process_account_creation.delay(self.pk) - except Exception as e: - logger.error( - f"Failed to dispatch account creation task " - f"for request {self.pk}: {str(e)}" - ) - - def reject(self, approver, notes=''): - """ - 拒绝开户申请 - - Args: - approver: 拒绝申请的管理员 - notes: 审核备注 - """ - self.status = 'rejected' - self.approved_by = approver - self.approval_date = timezone.now() - self.approval_notes = notes - self.save() - - def start_processing(self): - """ - 开始处理开户申请 - """ - self.status = 'processing' - self.save() - - def complete(self, cloud_user_id, cloud_user_password, result_message=''): - """ - 完成开户申请 - - Args: - cloud_user_id: 在云电脑上创建的用户ID - cloud_user_password: 用户初始密码(出于安全考虑,不会存储) - result_message: 结果信息 - """ - self.status = 'completed' - self.cloud_user_id = cloud_user_id - # 出于安全考虑,不存储用户密码明文 - # self.cloud_user_password = cloud_user_password - self.result_message = result_message - self.save() - - def fail(self, result_message=''): - """ - 开户申请失败 - - Args: - result_message: 失败原因 - """ - self.status = 'failed' - self.result_message = result_message - self.save() - - def retry(self, operator=None): - """ - 重试失败的开户申请 - - 将状态从 failed 重置为 approved, - 清除上次的错误信息,并重新触发开户流程。 - - Args: - operator: 执行重试操作的管理员 - - Returns: - bool: 重试是否成功启动 - """ - if self.status != 'failed': - return False - - self.status = 'approved' - self.result_message = '' - self.retry_count += 1 - if operator: - self.approval_notes = ( - f'重试(第{self.retry_count}次) - ' - f'操作人: {operator.username}' - ) - self.save(update_fields=[ - 'status', 'result_message', 'retry_count', - 'approval_notes', - ]) - - try: - from apps.operations.tasks import process_account_creation - process_account_creation.delay(self.pk) - logger.info( - f"Retry dispatched for request {self.pk}, " - f"attempt #{self.retry_count}" - ) - return True - except Exception as e: - logger.error( - f"Failed to dispatch retry task for " - f"request {self.pk}: {str(e)}" - ) - self.status = 'failed' - self.result_message = '重试失败,请联系管理员了解详情' - self.save(update_fields=['status', 'result_message']) - return False - - def save(self, *args, **kwargs): - """ - 重写save方法,当状态变为'approved'时自动处理用户创建 - """ - is_new_instance = not self.pk - - if is_new_instance: - logger.info(f"AccountOpeningRequest.save(): 发送 pre-submit 信号,实例ID: {self.pk}, 目标产品: {getattr(self.target_product, 'id', 'None')}, 联系邮箱: {self.contact_email}") - account_opening_request_pre_submit.send(sender=self.__class__, instance=self) - logger.info(f"AccountOpeningRequest.save(): pre-submit 信号发送完成,实例状态: {self.status}") - - old_status = None - if self.pk: - try: - old_status = AccountOpeningRequest.objects.filter(pk=self.pk).values_list('status', flat=True).first() - except Exception: - pass - - auto_approved = False - if (is_new_instance and self.target_product and - self.target_product.auto_approval and self.status == 'pending'): - self.status = 'approved' - from django.contrib.auth import get_user_model - from typing import cast - User = get_user_model() - system_user = User.objects.filter(is_superuser=True).first() - if system_user: - self.approved_by = cast(User, system_user) - self.approval_date = timezone.now() - self.approval_notes = '自动审核通过' - auto_approved = True - - super().save(*args, **kwargs) - - if is_new_instance: - logger.info(f"AccountOpeningRequest.save(): 发送 post-submit 信号,实例ID: {self.pk}, 最终状态: {self.status}") - account_opening_request_post_submit.send(sender=self.__class__, instance=self) - - if ((old_status == 'pending' and self.status == 'approved') or - (is_new_instance and auto_approved and self.status == 'approved')): - try: - from apps.operations.tasks import process_account_creation - process_account_creation.delay(self.pk) - except Exception as e: - logger.error(f"Failed to dispatch account creation task for request {self.pk}: {str(e)}") - - def auto_process_creation(self): - """审批通过后自动创建用户""" - from django.db import transaction - import os - - product = self.target_product - if not product: - logger.error(f"AccountOpeningRequest {self.id} has no target_product") - return - - host = product.host - - user_disk_quota = {} - if product.enable_disk_quota and product.default_disk_quota: - user_disk_quota = dict(product.default_disk_quota) - if self.requested_disk_capacity: - for disk, capacity in self.requested_disk_capacity.items(): - if disk in product.allow_extra_quota_disks: - user_disk_quota[disk] = capacity - - # DEMO模式 - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f'DEMO模式: 模拟创建用户 {self.username}') - password = CloudComputerUser.generate_complex_password() - - with transaction.atomic(): - self.status = 'completed' - self.result_message = f"用户 {self.username} 已在DEMO模式下创建(模拟)" - self.save(update_fields=['status', 'result_message']) - - CloudComputerUser.objects.get_or_create( - username=self.username, - product=self.target_product, - defaults={ - 'fullname': self.user_fullname, - 'email': self.user_email, - 'description': self.user_description, - 'created_from_request': self, - 'owner': self.applicant, - 'initial_password': password, - 'disk_quota': user_disk_quota, - } - ) - return - - # 正式模式 - try: - client = host.get_connection_client() - - password = CloudComputerUser.generate_complex_password() - result = client.create_user( - username=self.username, - password=password, - description=self.user_description - ) - - if result.status_code == 0: - if user_disk_quota: - try: - from utils.disk_quota import set_user_disk_quotas - quota_result = set_user_disk_quotas( - client, self.username, user_disk_quota - ) - if not quota_result['success']: - logger.warning( - f"用户 {self.username} 磁盘配额设置部分失败: " - f"{quota_result.get('errors', [])}" - ) - except Exception as e: - logger.error( - f"用户 {self.username} 磁盘配额设置失败: {str(e)}" - ) - - # 远程成功后,事务写本地 - with transaction.atomic(): - self.status = 'completed' - self.result_message = f"用户 {self.username} 已成功创建" - self.save(update_fields=['status', 'result_message']) - - CloudComputerUser.objects.get_or_create( - username=self.username, - product=self.target_product, - defaults={ - 'fullname': self.user_fullname, - 'email': self.user_email, - 'description': self.user_description, - 'created_from_request': self, - 'owner': self.applicant, - 'initial_password': password, - 'disk_quota': user_disk_quota, - } - ) - else: - error_msg = result.std_err or '未知错误' - self.status = 'failed' - self.result_message = f"创建用户失败: {error_msg}" - self.save(update_fields=['status', 'result_message']) - - except Exception as e: - self.status = 'failed' - self.result_message = f"处理异常: {str(e)}" - self.save(update_fields=['status', 'result_message']) - - -class CloudComputerUser(models.Model): - """ - 云电脑用户模型 - - 记录在各个云电脑产品上创建的用户信息 - """ - # 用户信息 - username = models.CharField( - max_length=150, - verbose_name=_('用户名'), - help_text=_('在云电脑上的用户名') - ) - fullname = models.CharField( - max_length=200, - verbose_name=_('用户姓名'), - help_text=_('用户真实姓名') - ) - email = models.EmailField( - verbose_name=_('用户邮箱'), - help_text=_('用户邮箱地址') - ) - description = models.TextField( - blank=True, - verbose_name=_('用户描述'), - help_text=_('关于该用户的附加信息') - ) - - # 关联的产品(替代原来的host) - product = models.ForeignKey( - Product, - on_delete=models.CASCADE, - verbose_name=_('所属产品'), - help_text=_('该用户所属的云电脑产品') - ) - - # 状态信息 - status = models.CharField( - max_length=20, - choices=[ - ('active', _('激活')), - ('inactive', _('未激活')), - ('disabled', _('已禁用')), - ('deleted', _('已删除')), - ], - default='active', - verbose_name=_('用户状态'), - help_text=_('用户在云电脑上的状态') - ) - - # 权限信息 - is_admin = models.BooleanField( - default=False, - verbose_name=_('管理员权限'), - help_text=_('是否具有管理员权限') - ) - groups = models.TextField( - blank=True, - verbose_name=_('用户组'), - help_text=_('用户所属的组(逗号分隔)') - ) - - disk_quota = models.JSONField( - default=dict, - blank=True, - verbose_name=_('磁盘配额'), - help_text=_('用户的磁盘配额配置(MB),如 {"C:": 10240, "D:": 20480}') - ) - - # 创建信息 - created_from_request = models.ForeignKey( - AccountOpeningRequest, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name=_('来源申请'), - help_text=_('创建此用户的开户申请') - ) - owner = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='cloud_users', - verbose_name=_('所有者'), - help_text=_('拥有此云电脑账户的用户') - ) - - # 密码信息(临时存储) - _initial_password = models.CharField( - max_length=512, - blank=True, - db_column='initial_password', - verbose_name=_('初始密码(加密)'), - help_text=_('用户的初始密码(加密存储),查看后将被清除') - ) - - @property - def initial_password(self): - if not self._initial_password: - return '' - try: - return decrypt_value(self._initial_password) - except ValueError: - raise ValueError("密码解密失败,数据可能已损坏或密钥已变更") - - @initial_password.setter - def initial_password(self, value): - if value: - self._initial_password = encrypt_value(value) - else: - self._initial_password = '' - password_viewed = models.BooleanField( - default=False, - verbose_name=_('密码已查看'), - help_text=_('指示初始密码是否已被查看') - ) - password_viewed_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('密码查看时间'), - help_text=_('初始密码被查看的时间') - ) - - # 时间信息 - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间'), - help_text=_('用户在云电脑上创建的时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间'), - help_text=_('信息最后更新时间') - ) - - class Meta: - verbose_name = _('云电脑用户') - verbose_name_plural = _('云电脑用户') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['product']), - models.Index(fields=['username']), - models.Index(fields=['status']), - models.Index(fields=['created_at']), - ] - unique_together = [['product', 'username']] # 确保同一产品上用户名唯一 - - def __str__(self): - return f'{self.username}@{self.product.display_name}' - - def activate(self): - """ - 激活用户 - """ - self.status = 'active' - self.save(update_fields=['status', 'updated_at']) - - def deactivate(self): - """ - 禁用用户 - """ - self.status = 'inactive' - self.save(update_fields=['status', 'updated_at']) - - def disable(self): - """ - 删除用户 - """ - self.status = 'disabled' - self.save(update_fields=['status', 'updated_at']) - - def delete_user(self): - """ - 标记用户为已删除 - """ - self.status = 'deleted' - self.save(update_fields=['status', 'updated_at']) - - def save(self, *args, **kwargs): - """ - 重写save方法,当状态改变时通过Celery异步执行远程操作 - """ - old_status = None - if self.pk: - try: - old_status = CloudComputerUser.objects.filter(pk=self.pk).values_list('status', flat=True).first() - except Exception: - pass - - super().save(*args, **kwargs) - - if old_status is not None: - remote_action = None - if old_status != 'disabled' and self.status == 'disabled': - remote_action = 'disable' - elif old_status == 'disabled' and self.status == 'active': - remote_action = 'enable' - elif old_status != 'deleted' and self.status == 'deleted': - remote_action = 'delete' - - if remote_action: - try: - from apps.operations.tasks import execute_cloud_user_remote_action - execute_cloud_user_remote_action.delay(self.pk, remote_action) - except Exception as e: - logger.error(f"Failed to dispatch remote action '{remote_action}' for user {self.username}: {str(e)}") - - def disable_remote_user(self): - import os - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f'DEMO模式: 模拟禁用用户 {self.username} 在产品 {self.product.display_name}') - return - - try: - product = self.product - host = product.host - client = host.get_connection_client() - - result = client.disabled_user(self.username) - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - logger.error(f"Failed to disable user {self.username} on host {host.name}: {error_msg}") - except Exception as e: - logger.error(f"Error disabling user {self.username} on host {host.name}: {str(e)}") - - def enable_remote_user(self): - import os - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f'DEMO模式: 模拟启用用户 {self.username} 在产品 {self.product.display_name}') - return - - try: - product = self.product - host = product.host - client = host.get_connection_client() - - result = client.enable_user(self.username) - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - logger.error(f"Failed to enable user {self.username} on host {host.name}: {error_msg}") - except Exception as e: - logger.error(f"Error enabling user {self.username} on host {host.name}: {str(e)}") - - def delete_remote_user(self): - import os - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f'DEMO模式: 模拟删除用户 {self.username} 在产品 {self.product.display_name}') - return - - try: - product = self.product - host = product.host - client = host.get_connection_client() - - result = client.delete_user(self.username) - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - logger.error(f"Failed to delete user {self.username} on host {host.name}: {error_msg}") - except Exception as e: - logger.error(f"Error deleting user {self.username} on host {host.name}: {str(e)}") - - def get_and_burn_password(self): - from django.utils import timezone - - if self.password_viewed: - raise Exception('密码已被查看,无法再次获取。如需重置请联系管理员。') - - if not self._initial_password: - raise Exception('密码不存在') - - password = self.initial_password - self.password_viewed = True - self.password_viewed_at = timezone.now() - self.initial_password = '' - self.save(update_fields=['password_viewed', 'password_viewed_at', '_initial_password']) - return password - - def reset_windows_password(self, new_password): - import os - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f'DEMO模式: 模拟重置用户 {self.username} 的密码') - return - - try: - product = self.product - host = product.host - client = host.get_connection_client() - - result = client.reset_password(self.username, new_password) - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - logger.error(f"Failed to reset password for user {self.username} on host {host.name}: {error_msg}") - raise Exception(f"远程重置密码失败: {error_msg}") - except Exception as e: - logger.error(f"Error resetting password for user {self.username} on host {host.name}: {str(e)}") - raise - - def reset_and_get_new_password(self): - """重置密码并返回新密码(用于阅后即焚流程)""" - from django.utils import timezone - - new_password = self.generate_complex_password() - self.reset_windows_password(new_password) - - self._initial_password = '' - self.initial_password = new_password - self.password_viewed = False - self.password_viewed_at = None - self.save(update_fields=['password_viewed', 'password_viewed_at', '_initial_password']) - - password = self.get_and_burn_password() - return password - - @staticmethod - def generate_complex_password(length=16): - """ - 生成复杂密码 - - Args: - length: 密码长度,默认为16位 - - Returns: - 生成的复杂密码 - """ - import secrets - import string - - # 包含大写字母、小写字母、数字和特殊字符 - # 排除 PowerShell 双引号字符串中有歧义的字符: " $ ` - _special = '!@#%^&*()_+-=[]{}|;:,.<>?' - alphabet = string.ascii_letters + string.digits + _special - - # 确保至少包含每种类型的字符 - while True: - password = ''.join(secrets.choice(alphabet) for i in range(length)) - - # 检查是否包含所需类型的字符 - has_upper = any(c.isupper() for c in password) - has_lower = any(c.islower() for c in password) - has_digit = any(c.isdigit() for c in password) - has_special = any(c in _special for c in password) - - if has_upper and has_lower and has_digit and has_special: - return password - - -class RdpDomainRoute(models.Model): - """ - RDP域名路由(SNI路由已弃用,现使用RD Gateway + tunnel_token路由) - domain字段仅用于显示和追踪,不再用于SNI路由 - """ - domain = models.CharField( - max_length=255, unique=True, - verbose_name=_('RDP域名'), - help_text=_('分配给用户的临时RDP访问域名(仅用于显示/追踪,不再用于SNI路由)') - ) - product = models.ForeignKey( - Product, - on_delete=models.CASCADE, - verbose_name=_('关联产品'), - help_text=_('此域名关联的云电脑产品') - ) - assigned_to = models.ForeignKey( - User, - on_delete=models.CASCADE, - verbose_name=_('分配用户'), - help_text=_('被分配此RDP域名的用户') - ) - tunnel_token = models.CharField( - max_length=64, blank=True, - verbose_name=_('隧道Token'), - help_text=_('关联主机的隧道Token,用于RD Gateway路由') - ) - is_active = models.BooleanField( - default=True, - verbose_name=_('是否有效'), - help_text=_('域名是否仍然有效') - ) - expires_at = models.DateTimeField( - verbose_name=_('过期时间'), - help_text=_('域名过期时间,10分钟无连接后过期') - ) - last_activity_at = models.DateTimeField( - auto_now=True, - verbose_name=_('最后活动时间'), - help_text=_('最后一次RDP连接活动时间') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - - class Meta: - verbose_name = _('RDP域名路由') - verbose_name_plural = _('RDP域名路由') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['domain']), - models.Index(fields=['is_active']), - models.Index(fields=['assigned_to']), - models.Index(fields=['expires_at']), - models.Index(fields=['product']), - ] - - def __str__(self): - status = '有效' if self.is_active else '已过期' - return f'{self.domain} -> {self.product.display_name} ({status})' - - @staticmethod - def generate_domain(): - import secrets - import string - prefix = ''.join( - secrets.choice(string.ascii_lowercase + string.digits) - for _ in range(8) - ) - from django.conf import settings as django_settings - base_domain = getattr( - django_settings, 'RDP_DOMAIN', '2c2a.com' - ) - return f'rdp-{prefix}.{base_domain}' - - def is_expired(self): - return timezone.now() > self.expires_at - - def deactivate(self): - self.is_active = False - self.save(update_fields=['is_active']) - - @property - def is_protected(self): - return self.product.enable_host_protection - - -class ProductInvitationToken(models.Model): - """ - 产品邀请令牌模型 - - 用于生成邀请链接,用户访问链接后可解锁指定产品或产品组的访问权限 - """ - token = models.CharField( - max_length=64, - unique=True, - verbose_name=_('邀请令牌'), - help_text=_('用于邀请链接的唯一令牌') - ) - product = models.ForeignKey( - Product, - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='invitation_tokens', - verbose_name=_('关联产品'), - help_text=_('此令牌关联的产品(与产品组至少选一个)') - ) - product_group = models.ForeignKey( - ProductGroup, - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='invitation_tokens', - verbose_name=_('关联产品组'), - help_text=_('此令牌关联的产品组(与产品至少选一个)') - ) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='created_invitation_tokens', - verbose_name=_('创建者'), - help_text=_('创建此邀请令牌的用户') - ) - max_uses = models.IntegerField( - default=0, - verbose_name=_('最大使用次数'), - help_text=_('令牌最大可使用次数,0表示无限制') - ) - used_count = models.IntegerField( - default=0, - verbose_name=_('已使用次数'), - help_text=_('令牌已被使用的次数') - ) - expires_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('过期时间'), - help_text=_('令牌过期时间,留空表示永不过期') - ) - is_active = models.BooleanField( - default=True, - verbose_name=_('是否启用'), - help_text=_('令牌是否有效') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('产品邀请令牌') - verbose_name_plural = _('产品邀请令牌') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['token']), - models.Index(fields=['is_active']), - models.Index(fields=['expires_at']), - models.Index(fields=['created_by']), - ] - - def __str__(self): - target = self.product.display_name if self.product else (self.product_group.name if self.product_group else _('未知')) - return f'{self.token[:8]}... -> {target}' - - def is_expired(self): - if self.expires_at and timezone.now() > self.expires_at: - return True - return False - - def is_exhausted(self): - if self.max_uses > 0 and self.used_count >= self.max_uses: - return True - return False - - def is_valid(self): - return self.is_active and not self.is_expired() and not self.is_exhausted() - - def increment_usage(self): - from django.db.models import F - self.used_count = F('used_count') + 1 - self.save(update_fields=['used_count', 'updated_at']) - self.refresh_from_db() - - def generate_token(self): - import secrets - import string - return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32)) - - def save(self, *args, **kwargs): - if not self.token: - self.token = self.generate_token() - super().save(*args, **kwargs) - - -class ProductAccessGrant(models.Model): - """ - 产品访问授权记录模型 - - 记录用户通过邀请链接获得的产品或产品组访问权限 - """ - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='product_access_grants', - verbose_name=_('用户'), - help_text=_('获得访问权限的用户') - ) - product = models.ForeignKey( - Product, - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='access_grants', - verbose_name=_('关联产品'), - help_text=_('授权访问的产品') - ) - product_group = models.ForeignKey( - ProductGroup, - on_delete=models.CASCADE, - null=True, - blank=True, - related_name='access_grants', - verbose_name=_('关联产品组'), - help_text=_('授权访问的产品组') - ) - granted_by_token = models.ForeignKey( - ProductInvitationToken, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='access_grants', - verbose_name=_('来源邀请令牌'), - help_text=_('通过哪个邀请令牌获得的权限') - ) - granted_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('授权时间') - ) - expires_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('授权过期时间'), - help_text=_('访问权限过期时间,留空表示永久有效') - ) - is_revoked = models.BooleanField( - default=False, - verbose_name=_('是否已撤销'), - help_text=_('权限是否已被撤销') - ) - revoked_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('撤销时间') - ) - revoked_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='revoked_access_grants', - verbose_name=_('撤销人'), - help_text=_('撤销此权限的用户') - ) - - class Meta: - verbose_name = _('产品访问授权') - verbose_name_plural = _('产品访问授权') - ordering = ['-granted_at'] - indexes = [ - models.Index(fields=['user']), - models.Index(fields=['product']), - models.Index(fields=['product_group']), - models.Index(fields=['is_revoked']), - models.Index(fields=['granted_at']), - ] - constraints = [ - models.UniqueConstraint( - fields=['user', 'product'], - condition=models.Q(product__isnull=False), - name='unique_user_product_grant' - ), - models.UniqueConstraint( - fields=['user', 'product_group'], - condition=models.Q(product_group__isnull=False), - name='unique_user_productgroup_grant' - ), - ] - - def __str__(self): - target = self.product.display_name if self.product else (self.product_group.name if self.product_group else _('未知')) - status = _('已撤销') if self.is_revoked else (_('已过期') if self.is_expired() else _('有效')) - return f'{self.user.username} -> {target} ({status})' - - def is_expired(self): - if self.expires_at and timezone.now() > self.expires_at: - return True - return False - - def is_effective(self): - return not self.is_revoked and not self.is_expired() diff --git a/apps/operations/services.py b/apps/operations/services.py deleted file mode 100644 index eeb2297..0000000 --- a/apps/operations/services.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -业务逻辑服务层 -将复杂的业务逻辑从业务视图和Admin中抽离出来,提供统一的服务接口 -""" -import logging -from django.db import transaction -from .models import CloudComputerUser - -logger = logging.getLogger(__name__) - - -def execute_account_opening(account_request): - """ - 执行开户操作:通过 WinRM 在目标主机上创建用户 - - Args: - account_request: AccountOpeningRequest 实例 - - Raises: - Exception: 连接或执行失败时抛出 - """ - with transaction.atomic(): - try: - # 记录开始处理 - logger.info(f"开始处理开户申请: {account_request.username}") - account_request.start_processing() - - # 系统生成强密码 - password = CloudComputerUser.generate_complex_password() - - # 连接到目标主机 - host = account_request.target_product.host - client = host.get_connection_client() - - # 执行远程用户创建 - result = client.create_user(account_request.username, password) - - if result.status_code == 0: - # 计算用户磁盘配额 - user_disk_quota = {} - product = account_request.target_product - if product.enable_disk_quota and product.default_disk_quota: - user_disk_quota = dict(product.default_disk_quota) - if account_request.requested_disk_capacity: - for disk, capacity in account_request.requested_disk_capacity.items(): - if disk in product.allow_extra_quota_disks: - user_disk_quota[disk] = capacity - - # 设置磁盘配额 - if user_disk_quota: - try: - from utils.disk_quota import set_user_disk_quotas - quota_result = set_user_disk_quotas( - client, account_request.username, user_disk_quota - ) - if not quota_result['success']: - logger.warning( - f"磁盘配额设置部分失败: " - f"{quota_result.get('errors', [])}" - ) - except Exception as e: - logger.error(f"磁盘配额设置失败: {str(e)}") - - # 成功创建用户 - cloud_user, created = CloudComputerUser.objects.get_or_create( - username=account_request.username, - product=account_request.target_product, - defaults={ - 'fullname': account_request.user_fullname, - 'email': account_request.user_email, - 'description': account_request.user_description, - 'created_from_request': account_request, - 'initial_password': password, - 'disk_quota': user_disk_quota, - } - ) - - # 更新申请状态 - account_request.complete( - cloud_user_id=account_request.username, - cloud_user_password='', - result_message=f"用户 {account_request.username} 已成功创建" - ) - - logger.info(f"开户申请处理成功: {account_request.username}") - return True - else: - # 创建用户失败 - error_msg = result.std_err if result.std_err else '未知错误' - account_request.fail("开户处理失败,请联系管理员了解详情") - logger.error(f"开户申请处理失败: {account_request.username}, 错误: {error_msg}") - raise Exception(f"创建用户失败: {error_msg}") - - except Exception as e: - # 处理过程中的任何异常 - error_msg = str(e) - account_request.fail("开户处理失败,请联系管理员了解详情") - logger.error(f"开户申请处理异常: {account_request.username}, 异常: {error_msg}") - raise - - -def update_user_admin_permission(cloud_user, make_admin): - """ - 更新用户的管理员权限 - - Args: - cloud_user: CloudComputerUser 实例 - make_admin: bool, True表示授予管理员权限,False表示撤销 - - Raises: - Exception: 权限操作失败时抛出 - """ - try: - # 连接到产品关联的主机 - product = cloud_user.product - host = product.host - client = host.get_connection_client() - - if make_admin: - # 授予管理员权限 - success = client.op_user(cloud_user.username) - if not success: - raise Exception(f"为用户 {cloud_user.username} 授予管理员权限失败") - else: - # 剥夺管理员权限 - success = client.deop_user(cloud_user.username) - if not success: - raise Exception(f"撤销用户 {cloud_user.username} 的管理员权限失败") - - logger.info(f"{'授予' if make_admin else '撤销'}用户 {cloud_user.username} 管理员权限成功") - return True - - except Exception as e: - logger.error(f"更新用户管理员权限失败: {cloud_user.username}, 错误: {str(e)}") - raise - - -def get_user_password_and_burn(cloud_user): - """ - 获取用户密码并销毁(阅后即焚) - - Args: - cloud_user: CloudComputerUser 实例 - - Returns: - str: 用户密码 - - Raises: - Exception: 获取密码失败时抛出 - """ - try: - password = cloud_user.get_and_burn_password() - logger.info(f"用户 {cloud_user.username} 成功获取并销毁初始密码") - return password - except Exception as e: - logger.error(f"获取用户 {cloud_user.username} 密码失败: {str(e)}") - raise - - -def toggle_user_status(cloud_user, action): - """ - 切换用户状态 - - Args: - cloud_user: CloudComputerUser 实例 - action: str, 操作类型 ('activate', 'deactivate', 'disable', 'delete') - - Returns: - bool: 操作是否成功 - - Raises: - Exception: 状态切换失败时抛出 - """ - try: - if action == 'activate': - cloud_user.activate() - elif action == 'deactivate': - cloud_user.deactivate() - elif action == 'disable': - cloud_user.disable() - elif action == 'delete': - cloud_user.delete_user() - else: - raise ValueError(f"无效的操作类型: {action}") - - logger.info(f"用户 {cloud_user.username} 状态切换成功: {action}") - return True - - except Exception as e: - logger.error(f"切换用户 {cloud_user.username} 状态失败: {action}, 错误: {str(e)}") - raise \ No newline at end of file diff --git a/apps/operations/tasks.py b/apps/operations/tasks.py deleted file mode 100755 index dc38c60..0000000 --- a/apps/operations/tasks.py +++ /dev/null @@ -1,672 +0,0 @@ -from celery import shared_task -from django.contrib.auth.models import User -from apps.operations.models import AccountOpeningRequest, CloudComputerUser -from apps.hosts.models import Host -from apps.tasks.models import AsyncTask -from apps.tasks.models import TaskProgress -import logging -import secrets -import string - -logger = logging.getLogger(__name__) - - -def generate_secure_password(length=16): - # 排除 PowerShell 双引号字符串中有歧义的字符: " $ ` - _special = "!@#%^&*()_+-=[]{}|;:,.<>?" - alphabet = string.ascii_letters + string.digits + _special - while True: - password = ''.join(secrets.choice(alphabet) for _ in range(length)) - has_upper = any(c.isupper() for c in password) - has_lower = any(c.islower() for c in password) - has_digit = any(c.isdigit() for c in password) - has_special = any(c in _special for c in password) - if has_upper and has_lower and has_digit and has_special: - return password - - -@shared_task(bind=True) -def process_opening_request(self, request_id, operator_id): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"处理开户请求 #{request_id}", - created_by_id=operator_id, - target_object_id=request_id, - target_content_type='operations.AccountOpeningRequest', - status='running' - ) - - try: - request_obj = AccountOpeningRequest.objects.get(id=request_id) - task.start_execution() - - task.progress = 10 - task.save() - - TaskProgress.objects.create( - task=task, - progress=10, - message="开始处理开户请求" - ) - - available_host = Host.objects.filter( - is_active=True, - init_status='ready' - ).first() - - if not available_host: - raise Exception("没有可用的主机资源") - - task.progress = 30 - task.save() - - TaskProgress.objects.create( - task=task, - progress=30, - message="找到可用主机" - ) - - username = request_obj.username - password = generate_secure_password() - - task.progress = 50 - task.save() - - TaskProgress.objects.create( - task=task, - progress=50, - message="执行PowerShell命令创建用户" - ) - - client = available_host.get_connection_client() - - result = client.create_user( - username=username, - password=password, - description=getattr(request_obj, 'user_description', 'Cloud computer user') - ) - - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - raise Exception(f"创建用户失败: {error_msg}") - - task.progress = 70 - task.save() - - TaskProgress.objects.create( - task=task, - progress=70, - message="用户创建成功" - ) - - request_obj.host = available_host - request_obj.windows_username = username - request_obj.windows_password = password - request_obj.status = 'approved' - request_obj.save() - - cloud_user, created = CloudComputerUser.objects.get_or_create( - account_opening_request=request_obj, - defaults={ - 'windows_username': username, - 'host': available_host, - 'status': 'active' - } - ) - if not created: - cloud_user.windows_username = username - cloud_user.host = available_host - cloud_user.status = 'active' - cloud_user.save() - - task.progress = 90 - task.save() - - TaskProgress.objects.create( - task=task, - progress=90, - message="更新请求状态" - ) - - task.progress = 100 - task.complete_success({ - 'host': available_host.hostname, - 'username': username, - 'success': True, - 'cloud_user_id': cloud_user.id - }) - - TaskProgress.objects.create( - task=task, - progress=100, - message="开户请求处理完成" - ) - - return { - 'success': True, - 'host': available_host.hostname, - 'username': username, - 'cloud_user_id': cloud_user.id - } - - except Exception as e: - logger.error(f"处理开户请求失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - try: - rollback_opening_request(request_id) - except Exception as rollback_error: - logger.error(f"回滚开户请求失败: {str(rollback_error)}", exc_info=True) - - return { - 'success': False, - 'error': str(e) - } - - -@shared_task(bind=True) -def remote_set_admin(self, cloud_user_id, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"设置管理员 - 用户 #{cloud_user_id}", - created_by_id=operator_id, - target_object_id=cloud_user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - cloud_user = CloudComputerUser.objects.select_related('product', 'product__host').get(pk=cloud_user_id) - task.start_execution() - - host = cloud_user.product.host - client = host.get_connection_client() - client.op_user(cloud_user.username) - - task.progress = 100 - task.complete_success({'username': cloud_user.username, 'is_admin': True}) - - return {'success': True, 'username': cloud_user.username} - - except Exception as e: - logger.error(f"远程设置管理员失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task(bind=True) -def remote_remove_admin(self, cloud_user_id, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"取消管理员 - 用户 #{cloud_user_id}", - created_by_id=operator_id, - target_object_id=cloud_user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - cloud_user = CloudComputerUser.objects.select_related('product', 'product__host').get(pk=cloud_user_id) - task.start_execution() - - host = cloud_user.product.host - client = host.get_connection_client() - client.deop_user(cloud_user.username) - - task.progress = 100 - task.complete_success({'username': cloud_user.username, 'is_admin': False}) - - return {'success': True, 'username': cloud_user.username} - - except Exception as e: - logger.error(f"远程取消管理员失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task(bind=True) -def remote_reset_windows_password(self, cloud_user_id, new_password, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"重置Windows密码 - 用户 #{cloud_user_id}", - created_by_id=operator_id, - target_object_id=cloud_user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - cloud_user = CloudComputerUser.objects.select_related('product', 'product__host').get(pk=cloud_user_id) - task.start_execution() - - cloud_user.reset_windows_password(new_password) - - task.progress = 100 - task.complete_success({'username': cloud_user.username}) - - return {'success': True, 'username': cloud_user.username} - - except Exception as e: - logger.error(f"远程重置密码失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task(bind=True) -def remote_set_disk_quota(self, cloud_user_id, disk, quota_mb, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"设置磁盘配额 - 用户 #{cloud_user_id}", - created_by_id=operator_id, - target_object_id=cloud_user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - cloud_user = CloudComputerUser.objects.select_related('product', 'product__host').get(pk=cloud_user_id) - task.start_execution() - - from utils.disk_quota import set_disk_quota_via_client - host = cloud_user.product.host - client = host.get_connection_client() - result = set_disk_quota_via_client(client, cloud_user.username, disk, quota_mb) - - if result['success']: - task.progress = 100 - task.complete_success(result) - return result - else: - task.complete_failure(result.get('message', '设置配额失败')) - return result - - except Exception as e: - logger.error(f"远程设置磁盘配额失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task(bind=True) -def remote_set_user_disk_quotas(self, cloud_user_id, disk_quota, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"设置用户磁盘配额 - 用户 #{cloud_user_id}", - created_by_id=operator_id, - target_object_id=cloud_user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - cloud_user = CloudComputerUser.objects.select_related('product', 'product__host').get(pk=cloud_user_id) - task.start_execution() - - from utils.disk_quota import set_user_disk_quotas - host = cloud_user.product.host - client = host.get_connection_client() - result = set_user_disk_quotas(client, cloud_user.username, disk_quota) - - if result['success']: - task.progress = 100 - task.complete_success(result) - return result - else: - task.complete_failure('; '.join(result.get('errors', ['设置配额失败']))) - return result - - except Exception as e: - logger.error(f"远程设置用户磁盘配额失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task(bind=True) -def remote_get_disk_info(self, host_id, operator_id=None): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"获取磁盘信息 - 主机 #{host_id}", - created_by_id=operator_id, - target_object_id=host_id, - target_content_type='hosts.Host', - status='running' - ) - - try: - host = Host.objects.get(pk=host_id) - task.start_execution() - - from utils.disk_quota import get_disk_info_via_client - client = host.get_connection_client() - disks = get_disk_info_via_client(client) - - task.progress = 100 - task.complete_success({'disks': disks}) - - return {'success': True, 'data': disks} - - except Exception as e: - logger.error(f"远程获取磁盘信息失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - return {'success': False, 'error': str(e)} - - -@shared_task( - bind=True, - max_retries=3, - default_retry_delay=30, - autoretry_for=(Exception,), -) -def execute_cloud_user_remote_action(self, user_id, action): - """ - 异步执行云电脑用户的远程操作(禁用/启用/删除) - 将远程 WinRM 调用从 save() 中解耦,避免阻塞数据库事务 - """ - try: - cloud_user = CloudComputerUser.objects.get(pk=user_id) - except CloudComputerUser.DoesNotExist: - logger.error(f"CloudComputerUser with pk={user_id} does not exist, skipping remote action '{action}'") - return {'success': False, 'error': f'User {user_id} not found'} - - if action == 'disable': - cloud_user.disable_remote_user() - elif action == 'enable': - cloud_user.enable_remote_user() - elif action == 'delete': - cloud_user.delete_remote_user() - else: - logger.error(f"Unknown remote action '{action}' for user {cloud_user.username}") - return {'success': False, 'error': f'Unknown action: {action}'} - - return {'success': True, 'action': action, 'user_id': user_id} - - -@shared_task( - bind=True, - max_retries=2, - default_retry_delay=60, -) -def process_account_creation(self, request_id): - """ - 异步处理开户请求的用户创建流程 - 将远程 WinRM 调用从 AccountOpeningRequest.save() 中解耦 - """ - try: - request_obj = AccountOpeningRequest.objects.get(pk=request_id) - except AccountOpeningRequest.DoesNotExist: - logger.error(f"AccountOpeningRequest with pk={request_id} does not exist") - return {'success': False, 'error': f'Request {request_id} not found'} - - try: - request_obj.auto_process_creation() - return {'success': True, 'request_id': request_id} - except Exception as e: - logger.error(f"Account creation failed for request {request_id}: {str(e)}", exc_info=True) - try: - request_obj.refresh_from_db() - if request_obj.status not in ('completed', 'failed'): - request_obj.status = 'failed' - request_obj.result_message = f"异步处理异常: {str(e)}" - request_obj.save(update_fields=['status', 'result_message']) - except Exception as save_err: - logger.error(f"Failed to update request status: {str(save_err)}") - return {'success': False, 'error': str(e)} - - -@shared_task -def cleanup_expired_rdp_domains(): - from django.utils import timezone - from apps.operations.models import RdpDomainRoute - - expired_routes = RdpDomainRoute.objects.filter( - is_active=True, - expires_at__lt=timezone.now(), - ) - - cleaned = 0 - for route in expired_routes: - route.is_active = False - route.save(update_fields=['is_active']) - cleaned += 1 - - logger.info(f"Cleaned up {cleaned} expired RDP domain routes") - return {'cleaned': cleaned} - - -@shared_task -def allocate_rdp_domain(user_id, product_id): - from django.contrib.auth import get_user_model - from django.utils import timezone - from datetime import timedelta - from apps.operations.models import RdpDomainRoute, Product - - User = get_user_model() - - try: - user = User.objects.get(id=user_id) - product = Product.objects.get(id=product_id) - host = product.host - - if not product.enable_host_protection: - return { - 'success': False, - 'error': 'Host protection not enabled for this product', - } - - if host.connection_type != 'tunnel' or not host.tunnel_token: - return { - 'success': False, - 'error': 'Host is not a tunnel host', - } - - domain = RdpDomainRoute.generate_domain() - - route = RdpDomainRoute.objects.create( - domain=domain, - product=product, - assigned_to=user, - tunnel_token=host.tunnel_token, - is_active=True, - expires_at=timezone.now() + timedelta(minutes=10), - ) - - return { - 'success': True, - 'domain': domain, - 'expires_at': route.expires_at.isoformat(), - } - - except Exception as e: - logger.error(f"RDP domain allocation failed: {e}") - return { - 'success': False, - 'error': str(e), - } - - -def rollback_opening_request(request_id): - try: - request_obj = AccountOpeningRequest.objects.get(id=request_id) - if request_obj.host and request_obj.windows_username: - client = request_obj.host.get_connection_client() - - result = client.disabled_user(request_obj.windows_username) - - if result.status_code == 0: - logger.info(f"已禁用用户 {request_obj.windows_username}") - else: - logger.warning(f"禁用用户失败: {result.std_err}") - - request_obj.status = 'pending' - request_obj.save() - - except Exception as e: - logger.error(f"回滚操作失败: {str(e)}") - - -@shared_task(bind=True) -def reset_user_password(self, user_id, operator_id): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"重置用户密码 - 用户 #{user_id}", - created_by_id=operator_id, - target_object_id=user_id, - target_content_type='operations.CloudComputerUser', - status='running' - ) - - try: - user = CloudComputerUser.objects.get(id=user_id) - task.start_execution() - - new_password = generate_secure_password() - - client = user.host.get_connection_client() - - result = client.reset_password(user.windows_username, new_password) - - if result.status_code != 0: - error_msg = result.std_err if result.std_err else 'Unknown error' - raise Exception(f"重置密码失败: {error_msg}") - - if hasattr(user, 'account_opening_request') and user.account_opening_request: - user.account_opening_request.windows_password = new_password - user.account_opening_request.save() - - task.progress = 100 - task.complete_success({ - 'success': True, - 'message': '密码重置成功', - 'username': user.windows_username - }) - - return { - 'success': True, - 'message': '密码重置成功', - 'username': user.windows_username - } - - except Exception as e: - logger.error(f"重置密码失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'error': str(e) - } - - -@shared_task(bind=True) -def batch_process_opening_requests(self, request_ids, operator_id): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"批量处理开户请求 ({len(request_ids)}个)", - created_by_id=operator_id, - status='running' - ) - - try: - task.start_execution() - - results = { - 'processed': 0, - 'successful': 0, - 'failed': 0, - 'errors': [] - } - - total_requests = len(request_ids) - - for idx, request_id in enumerate(request_ids): - try: - progress = int((idx / total_requests) * 80) + 10 - task.progress = progress - task.save() - - result = process_opening_request.delay(request_id, operator_id).get() - - results['processed'] += 1 - if result['success']: - results['successful'] += 1 - else: - results['failed'] += 1 - results['errors'].append({ - 'request_id': request_id, - 'error': result.get('error', 'Unknown error') - }) - - except Exception as e: - results['failed'] += 1 - results['errors'].append({ - 'request_id': request_id, - 'error': str(e) - }) - - task.progress = 100 - task.complete_success(results) - - return results - - except Exception as e: - logger.error(f"批量处理开户请求失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'error': str(e) - } - - -@shared_task(bind=True) -def cleanup_inactive_users(self, days_inactive=30): - task = AsyncTask.objects.create( - task_id=self.request.id, - name=f"清理非活跃用户 (超过{days_inactive}天未使用)", - status='running' - ) - - try: - task.start_execution() - - from django.utils import timezone - from datetime import timedelta - - cutoff_date = timezone.now() - timedelta(days=days_inactive) - - inactive_users = CloudComputerUser.objects.filter( - last_login__lt=cutoff_date, - status='active' - ) - - cleaned_count = 0 - for user in inactive_users: - client = user.host.get_connection_client() - - result = client.disabled_user(user.windows_username) - - if result.status_code == 0: - user.status = 'disabled' - user.save() - cleaned_count += 1 - else: - logger.warning(f"无法禁用用户 {user.windows_username}: {result.std_err}") - - task.progress = 100 - task.complete_success({ - 'cleaned_users': cleaned_count, - 'total_inactive': inactive_users.count() - }) - - return { - 'success': True, - 'cleaned_users': cleaned_count, - 'total_inactive': inactive_users.count() - } - - except Exception as e: - logger.error(f"清理非活跃用户失败: {str(e)}", exc_info=True) - task.complete_failure(str(e)) - - return { - 'success': False, - 'error': str(e) - } diff --git a/apps/operations/urls.py b/apps/operations/urls.py deleted file mode 100755 index 2f69424..0000000 --- a/apps/operations/urls.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -操作记录URL配置 -""" -from django.urls import path -from . import views - -app_name = 'operations' - -urlpatterns = [ - # 系统任务相关URL - path('tasks/', views.SystemTaskListView.as_view(), name='task_list'), - path('tasks//', views.SystemTaskDetailView.as_view(), name='task_detail'), - - # 开户申请相关URL - path('account-openings/', views.AccountOpeningRequestListView.as_view(), name='account_opening_list'), - path('account-openings/create/', views.AccountOpeningRequestCreateView.as_view(), name='account_opening_create'), - path('account-openings/confirm/', views.account_opening_confirm, name='account_opening_confirm'), - path('account-openings/submit/', views.account_opening_submit, name='account_opening_submit'), - # 已删除:approve/reject/process 路由 - 功能已迁移至 Django Admin - path('account-openings//', views.account_opening_detail, name='account_opening_detail'), - - # 云电脑用户相关URL - path('cloud-users/', views.CloudComputerUserListView.as_view(), name='cloud_user_list'), - # 已删除:toggle-status 路由 - 功能已迁移至 Django Admin - - # 我的云电脑相关URL - path('my-cloud-computers/', views.MyCloudComputersView.as_view(), name='my_cloud_computers'), - path('my-cloud-computers//', views.my_cloud_computer_detail, name='my_cloud_computer_detail'), - path('my-cloud-computers//get-password/', views.get_password_and_burn, name='get_password_and_burn'), - path('my-cloud-computers//reset-password/', views.reset_password_and_burn, name='reset_password_and_burn'), - - # 磁盘配额相关API - path('api/product//disk-config/', views.get_product_disk_config, name='product_disk_config'), - path('api/host//disk-info/', views.get_host_disk_info, name='host_disk_info'), - - # 邀请链接相关URL - path('invite//', views.product_invite_view, name='product_invite'), - - path('rdp/connect//', views.rdp_connect, name='rdp_connect'), -] \ No newline at end of file diff --git a/apps/operations/urls_admin.py b/apps/operations/urls_admin.py deleted file mode 100644 index aa82ef9..0000000 --- a/apps/operations/urls_admin.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -超管后台 - 运营管理 URL 配置 - -命名空间: admin_operations -超管可查看所有运营数据,无数据隔离。 -""" - -from django.urls import path - -from . import views_admin - -app_name = 'admin_operations' - -urlpatterns = [ - # ========== 产品管理 ========== - path('products/', views_admin.AdminProductListView.as_view(), name='product_list'), - path('products/wizard/', views_admin.admin_product_wizard, name='product_wizard'), - path('products/create/', views_admin.AdminProductCreateView.as_view(), name='product_create'), - path('products//edit/', views_admin.AdminProductUpdateView.as_view(), name='product_edit'), - path('products//delete/', views_admin.AdminProductDeleteView.as_view(), name='product_delete'), - - # ========== 产品组管理 ========== - path('product-groups/', views_admin.AdminProductGroupListView.as_view(), name='productgroup_list'), - path('product-groups/create/', views_admin.AdminProductGroupCreateView.as_view(), name='productgroup_create'), - path('product-groups//edit/', views_admin.AdminProductGroupUpdateView.as_view(), name='productgroup_edit'), - path('product-groups//delete/', views_admin.AdminProductGroupDeleteView.as_view(), name='productgroup_delete'), - - # ========== 开户申请管理 ========== - path('requests/', views_admin.AdminRequestListView.as_view(), name='request_list'), - path('requests//', views_admin.AdminRequestDetailView.as_view(), name='request_detail'), - path('requests//approve/', views_admin.AdminRequestApproveView.as_view(), name='request_approve'), - path('requests//reject/', views_admin.AdminRequestRejectView.as_view(), name='request_reject'), - path('requests//retry/', views_admin.AdminRequestRetryView.as_view(), name='request_retry'), - path('requests/batch-approve/', views_admin.AdminRequestBatchApproveView.as_view(), name='request_batch_approve'), - path('requests/batch-reject/', views_admin.AdminRequestBatchRejectView.as_view(), name='request_batch_reject'), - - # ========== 云电脑用户管理 ========== - path('users/', views_admin.AdminCloudUserListView.as_view(), name='user_list'), - path('users//', views_admin.AdminCloudUserDetailView.as_view(), name='user_detail'), - path('users//action/', views_admin.admin_cloud_user_action, name='user_action'), - path('users//set-quota/', views_admin.admin_cloud_user_set_quota, name='user_set_quota'), - - # ========== 邀请令牌管理 ========== - path('tokens/', views_admin.AdminTokenListView.as_view(), name='token_list'), - path('tokens//', views_admin.AdminTokenDetailView.as_view(), name='token_detail'), - - # ========== 访问授权管理 ========== - path('grants/', views_admin.AdminGrantListView.as_view(), name='grant_list'), - - # ========== RDP域名路由管理 ========== - path('routes/', views_admin.AdminRouteListView.as_view(), name='route_list'), - - # ========== 系统任务管理 ========== - path('tasks/', views_admin.AdminTaskListView.as_view(), name='task_list'), -] diff --git a/apps/operations/urls_provider.py b/apps/operations/urls_provider.py deleted file mode 100644 index 5146754..0000000 --- a/apps/operations/urls_provider.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -运营管理 - 提供商后台 URL 配置 - -开户申请、邀请令牌、访问授权、RDP域名路由、系统任务、 -产品管理、产品组管理相关路由, -挂载在 /provider/operations/ 下。 -命名空间: provider_operations -""" - -from django.urls import path - -from . import views_provider - -app_name = 'provider_operations' - -urlpatterns = [ - # ---- 产品管理 ---- - path( - 'products/', - views_provider.ProductListView.as_view(), - name='product_list', - ), - path( - 'products/create/', - views_provider.ProductCreateView.as_view(), - name='product_create', - ), - path( - 'products//', - views_provider.ProductDetailView.as_view(), - name='product_detail', - ), - path( - 'products//edit/', - views_provider.ProductUpdateView.as_view(), - name='product_edit', - ), - path( - 'products//delete/', - views_provider.ProductDeleteView.as_view(), - name='product_delete', - ), - - # ---- 产品组管理 ---- - path( - 'product-groups/', - views_provider.ProductGroupListView.as_view(), - name='productgroup_list', - ), - path( - 'product-groups/create/', - views_provider.ProductGroupCreateView.as_view(), - name='productgroup_create', - ), - path( - 'product-groups//edit/', - views_provider.ProductGroupUpdateView.as_view(), - name='productgroup_edit', - ), - path( - 'product-groups//delete/', - views_provider.ProductGroupDeleteView.as_view(), - name='productgroup_delete', - ), - - # ---- 云电脑用户 ---- - path( - 'users/', - views_provider.CloudComputerUserListView.as_view(), - name='user_list', - ), - path( - 'users//', - views_provider.CloudComputerUserDetailView.as_view(), - name='user_detail', - ), - path( - 'users//sync-admin/', - views_provider.CloudComputerUserSyncAdminView.as_view(), - name='user_sync_admin', - ), - path( - 'users//disk-quota/', - views_provider.CloudComputerUserSetDiskQuotaView.as_view(), - name='user_disk_quota', - ), - path( - 'users//reset-password/', - views_provider.CloudComputerUserResetPasswordView.as_view(), - name='user_reset_password', - ), - path( - 'users/batch-activate/', - views_provider.CloudComputerUserBatchActivateView.as_view(), - name='user_batch_activate', - ), - path( - 'users/batch-deactivate/', - views_provider.CloudComputerUserBatchDeactivateView.as_view(), - name='user_batch_deactivate', - ), - path( - 'users/batch-disable/', - views_provider.CloudComputerUserBatchDisableView.as_view(), - name='user_batch_disable', - ), - - # ---- 开户申请 ---- - path( - 'requests/', - views_provider.AccountOpeningRequestListView.as_view(), - name='request_list', - ), - # 批量批准(必须在 之前,避免路径冲突) - path( - 'requests/batch-approve/', - views_provider.AccountOpeningRequestBatchApproveView.as_view(), - name='request_batch_approve', - ), - # 批量驳回(必须在 之前,避免路径冲突) - path( - 'requests/batch-reject/', - views_provider.AccountOpeningRequestBatchRejectView.as_view(), - name='request_batch_reject', - ), - # 开户申请详情 - path( - 'requests//', - views_provider.AccountOpeningRequestDetailView.as_view(), - name='request_detail', - ), - # 批准单条申请 - path( - 'requests//approve/', - views_provider.AccountOpeningRequestApproveView.as_view(), - name='request_approve', - ), - # 驳回单条申请 - path( - 'requests//reject/', - views_provider.AccountOpeningRequestRejectView.as_view(), - name='request_reject', - ), - # 执行开户 - path( - 'requests//execute/', - views_provider.AccountOpeningRequestExecuteView.as_view(), - name='request_execute', - ), - - # ---- 邀请令牌 ---- - path( - 'tokens/', - views_provider.ProductInvitationTokenListView.as_view(), - name='token_list', - ), - path( - 'tokens//', - views_provider.ProductInvitationTokenDetailView.as_view(), - name='token_detail', - ), - path( - 'tokens/create/', - views_provider.ProductInvitationTokenCreateView.as_view(), - name='token_create', - ), - path( - 'tokens/batch-enable/', - views_provider.ProductInvitationTokenBatchEnableView.as_view(), - name='token_batch_enable', - ), - path( - 'tokens/batch-disable/', - views_provider.ProductInvitationTokenBatchDisableView.as_view(), - name='token_batch_disable', - ), - - # ---- 访问授权 ---- - path( - 'grants/', - views_provider.ProductAccessGrantListView.as_view(), - name='grant_list', - ), - path( - 'grants/batch-revoke/', - views_provider.ProductAccessGrantBatchRevokeView.as_view(), - name='grant_batch_revoke', - ), - - # ---- RDP 域名路由 ---- - path( - 'routes/', - views_provider.RdpDomainRouteListView.as_view(), - name='route_list', - ), - - # ---- 系统任务(只读参考) ---- - path( - 'tasks/', - views_provider.SystemTaskListView.as_view(), - name='task_list', - ), -] diff --git a/apps/operations/views.py b/apps/operations/views.py deleted file mode 100755 index 0922a9b..0000000 --- a/apps/operations/views.py +++ /dev/null @@ -1,955 +0,0 @@ -""" -操作记录视图 -""" - -from django.shortcuts import render, get_object_or_404, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.urls import reverse_lazy -from django.views.generic import ListView, CreateView, DetailView -from django.utils.decorators import method_decorator -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse, HttpResponseForbidden -import logging - -logger = logging.getLogger(__name__) -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.http import require_POST -from django.utils.translation import gettext_lazy as _ -from django.db.models import Q -from datetime import timedelta -from .models import AccountOpeningRequest, SystemTask, CloudComputerUser, Product -from .forms import ( - AccountOpeningRequestForm, - AccountOpeningRequestFilterForm, - CloudComputerUserFilterForm, -) -from apps.hosts.models import Host - - -@method_decorator(login_required, name="dispatch") -class SystemTaskListView(ListView): - """系统任务列表视图""" - - model = SystemTask - template_name = "operations/systemtask_list.html" - context_object_name = "tasks" - paginate_by = 20 - - def get_queryset(self): - """获取查询集""" - queryset = SystemTask.objects.all() - - # 应用过滤条件 - form = SystemTaskFilterForm(self.request.GET) - if form.is_valid(): - task_type = form.cleaned_data.get("task_type") - status = form.cleaned_data.get("status") - start_date = form.cleaned_data.get("start_date") - end_date = form.cleaned_data.get("end_date") - - if task_type: - queryset = queryset.filter(task_type__icontains=task_type[:50]) - if status: - queryset = queryset.filter(status=status) - if start_date: - queryset = queryset.filter(created_at__gte=start_date) - if end_date: - # 包含结束日期的整天 - end_date = end_date + timedelta(days=1) - queryset = queryset.filter(created_at__lt=end_date) - - return queryset.select_related("created_by").order_by("-created_at") - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context["filter_form"] = SystemTaskFilterForm(self.request.GET) - return context - - -@method_decorator(login_required, name="dispatch") -class SystemTaskDetailView(DetailView): - """系统任务详情视图""" - - model = SystemTask - template_name = "operations/systemtask_detail.html" - context_object_name = "task" - - -@login_required -def task_progress(request, task_id): - """ - 获取任务进度 - - Args: - request: HTTP请求对象 - task_id: 任务ID - - Returns: - JsonResponse: JSON格式的响应 - """ - try: - task = SystemTask.objects.get(pk=task_id) - return JsonResponse( - { - "success": True, - "data": { - "id": task.id, - "name": task.name, - "status": task.status, - "progress": task.progress, - "result": task.result, - "error_message": task.error_message, - }, - } - ) - except SystemTask.DoesNotExist: - return JsonResponse({"success": False, "message": "任务不存在"}) - - -@method_decorator(login_required, name="dispatch") -class AccountOpeningRequestCreateView(CreateView): - """创建开户申请视图""" - - model = AccountOpeningRequest - form_class = AccountOpeningRequestForm - template_name = "operations/account_opening_request_form.html" - success_url = reverse_lazy("operations:account_opening_confirm") - - def get_form_kwargs(self): - """获取表单初始化参数""" - kwargs = super().get_form_kwargs() - - # 获取目标产品ID参数 - target_product_id = self.request.GET.get("target_product") - target_host_id = self.request.GET.get("target_host") # 兼容旧参数 - - # 获取可用产品查询集 - site_group = getattr(self.request, "site_group", None) - if site_group: - products_qs = Product.objects.filter( - is_available=True, site_group=site_group - ) - else: - products_qs = Product.objects.filter( - is_available=True, site_group__isnull=True - ) - - # 如果指定了特定产品,限制查询集 - if target_product_id: - try: - if site_group: - target_product = Product.objects.filter(site_group=site_group).get( - id=target_product_id, is_available=True - ) - else: - target_product = Product.objects.get( - id=target_product_id, is_available=True, site_group__isnull=True - ) - products_qs = Product.objects.filter(id=target_product.id) - except Product.DoesNotExist: - pass - elif target_host_id: - # 兼容旧参数:如果通过target_host指定,则找出关联的产品 - try: - from apps.hosts.models import Host - - host = Host.objects.get(id=target_host_id) - # 获取与该主机关联的所有可用产品 - if site_group: - products_qs = Product.objects.filter( - host=host, is_available=True, site_group=site_group - ) - else: - products_qs = Product.objects.filter( - host=host, is_available=True, site_group__isnull=True - ) - except Host.DoesNotExist: - pass - - # 将产品查询集传递给表单 - kwargs["products_qs"] = products_qs - return kwargs - - def form_valid(self, form): - """表单验证成功后的处理""" - # 将表单数据存储到session中以供确认页面使用 - confirm_data = { - "contact_email": self.request.user.email, # 使用当前用户的邮箱,而不是从表单获取 - "username": form.cleaned_data["username"], - "user_fullname": form.cleaned_data["user_fullname"], - "user_description": form.cleaned_data["user_description"], - "target_product_id": form.cleaned_data["target_product"].id, - "target_product_name": form.cleaned_data["target_product"].display_name, - "requested_disk_capacity": form.cleaned_data.get( - "requested_disk_capacity", {} - ), - } - self.request.session["confirm_data"] = confirm_data - - # 重定向到确认页面,而不是直接保存 - return redirect("operations:account_opening_confirm") - - def form_invalid(self, form): - """表单验证失败后的处理""" - messages.error(self.request, "开户申请信息填写有误,请检查输入信息。") - return super().form_invalid(form) - - -@login_required -def account_opening_confirm(request): - """开户申请确认页面""" - confirm_data = request.session.get("confirm_data") - if not confirm_data: - messages.error(request, "未找到待确认的申请信息,请重新填写申请。") - return redirect("operations:account_opening_create") - - context = {"confirm_data": confirm_data} - return render(request, "operations/account_opening_confirm.html", context) - - -@csrf_protect -@require_POST -@login_required -def account_opening_submit(request): - """提交开户申请""" - confirm_data = request.session.get("confirm_data") - if not confirm_data: - messages.error(request, "未找到待提交的申请信息。") - logger.warning( - f"用户 {request.user.username} 尝试提交开户申请,但未找到确认数据" - ) - return redirect("operations:account_opening_create") - - # 创建开户申请对象 - account_request = AccountOpeningRequest() - account_request.applicant = request.user - account_request.contact_email = ( - request.user.email - ) # 使用当前用户的邮箱,而不是表单中的数据 - account_request.username = confirm_data["username"] - account_request.user_fullname = confirm_data["user_fullname"] - account_request.user_email = request.user.email # 使用当前用户的邮箱 - account_request.user_description = confirm_data["user_description"] - account_request.requested_disk_capacity = confirm_data.get( - "requested_disk_capacity", {} - ) - # 移除了requested_password字段,由系统自动生成 - - # 设置目标产品 - try: - site_group = getattr(request, "site_group", None) - if site_group: - target_product = Product.objects.filter(site_group=site_group).get( - id=confirm_data["target_product_id"] - ) - else: - target_product = Product.objects.get( - id=confirm_data["target_product_id"], site_group__isnull=True - ) - account_request.target_product = target_product - logger.info( - f"用户 {request.user.username} 提交开户申请,目标产品: {target_product.name}, 用户名: {account_request.username}, 联系邮箱: {account_request.contact_email}" - ) - except Product.DoesNotExist: - messages.error(request, "指定的目标产品不存在。") - logger.error( - f'用户 {request.user.username} 尝试提交申请,但目标产品ID {confirm_data["target_product_id"]} 不存在' - ) - return redirect("operations:account_opening_create") - - try: - logger.info(f"准备保存开户申请,当前状态: {account_request.status}") - account_request.save() - logger.info( - f"开户申请已保存,ID: {account_request.id}, 最终状态: {account_request.status}" - ) - messages.success(request, "开户申请已成功提交,请等待审核。") - - # 清除session中的确认数据 - del request.session["confirm_data"] - - return redirect("operations:account_opening_list") - except Exception as e: - logger.error(f"提交申请时发生错误: {str(e)}", exc_info=True) - messages.error(request, "提交申请时发生错误,请稍后重试") - return redirect("operations:account_opening_create") - - -class AccountOpeningRequestListView(ListView): - """开户申请列表视图""" - - model = AccountOpeningRequest - template_name = "operations/account_opening_request_list.html" - context_object_name = "requests" - paginate_by = 20 - - def get_queryset(self): - """获取查询集""" - queryset = AccountOpeningRequest.objects.all() - - site_group = getattr(self.request, "site_group", None) - if site_group: - queryset = queryset.filter(target_product__site_group=site_group) - else: - queryset = queryset.filter(target_product__site_group__isnull=True) - - if self.request.user.is_authenticated: - if not (self.request.user.is_staff or self.request.user.is_superuser): - queryset = queryset.filter(applicant=self.request.user) - else: - # 未认证用户不显示任何申请 - queryset = queryset.none() - - # 应用过滤条件 - form = AccountOpeningRequestFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - if form.is_valid(): - status = form.cleaned_data.get("status") - if status: - queryset = queryset.filter(status=status) - - host = form.cleaned_data.get("host") - if host: - # 查询与该主机相关的产品的申请 - queryset = queryset.filter(target_product__host=host) - - search = form.cleaned_data.get("search") - if search: - queryset = queryset.filter( - Q(username__icontains=search[:50]) - | Q(user_fullname__icontains=search[:50]) - | Q(contact_email__icontains=search[:50]) - ) - - return queryset.select_related( - "applicant", "target_product", "target_product__host", "approved_by" - ).order_by("-created_at") - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context["filter_form"] = AccountOpeningRequestFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - context["statuses"] = AccountOpeningRequest._meta.get_field("status").choices - - # 如果是管理员,显示所有主机;否则只显示与用户申请相关的产品的主机 - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_authenticated and ( - self.request.user.is_staff or self.request.user.is_superuser - ): - if site_group: - context["hosts"] = Host.objects.filter(site_group=site_group) - else: - context["hosts"] = Host.objects.filter(site_group__isnull=True) - elif self.request.user.is_authenticated: - host_qs = Host.objects.filter( - product__accountopeningrequest__applicant=self.request.user - ).distinct() - if site_group: - host_qs = host_qs.filter(site_group=site_group) - else: - host_qs = host_qs.filter(site_group__isnull=True) - context["hosts"] = host_qs - else: - context["hosts"] = Host.objects.none() - - return context - - -@login_required -def account_opening_detail(request, pk): - """查看开户申请详情""" - site_group = getattr(request, "site_group", None) - if site_group: - account_request = get_object_or_404( - AccountOpeningRequest.objects.filter( - Q(target_product__site_group=site_group) - ), - pk=pk, - ) - else: - account_request = get_object_or_404( - AccountOpeningRequest, pk=pk, target_product__site_group__isnull=True - ) - - # 检查权限:用户只能查看自己提交的申请 - if account_request.applicant != request.user and not ( - request.user.is_staff or request.user.is_superuser - ): - messages.error(request, "您没有权限查看此申请的详情。") - return redirect("operations:account_opening_list") - - timeline = [] - timeline.append( - { - "label": "提交申请", - "time": account_request.created_at, - "done": True, - } - ) - if account_request.status in ( - "approved", - "rejected", - "processing", - "completed", - "failed", - ): - timeline.append( - { - "label": "审核完成", - "time": account_request.approval_date, - "done": ( - account_request.status != "failed" - or account_request.approval_date is not None - ), - "detail": ("批准" if account_request.status != "rejected" else "驳回"), - } - ) - if account_request.status in ("processing", "completed", "failed"): - timeline.append( - { - "label": "执行开户", - "time": ( - account_request.updated_at - if account_request.status == "completed" - else None - ), - "done": account_request.status in ("completed",), - "detail": ( - "开户处理失败,请联系管理员了解详情" - if account_request.status == "failed" - else account_request.result_message - if account_request.result_message - else None - ), - } - ) - - context = { - "request": account_request, - "timeline": timeline, - } - return render(request, "operations/account_opening_request_detail.html", context) - - -@method_decorator(login_required, name="dispatch") -class CloudComputerUserListView(ListView): - """云电脑用户列表视图""" - - model = CloudComputerUser - template_name = "operations/cloud_computer_user_list.html" - context_object_name = "cloud_users" - paginate_by = 20 - - def get_queryset(self): - """获取查询集""" - queryset = CloudComputerUser.objects.all() - - site_group = getattr(self.request, "site_group", None) - if site_group: - queryset = queryset.filter(product__site_group=site_group) - else: - queryset = queryset.filter(product__site_group__isnull=True) - - form = CloudComputerUserFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - if form.is_valid(): - status = form.cleaned_data.get("status") - if status: - queryset = queryset.filter(status=status) - - product = form.cleaned_data.get("product") - if product: - queryset = queryset.filter(product=product) - - search = form.cleaned_data.get("search") - if search: - queryset = queryset.filter( - Q(username__icontains=search[:50]) - | Q(fullname__icontains=search[:50]) - | Q(email__icontains=search[:50]) - ) - - return queryset.select_related( - "product", "created_from_request__applicant" - ).order_by("-created_at") - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context["filter_form"] = CloudComputerUserFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - context["statuses"] = CloudComputerUser._meta.get_field("status").choices - site_group = getattr(self.request, "site_group", None) - if site_group: - context["products"] = Product.objects.filter(site_group=site_group) - else: - context["products"] = Product.objects.filter(site_group__isnull=True) - return context - - -# 已删除:toggle_cloud_user_status -# 用户状态切换功能已迁移至 Django Admin 的 Action 实现 - - -@method_decorator(login_required, name="dispatch") -class MyCloudComputersView(ListView): - """我的云电脑用户列表视图 - - 显示当前用户拥有的云电脑用户 - """ - - model = CloudComputerUser - template_name = "operations/my_cloud_computers.html" - context_object_name = "cloud_users" - paginate_by = 20 - - def get_queryset(self): - """获取查询集 - 只显示当前用户通过开户申请创建的云电脑用户""" - queryset = CloudComputerUser.objects.filter( - created_from_request__applicant=self.request.user - ) - - site_group = getattr(self.request, "site_group", None) - if site_group: - queryset = queryset.filter(product__site_group=site_group) - else: - queryset = queryset.filter(product__site_group__isnull=True) - - form = CloudComputerUserFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - if form.is_valid(): - status = form.cleaned_data.get("status") - if status: - queryset = queryset.filter(status=status) - - search = form.cleaned_data.get("search") - if search: - queryset = queryset.filter( - Q(username__icontains=search) - | Q(fullname__icontains=search) - | Q(email__icontains=search) - | Q(product__display_name__icontains=search) - ) - - # 按产品筛选 - product_filter = self.request.GET.get("product") - if product_filter: - queryset = queryset.filter(product__display_name=product_filter) - - return queryset.select_related("product", "created_from_request").order_by( - "-created_at" - ) - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context["filter_form"] = CloudComputerUserFilterForm( - self.request.GET, - site_group=getattr(self.request, "site_group", None), - ) - context["statuses"] = CloudComputerUser._meta.get_field("status").choices - - context["current_product"] = self.request.GET.get("product", "") - context["current_search"] = self.request.GET.get("search", "") - context["current_status"] = self.request.GET.get("status", "") - - from collections import defaultdict - - cloud_users_by_product = defaultdict(list) - for user in context["cloud_users"]: - cloud_users_by_product[user.product.display_name].append(user) - context["cloud_users_by_product"] = dict(cloud_users_by_product) - - return context - - -@login_required -def my_cloud_computer_detail(request, pk): - """我的云电脑用户详情页面""" - cloud_user = get_object_or_404(CloudComputerUser, pk=pk) - - # 权限检查:owner优先,兼容旧数据用created_from_request - if cloud_user.owner: - if cloud_user.owner != request.user: - return HttpResponseForbidden("无权访问") - elif cloud_user.created_from_request: - if cloud_user.created_from_request.applicant != request.user: - return HttpResponseForbidden("无权访问") - else: - return HttpResponseForbidden("无权访问") - - context = {"cloud_user": cloud_user} - return render(request, "operations/my_cloud_computer_detail.html", context) - - -@login_required -@require_POST -def get_password_and_burn(request, pk): - """获取密码并销毁 - 阅后即焚""" - cloud_user = get_object_or_404(CloudComputerUser, pk=pk) - - # 权限检查 - has_access = False - if cloud_user.owner and cloud_user.owner == request.user: - has_access = True - elif ( - cloud_user.created_from_request - and cloud_user.created_from_request.applicant == request.user - ): - has_access = True - - if not has_access: - return JsonResponse({"success": False, "error": "无权访问"}, status=403) - - try: - password = cloud_user.get_and_burn_password() - return JsonResponse({"success": True, "password": password}) - except Exception as e: - logger.error(f"获取密码失败: {str(e)}", exc_info=True) - return JsonResponse({"success": False, "error": "Failed to retrieve password"}) - - -@login_required -@require_POST -def reset_password_and_burn(request, pk): - """重置密码并返回新密码(阅后即焚)""" - cloud_user = get_object_or_404(CloudComputerUser, pk=pk) - - # 权限检查 - has_access = False - if cloud_user.owner and cloud_user.owner == request.user: - has_access = True - elif ( - cloud_user.created_from_request - and cloud_user.created_from_request.applicant == request.user - ): - has_access = True - - if not has_access: - return JsonResponse({"success": False, "error": "无权访问"}, status=403) - - try: - new_password = cloud_user.reset_and_get_new_password() - return JsonResponse({"success": True, "password": new_password}) - except Exception as e: - logger.error(f"重置密码失败: {str(e)}", exc_info=True) - return JsonResponse({"success": False, "error": str(e)}) - - -@login_required -def get_product_disk_config(request, product_id): - """获取产品的磁盘配额配置""" - try: - site_group = getattr(request, "site_group", None) - if site_group: - product = Product.objects.filter(site_group=site_group).get( - pk=product_id, is_available=True - ) - else: - product = Product.objects.get( - pk=product_id, is_available=True, site_group__isnull=True - ) - except Product.DoesNotExist: - return JsonResponse({"success": False, "error": "产品不存在"}, status=404) - - return JsonResponse( - { - "success": True, - "data": { - "enable_disk_quota": product.enable_disk_quota, - "default_disk_quota": product.default_disk_quota, - "allow_extra_quota_disks": product.allow_extra_quota_disks, - }, - } - ) - - -@login_required -def get_host_disk_info(request, host_id): - """获取主机的磁盘信息""" - - site_group = getattr(request, "site_group", None) - host_qs = Host.objects.filter(pk=host_id) - if site_group: - host_qs = host_qs.filter(site_group=site_group) - else: - host_qs = host_qs.filter(site_group__isnull=True) - try: - host = host_qs.get() - except Host.DoesNotExist: - return JsonResponse({"success": False, "error": "主机不存在"}, status=404) - - if not request.user.is_superuser and not request.user.is_staff: - if ( - not host.administrators.filter(pk=request.user.pk).exists() - and not host.providers.filter(pk=request.user.pk).exists() - ): - return JsonResponse({"success": False, "error": "无权访问"}, status=403) - - try: - from apps.operations.tasks import remote_get_disk_info - - result = remote_get_disk_info.delay(host_id, operator_id=request.user.pk) - return JsonResponse( - { - "success": True, - "task_id": result.id, - "message": "磁盘信息获取已提交,正在后台执行", - } - ) - except Exception as e: - logger.error(f"Error dispatching disk info task: {str(e)}", exc_info=True) - return JsonResponse({"success": False, "error": "Failed to get disk info"}) - - -def product_invite_view(request, token): - """ - 产品邀请链接视图 - - 用户访问邀请链接后,解锁对应产品或产品组的访问权限 - GET: 显示邀请信息和确认页面 - POST: 确认接受邀请,执行授权操作 - """ - from django.shortcuts import render, redirect - from django.contrib import messages - from django.contrib.auth import get_user_model - from django.urls import reverse - from django.db.models import Q - from .models import ProductInvitationToken, ProductAccessGrant - - User = get_user_model() - - site_group = getattr(request, "site_group", None) - if site_group: - invite_token = ( - ProductInvitationToken.objects.filter(token=token) - .filter( - Q(product__site_group=site_group) - | Q(product_group__site_group=site_group) - ) - .first() - ) - else: - invite_token = ( - ProductInvitationToken.objects.filter(token=token) - .filter( - Q(product__site_group__isnull=True) - | Q(product_group__site_group__isnull=True) - ) - .first() - ) - - if not invite_token: - return render( - request, - "operations/invite_result.html", - { - "success": False, - "message": "邀请链接无效或不存在。", - }, - ) - - if not invite_token.is_valid(): - if invite_token.is_expired(): - return render( - request, - "operations/invite_result.html", - { - "success": False, - "message": "邀请链接已过期。", - }, - ) - if invite_token.is_exhausted(): - return render( - request, - "operations/invite_result.html", - { - "success": False, - "message": "邀请链接已达到最大使用次数。", - }, - ) - return render( - request, - "operations/invite_result.html", - { - "success": False, - "message": "邀请链接已被禁用。", - }, - ) - - if not request.user.is_authenticated: - login_url = reverse("accounts:login") + f"?next={request.path}" - return redirect(login_url) - - existing_grant = ProductAccessGrant.objects.filter( - user=request.user, - product=invite_token.product, - product_group=invite_token.product_group, - is_revoked=False, - ).first() - - if existing_grant and not existing_grant.is_expired(): - return render( - request, - "operations/invite_result.html", - { - "success": True, - "message": "您已经拥有该产品的访问权限。", - "product": invite_token.product, - "product_group": invite_token.product_group, - }, - ) - - if request.method == "GET": - target_name = ( - invite_token.product.display_name - if invite_token.product - else ( - invite_token.product_group.name - if invite_token.product_group - else "未知" - ) - ) - return render( - request, - "operations/invite_result.html", - { - "success": None, - "message": f'您即将解锁 "{target_name}" 的访问权限。', - "product": invite_token.product, - "product_group": invite_token.product_group, - "needs_confirm": True, - "token": token, - }, - ) - - try: - grant, created = ProductAccessGrant.objects.get_or_create( - user=request.user, - product=invite_token.product, - product_group=invite_token.product_group, - defaults={ - "granted_by_token": invite_token, - "expires_at": invite_token.expires_at, - }, - ) - if not created: - if grant.is_revoked or grant.is_expired(): - grant.is_revoked = False - grant.revoked_at = None - grant.revoked_by = None - grant.granted_by_token = invite_token - grant.expires_at = invite_token.expires_at - grant.save( - update_fields=[ - "is_revoked", - "revoked_at", - "revoked_by", - "granted_by_token", - "expires_at", - ] - ) - except Exception as e: - logger.error(f"创建授权记录失败: {str(e)}", exc_info=True) - return render( - request, - "operations/invite_result.html", - { - "success": False, - "message": "授权处理失败,请稍后重试。", - }, - ) - - invite_token.increment_usage() - - target_name = ( - invite_token.product.display_name - if invite_token.product - else (invite_token.product_group.name if invite_token.product_group else "未知") - ) - - return render( - request, - "operations/invite_result.html", - { - "success": True, - "message": f'恭喜!您已成功解锁 "{target_name}" 的访问权限。', - "product": invite_token.product, - "product_group": invite_token.product_group, - }, - ) - - -@login_required -def rdp_connect(request, product_id): - from django.http import HttpResponse - from django.conf import settings - from utils.gateway_client import GatewayClient - from apps.dashboard.models import SystemConfig - - site_group = getattr(request, "site_group", None) - if site_group: - product = get_object_or_404( - Product.objects.filter(site_group=site_group), pk=product_id - ) - else: - product = get_object_or_404(Product, pk=product_id, site_group__isnull=True) - - cloud_user = ( - CloudComputerUser.objects.filter( - product=product, - status="active", - ) - .filter(Q(owner=request.user) | Q(created_from_request__applicant=request.user)) - .first() - ) - - if cloud_user is None: - messages.error(request, "您没有访问此云电脑的权限。") - return redirect("operations:my_cloud_computers") - - host = product.host - if not host or not host.tunnel_token: - messages.error(request, "该产品关联的主机未配置隧道,无法通过RD Gateway连接。") - return redirect("operations:my_cloud_computers") - - gateway_address = getattr(settings, "GATEWAY_ADDRESS", "rdp.2c2a.com") - gateway_port = getattr(settings, "GATEWAY_PORT", 443) - expires_in = getattr(settings, "GATEWAY_PAA_TOKEN_EXPIRY_SECONDS", 600) - - client_ip = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip() - if not client_ip: - client_ip = request.META.get("REMOTE_ADDR") - - client = GatewayClient() - paa_token = client.issue_paa_token( - user_email=request.user.email, - tunnel_token=host.tunnel_token, - client_ip=client_ip, - expires_in=expires_in, - ) - - rdp_content = client.generate_rdp_file( - gateway_address=gateway_address, - gateway_port=gateway_port, - user_email=request.user.email, - paa_token=paa_token, - ) - - filename = f"{product.display_name}.rdp" - response = HttpResponse(rdp_content, content_type="application/x-rdp") - response["Content-Disposition"] = f'attachment; filename="{filename}"' - return response diff --git a/apps/operations/views_admin.py b/apps/operations/views_admin.py deleted file mode 100644 index bd85bc5..0000000 --- a/apps/operations/views_admin.py +++ /dev/null @@ -1,1773 +0,0 @@ -""" -运营管理 - 超级管理员后台视图 - -包含产品、产品组、开户申请、云电脑用户、邀请令牌、访问授权、 -RDP域名路由、系统任务等视图。 -所有视图均受超级管理员身份验证保护。 -超管可查看所有数据;提供商仅可查看自己创建的数据。 -""" - -import json -import logging - -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.core.paginator import Paginator -from django.db.models import Q -from django.http import HttpResponseRedirect, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse -from django.utils import timezone -from django.utils.decorators import method_decorator -from django.views import View -from django.views.generic import DetailView, ListView, TemplateView - -from apps.accounts.provider_decorators import admin_required -from utils.provider import get_provider_products -from apps.operations.models import ( - AccountOpeningRequest, - CloudComputerUser, - Product, - ProductGroup, - ProductInvitationToken, - ProductAccessGrant, - RdpDomainRoute, -) -from apps.tasks.models import AsyncTask - -from .forms_admin import ( - AdminProductForm, - AdminProductGroupForm, - AdminRequestRejectForm, -) - -logger = logging.getLogger(__name__) -User = get_user_model() - - -# ========== 辅助函数 ========== - - -def _get_selected_ids(request): - """ - 从 POST 请求中提取选中的 ID 列表 - - 支持两种格式: - 1. 表单字段: selected_ids=1&selected_ids=2 - 2. JSON 字符串: selected_ids=[1,2,3] - """ - ids = request.POST.getlist("selected_ids") - if ids: - return [int(i) for i in ids if i.strip().isdigit()] - - raw = request.POST.get("selected_ids", "") - if raw: - try: - parsed = json.loads(raw) - if isinstance(parsed, list): - return [ - int(i) - for i in parsed - if isinstance(i, (int, str)) and str(i).isdigit() - ] - except (json.JSONDecodeError, ValueError): - pass - - return [] - - -# =========================================================================== -# 产品管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminProductListView(TemplateView): - """ - 超管产品列表视图 - - - 查看所有产品(无数据隔离) - - 搜索、筛选、分页 - - 显示创建者信息 - """ - - template_name = "admin_base/operations/product_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - queryset = Product.objects.select_related( - "host", - "product_group", - "created_by", - ) - elif self.request.user.is_site_group_admin(site_group): - if site_group: - queryset = Product.objects.filter(site_group=site_group).select_related( - "host", - "product_group", - "created_by", - ) - else: - queryset = Product.objects.none() - else: - queryset = get_provider_products( - self.request.user, site_group=site_group - ).select_related( - "host", - "product_group", - "created_by", - ) - - # 搜索 - search = self.request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(display_name__icontains=search) - | Q(host__name__icontains=search) - | Q(created_by__username__icontains=search) - ) - - # 可用状态筛选 - available_filter = self.request.GET.get("is_available", "").strip() - if available_filter: - queryset = queryset.filter(is_available=available_filter == "true") - - # 可见性筛选 - visibility_filter = self.request.GET.get("visibility", "").strip() - if visibility_filter: - queryset = queryset.filter(visibility=visibility_filter) - - # 排序 - queryset = queryset.order_by("-created_at") - - # 分页 - paginator = Paginator(queryset, 20) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context.update( - { - "page_obj": page_obj, - "products": page_obj, - "search": search, - "available_filter": available_filter, - "visibility_filter": visibility_filter, - "visibility_choices": Product._meta.get_field("visibility").choices, - "page_title": "产品管理", - "active_nav": "products", - } - ) - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminProductCreateView(TemplateView): - """ - 超管产品创建视图 - - 处理 GET 和 POST 请求,创建新产品。 - """ - - template_name = "admin_base/operations/product_form.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - site_group = getattr(self.request, "site_group", None) - context.update( - { - "form": kwargs.get( - "form", - AdminProductForm(user=self.request.user, site_group=site_group), - ), - "page_title": "创建产品", - "active_nav": "products", - "is_create": True, - "existing_disk_quota": kwargs.get("existing_disk_quota", "{}"), - "existing_extra_disks": kwargs.get("existing_extra_disks", "[]"), - "initial_host_id": kwargs.get("initial_host_id", '""'), - "enable_disk_quota_initial": kwargs.get( - "enable_disk_quota_initial", "false" - ), - } - ) - return context - - def post(self, request, *args, **kwargs): - site_group = getattr(request, "site_group", None) - form = AdminProductForm(request.POST, user=request.user, site_group=site_group) - if form.is_valid(): - product = form.save(commit=False) - product.created_by = request.user - if site_group: - product.site_group = site_group - product.save() - messages.success( - request, - f"产品 {product.display_name} 创建成功", - ) - return redirect("admin:admin_operations:product_list") - - return self.render_to_response( - self.get_context_data( - form=form, - existing_disk_quota=request.POST.get("default_disk_quota", "{}"), - existing_extra_disks=request.POST.get("allow_extra_quota_disks", "[]"), - initial_host_id=json.dumps(request.POST.get("host", "")), - enable_disk_quota_initial=json.dumps( - "enable_disk_quota" in request.POST - ), - ) - ) - - -# ========== 产品创建向导 ========== - - -@admin_required -def admin_product_wizard(request): - """ - 产品创建向导视图 - - 引导超管分步创建产品: - - Step 1: 基本信息 (显示名称、描述、产品组) - - Step 2: 主机关联与配置 (主机、显示地址、RDP端口、可见性、状态) - - Step 3: 高级设置 (主机保护、磁盘配额、创建预览) - - 使用 Alpine.js 在客户端管理步骤切换, - 最终一次性提交表单创建产品。 - """ - from .forms_wizard import ProductWizardForm - - site_group = getattr(request, "site_group", None) - - if request.method == "POST": - form = ProductWizardForm(request.POST, user=request.user, site_group=site_group) - if form.is_valid(): - product = form.save(commit=False) - product.created_by = request.user - if site_group: - product.site_group = site_group - product.save() - - messages.success( - request, - f"产品 {product.display_name} 创建成功", - ) - return redirect( - "admin:admin_operations:product_edit", - pk=product.pk, - ) - else: - form = ProductWizardForm(user=request.user, site_group=site_group) - - hosts_info = form.get_hosts_info() - - context = { - "form": form, - "hosts_info": json.dumps(hosts_info), - "visibility_choices": Product._meta.get_field("visibility").choices, - "page_title": "创建产品", - "active_nav": "products", - } - - return render( - request, - "admin_base/operations/product_wizard.html", - context, - ) - - -@method_decorator(admin_required, name="dispatch") -class AdminProductUpdateView(TemplateView): - """ - 超管产品编辑视图 - - 处理 GET 和 POST 请求,编辑产品信息。 - """ - - template_name = "admin_base/operations/product_form.html" - - def get_product(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404( - Product.objects.select_related("host", "product_group", "created_by"), - pk=self.kwargs["pk"], - ) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - Product.objects.filter(site_group=site_group).select_related( - "host", "product_group", "created_by" - ), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - Product.objects.none(), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - get_provider_products( - self.request.user, site_group=site_group - ).select_related("host", "product_group", "created_by"), - pk=self.kwargs["pk"], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - product = self.get_product() - site_group = getattr(self.request, "site_group", None) - form = kwargs.get( - "form", - AdminProductForm( - instance=product, user=self.request.user, site_group=site_group - ), - ) - context.update( - { - "form": form, - "product": product, - "page_title": f"编辑产品 - {product.display_name}", - "active_nav": "products", - "is_create": False, - "existing_disk_quota": kwargs.get( - "existing_disk_quota", - json.dumps(product.default_disk_quota or {}), - ), - "existing_extra_disks": kwargs.get( - "existing_extra_disks", - json.dumps(product.allow_extra_quota_disks or []), - ), - "initial_host_id": kwargs.get( - "initial_host_id", - json.dumps(product.host_id), - ), - "enable_disk_quota_initial": kwargs.get( - "enable_disk_quota_initial", - json.dumps(product.enable_disk_quota), - ), - } - ) - return context - - def post(self, request, *args, **kwargs): - product = self.get_product() - site_group = getattr(request, "site_group", None) - form = AdminProductForm( - request.POST, instance=product, user=request.user, site_group=site_group - ) - if form.is_valid(): - product = form.save() - messages.success( - request, - f"产品 {product.display_name} 更新成功", - ) - return redirect("admin:admin_operations:product_list") - - return self.render_to_response( - self.get_context_data( - form=form, - existing_disk_quota=request.POST.get("default_disk_quota", "{}"), - existing_extra_disks=request.POST.get("allow_extra_quota_disks", "[]"), - initial_host_id=json.dumps(request.POST.get("host", "")), - enable_disk_quota_initial=json.dumps( - "enable_disk_quota" in request.POST - ), - ) - ) - - -@method_decorator(admin_required, name="dispatch") -class AdminProductDeleteView(TemplateView): - """ - 超管产品删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = "admin_base/operations/product_confirm_delete.html" - - def get_product(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(Product, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - Product.objects.filter(site_group=site_group), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - Product.objects.none(), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - get_provider_products(self.request.user, site_group=site_group), - pk=self.kwargs["pk"], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - product = self.get_product() - - # 获取关联用户数 - user_count = CloudComputerUser.objects.filter(product=product).count() - - context.update( - { - "product": product, - "user_count": user_count, - "page_title": f"删除产品 - {product.display_name}", - "active_nav": "products", - } - ) - return context - - def post(self, request, *args, **kwargs): - product = self.get_product() - product_name = product.display_name - product.delete() - - messages.success( - request, - f"产品 {product_name} 已删除", - ) - return redirect("admin:admin_operations:product_list") - - -# =========================================================================== -# 产品组管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminProductGroupListView(TemplateView): - """ - 超管产品组列表视图 - - - 查看所有产品组(无数据隔离) - - 搜索、分页 - """ - - template_name = "admin_base/operations/productgroup_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - queryset = ProductGroup.objects.select_related( - "created_by", - ) - elif self.request.user.is_site_group_admin(site_group): - if site_group: - queryset = ProductGroup.objects.filter( - site_group=site_group - ).select_related("created_by") - else: - queryset = ProductGroup.objects.none() - else: - queryset = ProductGroup.objects.filter( - created_by=self.request.user - ).select_related("created_by") - - # 搜索 - search = self.request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(name__icontains=search) | Q(description__icontains=search) - ) - - # 排序 - queryset = queryset.order_by("display_order", "name") - - # 分页 - paginator = Paginator(queryset, 20) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context.update( - { - "page_obj": page_obj, - "productgroups": page_obj, - "search": search, - "page_title": "产品组管理", - "active_nav": "productgroups", - } - ) - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminProductGroupCreateView(TemplateView): - """ - 超管产品组创建视图 - - 处理 GET 和 POST 请求,创建新产品组。 - """ - - template_name = "admin_base/operations/productgroup_form.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update( - { - "form": kwargs.get("form", AdminProductGroupForm()), - "page_title": "创建产品组", - "active_nav": "productgroups", - "is_create": True, - } - ) - return context - - def post(self, request, *args, **kwargs): - form = AdminProductGroupForm(request.POST) - if form.is_valid(): - productgroup = form.save(commit=False) - productgroup.created_by = request.user - site_group = getattr(request, "site_group", None) - if site_group: - productgroup.site_group = site_group - productgroup.save() - messages.success( - request, - f"产品组 {productgroup.name} 创建成功", - ) - return redirect("admin:admin_operations:productgroup_list") - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminProductGroupUpdateView(TemplateView): - """ - 超管产品组编辑视图 - - 处理 GET 和 POST 请求,编辑产品组信息。 - """ - - template_name = "admin_base/operations/productgroup_form.html" - - def get_productgroup(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(ProductGroup, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - ProductGroup.objects.filter(site_group=site_group), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - ProductGroup.objects.none(), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - ProductGroup, - pk=self.kwargs["pk"], - created_by=self.request.user, - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - productgroup = self.get_productgroup() - form = kwargs.get( - "form", - AdminProductGroupForm(instance=productgroup), - ) - context.update( - { - "form": form, - "productgroup": productgroup, - "page_title": f"编辑产品组 - {productgroup.name}", - "active_nav": "productgroups", - "is_create": False, - } - ) - return context - - def post(self, request, *args, **kwargs): - productgroup = self.get_productgroup() - form = AdminProductGroupForm(request.POST, instance=productgroup) - if form.is_valid(): - productgroup = form.save() - messages.success( - request, - f"产品组 {productgroup.name} 更新成功", - ) - return redirect("admin:admin_operations:productgroup_list") - - return self.render_to_response(self.get_context_data(form=form)) - - -@method_decorator(admin_required, name="dispatch") -class AdminProductGroupDeleteView(TemplateView): - """ - 超管产品组删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = "admin_base/operations/productgroup_confirm_delete.html" - - def get_productgroup(self): - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - return get_object_or_404(ProductGroup, pk=self.kwargs["pk"]) - if self.request.user.is_site_group_admin(site_group): - if site_group: - return get_object_or_404( - ProductGroup.objects.filter(site_group=site_group), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - ProductGroup.objects.none(), - pk=self.kwargs["pk"], - ) - return get_object_or_404( - ProductGroup, - pk=self.kwargs["pk"], - created_by=self.request.user, - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - productgroup = self.get_productgroup() - - # 获取关联产品数 - product_count = Product.objects.filter(product_group=productgroup).count() - - context.update( - { - "productgroup": productgroup, - "product_count": product_count, - "page_title": f"删除产品组 - {productgroup.name}", - "active_nav": "productgroups", - } - ) - return context - - def post(self, request, *args, **kwargs): - productgroup = self.get_productgroup() - productgroup_name = productgroup.name - productgroup.delete() - - messages.success( - request, - f"产品组 {productgroup_name} 已删除", - ) - return redirect("admin:admin_operations:productgroup_list") - - -# =========================================================================== -# 开户申请管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestListView(ListView): - """ - 超管开户申请列表视图 - - - 查看所有开户申请(无数据隔离) - - 状态筛选、搜索、批量操作 - """ - - model = AccountOpeningRequest - template_name = "admin_base/operations/request_list.html" - context_object_name = "requests" - paginate_by = 20 - - def get_queryset(self): - qs = AccountOpeningRequest.objects.select_related( - "applicant", - "target_product", - "target_product__host", - "approved_by", - ) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(target_product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - qs = qs.filter(target_product__in=provider_products) - - # 状态筛选 - status = self.request.GET.get("status", "").strip() - if status: - qs = qs.filter(status=status) - - # 搜索 - search = self.request.GET.get("search", "").strip() - if search: - qs = qs.filter( - Q(username__icontains=search[:50]) - | Q(user_fullname__icontains=search[:50]) - | Q(contact_email__icontains=search[:50]) - | Q(applicant__username__icontains=search[:50]) - ) - - return qs.order_by("-created_at") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["status_choices"] = AccountOpeningRequest._meta.get_field( - "status" - ).choices - context["current_status"] = self.request.GET.get("status", "") - context["current_search"] = self.request.GET.get("search", "") - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - base_qs = AccountOpeningRequest.objects.all() - elif self.request.user.is_site_group_admin(site_group): - if site_group: - base_qs = AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ) - else: - base_qs = AccountOpeningRequest.objects.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - base_qs = AccountOpeningRequest.objects.filter( - target_product__in=provider_products - ) - counts = { - "pending": base_qs.filter(status="pending").count(), - "approved": base_qs.filter(status="approved").count(), - "rejected": base_qs.filter(status="rejected").count(), - "processing": base_qs.filter(status="processing").count(), - "completed": base_qs.filter(status="completed").count(), - "failed": base_qs.filter(status="failed").count(), - } - context["status_choices_with_counts"] = [ - (v, l, counts.get(v, 0)) for v, l in context["status_choices"] - ] - context["total_count"] = base_qs.count() - context["page_title"] = "开户申请" - context["active_nav"] = "requests" - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestDetailView(DetailView): - """ - 超管开户申请详情视图 - - 展示申请完整信息、状态时间线,以及批准/驳回按钮。 - """ - - model = AccountOpeningRequest - template_name = "admin_base/operations/request_detail.html" - context_object_name = "request_obj" - - def get_queryset(self): - qs = AccountOpeningRequest.objects.select_related( - "applicant", - "target_product", - "target_product__host", - "approved_by", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(target_product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - qs = qs.filter(target_product__in=provider_products) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - obj = self.object - - # 构建状态时间线 - timeline = [] - timeline.append( - { - "label": "提交申请", - "time": obj.created_at, - "done": True, - } - ) - if obj.status in ( - "approved", - "rejected", - "processing", - "completed", - "failed", - ): - timeline.append( - { - "label": "审核完成", - "time": obj.approval_date, - "done": (obj.status != "failed" or obj.approval_date is not None), - "detail": ("批准" if obj.status != "rejected" else "驳回"), - } - ) - if obj.status in ("processing", "completed", "failed"): - timeline.append( - { - "label": "执行开户", - "time": (obj.updated_at if obj.status == "completed" else None), - "done": obj.status in ("completed",), - "detail": (obj.result_message if obj.result_message else None), - } - ) - context["timeline"] = timeline - context["reject_form"] = AdminRequestRejectForm() - context["page_title"] = "申请详情" - context["active_nav"] = "requests" - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestApproveView(View): - """超管批准单条开户申请 (POST)""" - - def post(self, request, pk): - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - obj = get_object_or_404(AccountOpeningRequest, pk=pk) - elif request.user.is_site_group_admin(site_group): - if site_group: - obj = get_object_or_404( - AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ), - pk=pk, - ) - else: - obj = get_object_or_404( - AccountOpeningRequest.objects.none(), - pk=pk, - ) - else: - provider_products = get_provider_products( - request.user, site_group=site_group - ) - obj = get_object_or_404( - AccountOpeningRequest, - pk=pk, - target_product__in=provider_products, - ) - if obj.status != "pending": - messages.warning( - request, - f"申请 {obj.username} 当前状态为" - f" {obj.get_status_display()},无法批准。", - ) - return redirect("admin:admin_operations:request_detail", pk=obj.pk) - - obj.approve(approver=request.user, notes="") - messages.success(request, f"已批准申请 {obj.username}。") - return redirect("admin:admin_operations:request_detail", pk=obj.pk) - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestRejectView(View): - """ - 超管驳回单条开户申请 - - POST: 提交驳回(含驳回原因) - """ - - def post(self, request, pk): - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - obj = get_object_or_404(AccountOpeningRequest, pk=pk) - elif request.user.is_site_group_admin(site_group): - if site_group: - obj = get_object_or_404( - AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ), - pk=pk, - ) - else: - obj = get_object_or_404( - AccountOpeningRequest.objects.none(), - pk=pk, - ) - else: - provider_products = get_provider_products( - request.user, site_group=site_group - ) - obj = get_object_or_404( - AccountOpeningRequest, - pk=pk, - target_product__in=provider_products, - ) - if obj.status != "pending": - messages.warning( - request, - f"申请 {obj.username} 当前状态为" - f" {obj.get_status_display()},无法驳回。", - ) - return redirect("admin:admin_operations:request_detail", pk=obj.pk) - - form = AdminRequestRejectForm(request.POST) - if form.is_valid(): - reason = form.cleaned_data["rejection_reason"] - obj.reject(approver=request.user, notes=reason) - messages.success(request, f"已驳回申请 {obj.username}。") - return redirect("admin:admin_operations:request_detail", pk=obj.pk) - - messages.error(request, "请输入驳回原因。") - return redirect("admin:admin_operations:request_detail", pk=obj.pk) - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestRetryView(View): - """ - 超管重试失败的开户申请 - - POST: 将失败状态的申请重新触发开户流程。 - """ - - def post(self, request, pk): - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - obj = get_object_or_404(AccountOpeningRequest, pk=pk) - elif request.user.is_site_group_admin(site_group): - if site_group: - obj = get_object_or_404( - AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ), - pk=pk, - ) - else: - obj = get_object_or_404( - AccountOpeningRequest.objects.none(), - pk=pk, - ) - else: - provider_products = get_provider_products( - request.user, site_group=site_group - ) - obj = get_object_or_404( - AccountOpeningRequest, - pk=pk, - target_product__in=provider_products, - ) - if obj.status != "failed": - messages.warning( - request, - f"申请 {obj.username} 当前状态为" - f" {obj.get_status_display()},无法重试。", - ) - return redirect( - "admin:admin_operations:request_detail", - pk=obj.pk, - ) - - success = obj.retry(operator=request.user) - if success: - messages.success( - request, - f"申请 {obj.username} 已重新提交处理" - f"(第{obj.retry_count}次重试)。", - ) - else: - messages.error( - request, - f"申请 {obj.username} 重试失败,请稍后再试。", - ) - return redirect( - "admin:admin_operations:request_detail", - pk=obj.pk, - ) - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestBatchApproveView(View): - """ - 超管批量批准开户申请 (POST) - - 请求体需包含 selected_ids。 - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, "未选择任何申请。") - return redirect("admin:admin_operations:request_list") - - qs = AccountOpeningRequest.objects.filter( - pk__in=selected_ids, - status="pending", - ) - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - pass - elif request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(target_product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - request.user, site_group=site_group - ) - qs = qs.filter(target_product__in=provider_products) - updated_count = 0 - for obj in qs: - obj.approve(approver=request.user, notes="") - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f"成功批准了 {updated_count} 个开户申请。", - ) - else: - messages.warning( - request, - "没有符合条件的待审核申请需要批准。", - ) - - return redirect("admin:admin_operations:request_list") - - -@method_decorator(admin_required, name="dispatch") -class AdminRequestBatchRejectView(View): - """ - 超管批量驳回开户申请 (POST) - - 请求体需包含 selected_ids 和可选的 rejection_reason。 - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, "未选择任何申请。") - return redirect("admin:admin_operations:request_list") - - rejection_reason = request.POST.get( - "rejection_reason", - "批量驳回", - ) - - qs = AccountOpeningRequest.objects.filter( - pk__in=selected_ids, - status="pending", - ) - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - pass - elif request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(target_product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - request.user, site_group=site_group - ) - qs = qs.filter(target_product__in=provider_products) - updated_count = 0 - for obj in qs: - obj.reject(approver=request.user, notes=rejection_reason) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f"成功驳回了 {updated_count} 个开户申请。", - ) - else: - messages.warning( - request, - "没有符合条件的待审核申请需要驳回。", - ) - - return redirect("admin:admin_operations:request_list") - - -# =========================================================================== -# 云电脑用户管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminCloudUserListView(TemplateView): - """ - 超管云电脑用户列表视图 - - - 查看所有云电脑用户(无数据隔离) - - 搜索、状态筛选、产品筛选 - """ - - template_name = "admin_base/operations/user_list.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - queryset = CloudComputerUser.objects.select_related( - "product", - "product__host", - "created_from_request", - "created_from_request__applicant", - "owner", - ) - elif self.request.user.is_site_group_admin(site_group): - if site_group: - queryset = CloudComputerUser.objects.filter( - product__site_group=site_group - ).select_related( - "product", - "product__host", - "created_from_request", - "created_from_request__applicant", - "owner", - ) - else: - queryset = CloudComputerUser.objects.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - queryset = CloudComputerUser.objects.filter( - product__in=provider_products - ).select_related( - "product", - "product__host", - "created_from_request", - "created_from_request__applicant", - "owner", - ) - - # 搜索 - search = self.request.GET.get("search", "").strip() - if search: - q_filter = ( - Q(username__icontains=search) - | Q(fullname__icontains=search) - | Q(email__icontains=search) - | Q(product__display_name__icontains=search) - ) - queryset = queryset.filter(q_filter).distinct() - - # 状态筛选 - status_filter = self.request.GET.get("status", "").strip() - if status_filter: - queryset = queryset.filter(status=status_filter) - - # 产品筛选 - product_filter = self.request.GET.get("product", "").strip() - if product_filter: - queryset = queryset.filter(product_id=product_filter) - - # 排序 - queryset = queryset.order_by("-created_at") - - # 分页 - paginator = Paginator(queryset, 20) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - # 状态选项 - status_choices = CloudComputerUser._meta.get_field("status").choices - - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - products_for_filter = Product.objects.all().order_by("display_name") - elif self.request.user.is_site_group_admin(site_group): - if site_group: - products_for_filter = Product.objects.filter( - site_group=site_group - ).order_by("display_name") - else: - products_for_filter = Product.objects.none() - else: - products_for_filter = get_provider_products( - self.request.user, site_group=site_group - ).order_by("display_name") - - context.update( - { - "page_obj": page_obj, - "users": page_obj, - "search": search, - "status_filter": status_filter, - "product_filter": product_filter, - "status_choices": status_choices, - "products": products_for_filter, - "page_title": "云电脑用户", - "active_nav": "cloud_users", - } - ) - - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminCloudUserDetailView(DetailView): - """ - 超管云电脑用户详情视图 - - 显示用户信息、管理员状态、磁盘配额等 - """ - - model = CloudComputerUser - template_name = "admin_base/operations/user_detail.html" - context_object_name = "cloud_user" - - def get_queryset(self): - qs = CloudComputerUser.objects.select_related( - "product", - "product__host", - "created_from_request", - "created_from_request__applicant", - "owner", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - qs = qs.filter(product__in=provider_products) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - cloud_user = self.object - - context.update( - { - "page_title": f"用户详情 - {cloud_user.username}", - "active_nav": "cloud_users", - "disk_quota_json": ( - json.dumps(cloud_user.disk_quota, ensure_ascii=False) - if cloud_user.disk_quota - else "{}" - ), - } - ) - - return context - - -@admin_required -def admin_cloud_user_action(request, pk): - if request.method != "POST": - return JsonResponse( - {"success": False, "message": "仅支持 POST 请求"}, status=405 - ) - - qs = CloudComputerUser.objects.select_related("product", "product__host") - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - pass - elif request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products(request.user, site_group=site_group) - qs = qs.filter(product__in=provider_products) - - cloud_user = get_object_or_404(qs, pk=pk) - - if cloud_user.status == "deleted": - return JsonResponse({"success": False, "message": "已删除的用户无法执行操作"}) - - action = request.POST.get("action", "") - - if action == "disable": - cloud_user.disable() - cloud_user.refresh_from_db() - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 已封禁", - "status": cloud_user.status, - } - ) - - elif action == "enable": - if cloud_user.status != "disabled": - return JsonResponse({"success": False, "message": "仅已封禁的用户可以解封"}) - cloud_user.activate() - cloud_user.refresh_from_db() - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 已解封", - "status": cloud_user.status, - } - ) - - elif action == "delete": - cloud_user.delete_user() - cloud_user.refresh_from_db() - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 已删除", - "status": cloud_user.status, - } - ) - - elif action == "set_admin": - if cloud_user.is_admin: - return JsonResponse({"success": False, "message": "该用户已是管理员"}) - cloud_user.is_admin = True - cloud_user.save(update_fields=["is_admin", "updated_at"]) - from apps.operations.tasks import remote_set_admin - - remote_set_admin.delay(cloud_user.pk, operator_id=request.user.pk) - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 已设为管理员", - "is_admin": True, - } - ) - - elif action == "remove_admin": - if not cloud_user.is_admin: - return JsonResponse({"success": False, "message": "该用户不是管理员"}) - cloud_user.is_admin = False - cloud_user.save(update_fields=["is_admin", "updated_at"]) - from apps.operations.tasks import remote_remove_admin - - remote_remove_admin.delay(cloud_user.pk, operator_id=request.user.pk) - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 已取消管理员", - "is_admin": False, - } - ) - - elif action == "reset_password": - new_password = CloudComputerUser.generate_complex_password() - cloud_user.initial_password = new_password - cloud_user.password_viewed = False - cloud_user.password_viewed_at = None - cloud_user.save( - update_fields=[ - "initial_password", - "password_viewed", - "password_viewed_at", - "updated_at", - ] - ) - from apps.operations.tasks import remote_reset_windows_password - - remote_reset_windows_password.delay( - cloud_user.pk, - new_password, - operator_id=request.user.pk, - ) - return JsonResponse( - { - "success": True, - "message": f"用户 {cloud_user.username} 密码已重置", - "new_password": new_password, - } - ) - - else: - return JsonResponse({"success": False, "message": "无效的操作类型"}, status=400) - - -@admin_required -def admin_cloud_user_set_quota(request, pk): - if request.method != "POST": - return JsonResponse( - {"success": False, "message": "仅支持 POST 请求"}, status=405 - ) - - qs = CloudComputerUser.objects.select_related("product", "product__host") - site_group = getattr(request, "site_group", None) - if request.user.is_superuser: - pass - elif request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products(request.user, site_group=site_group) - qs = qs.filter(product__in=provider_products) - - cloud_user = get_object_or_404(qs, pk=pk) - - if not cloud_user.product.enable_disk_quota: - return JsonResponse({"success": False, "message": "该产品未启用磁盘配额管理"}) - - disk = request.POST.get("disk", "").strip().upper() - quota_str = request.POST.get("quota", "").strip() - - if not disk or not quota_str: - return JsonResponse({"success": False, "message": "磁盘盘符和配额值不能为空"}) - - try: - quota_mb = int(quota_str) - if quota_mb < 0: - return JsonResponse({"success": False, "message": "配额值不能为负数"}) - except (ValueError, TypeError): - return JsonResponse({"success": False, "message": "配额值必须为数字"}) - - import re - - if not re.match(r"^[A-Za-z]:\\?$", disk): - return JsonResponse({"success": False, "message": f"无效的磁盘盘符: {disk}"}) - disk = disk.rstrip("\\") - - new_quota = dict(cloud_user.disk_quota) if cloud_user.disk_quota else {} - new_quota[disk] = quota_mb - cloud_user.disk_quota = new_quota - cloud_user.save(update_fields=["disk_quota", "updated_at"]) - - try: - from apps.operations.tasks import remote_set_disk_quota - - remote_set_disk_quota.delay( - cloud_user.pk, - disk, - quota_mb, - operator_id=request.user.pk, - ) - except Exception as e: - logger.error(f"远程设置磁盘配额失败: {e}", exc_info=True) - return JsonResponse({"success": False, "message": "远程设置配额任务已提交"}) - - return JsonResponse( - { - "success": True, - "message": f"磁盘 {disk} 配额已设置为 {quota_mb} MB", - "disk": disk, - "quota": quota_mb, - } - ) - - -# =========================================================================== -# 邀请令牌管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminTokenListView(ListView): - """ - 超管邀请令牌列表视图 - - - 查看所有邀请令牌(无数据隔离) - - 显示邀请链接 - """ - - model = ProductInvitationToken - template_name = "admin_base/operations/token_list.html" - context_object_name = "tokens" - paginate_by = 20 - - def get_queryset(self): - qs = ProductInvitationToken.objects.select_related( - "product", - "product_group", - "created_by", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter( - product__in=Product.objects.filter(site_group=site_group) - ) - else: - qs = qs.none() - else: - qs = qs.filter(created_by=self.request.user) - return qs.order_by("-created_at") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["active_nav"] = "operations_tokens" - context["page_title"] = "邀请令牌" - - from django.conf import settings - - site_url = getattr(settings, "SITE_URL", "") - for token in context["tokens"]: - token.invite_link = f"{site_url}/operations/invite/{token.token}/" - return context - - -@method_decorator(admin_required, name="dispatch") -class AdminTokenDetailView(DetailView): - """ - 超管邀请令牌详情视图 - - - 查看令牌基本信息 - - 查看所有使用该令牌的用户列表(ProductAccessGrant) - """ - - model = ProductInvitationToken - template_name = "admin_base/operations/token_detail.html" - context_object_name = "token_obj" - - def get_queryset(self): - qs = ProductInvitationToken.objects.select_related( - "product", - "product_group", - "created_by", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter( - product__in=Product.objects.filter(site_group=site_group) - ) - else: - qs = qs.none() - else: - qs = qs.filter(created_by=self.request.user) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - token_obj = context["token_obj"] - context["active_nav"] = "operations_tokens" - context["page_title"] = "邀请令牌详情" - - from django.conf import settings - - site_url = getattr(settings, "SITE_URL", "") - token_obj.invite_link = f"{site_url}/operations/invite/{token_obj.token}/" - - grants = ( - ProductAccessGrant.objects.filter( - granted_by_token=token_obj, - ) - .select_related( - "user", - "product", - "product_group", - ) - .order_by("-granted_at") - ) - - paginator = Paginator(grants, 20) - page_number = self.request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - context["grants"] = page_obj - context["grant_count"] = grants.count() - context["effective_grant_count"] = ( - grants.filter( - is_revoked=False, - ) - .exclude( - expires_at__lt=timezone.now(), - ) - .count() - if grants.exists() - else 0 - ) - return context - - -# =========================================================================== -# 访问授权管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminGrantListView(ListView): - """ - 超管访问授权列表视图 - - - 查看所有访问授权(无数据隔离) - """ - - model = ProductAccessGrant - template_name = "admin_base/operations/grant_list.html" - context_object_name = "grants" - paginate_by = 20 - - def get_queryset(self): - qs = ProductAccessGrant.objects.select_related( - "user", - "product", - "product_group", - "granted_by_token", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - qs = qs.filter(product__in=provider_products) - return qs.order_by("-granted_at") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["active_nav"] = "grants" - context["page_title"] = "访问授权" - return context - - -# =========================================================================== -# RDP 域名路由管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminRouteListView(ListView): - """ - 超管RDP域名路由列表视图 - - - 查看所有域名路由(无数据隔离) - - 只读视图 - """ - - model = RdpDomainRoute - template_name = "admin_base/operations/route_list.html" - context_object_name = "routes" - paginate_by = 20 - - def get_queryset(self): - qs = RdpDomainRoute.objects.select_related( - "product", - "assigned_to", - ) - site_group = getattr(self.request, "site_group", None) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.none() - else: - provider_products = get_provider_products( - self.request.user, site_group=site_group - ) - qs = qs.filter(product__in=provider_products) - return qs.order_by("-created_at") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["active_nav"] = "operations_routes" - context["page_title"] = "域名路由" - return context - - -# =========================================================================== -# 系统任务管理 -# =========================================================================== - - -@method_decorator(admin_required, name="dispatch") -class AdminTaskListView(ListView): - """ - 超管系统任务列表视图 - - - 查看所有 Celery 异步任务(无数据隔离) - - 只读参考视图 - """ - - model = AsyncTask - template_name = "admin_base/operations/task_list.html" - context_object_name = "tasks" - paginate_by = 20 - - def get_queryset(self): - qs = AsyncTask.objects.select_related( - "created_by", - ).prefetch_related("progress_updates") - site_group = getattr(self.request, "site_group", None) - search = self.request.GET.get("search", "").strip() - if search: - qs = qs.filter( - Q(name__icontains=search) - | Q(target_content_type__icontains=search) - | Q(task_id__icontains=search) - ) - if self.request.user.is_superuser: - pass - elif self.request.user.is_site_group_admin(site_group): - if site_group: - from apps.hosts.models import Host - from apps.operations.models import Product - - sg_host_ids = set( - Host.objects.filter(site_group=site_group).values_list( - "pk", flat=True - ) - ) - sg_product_ids = set( - Product.objects.filter(site_group=site_group).values_list( - "pk", flat=True - ) - ) - qs = qs.filter( - Q( - target_content_type="hosts.Host", - target_object_id__in=sg_host_ids, - ) - | Q( - target_content_type="operations.Product", - target_object_id__in=sg_product_ids, - ) - | Q( - target_content_type="operations.AccountOpeningRequest", - target_object_id__in=AccountOpeningRequest.objects.filter( - target_product__site_group=site_group - ).values_list("pk", flat=True), - ) - ) - else: - qs = qs.filter(created_by=self.request.user) - else: - qs = qs.filter(created_by=self.request.user) - return qs.order_by("-created_at") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["active_nav"] = "tasks" - context["page_title"] = "系统任务" - context["search"] = self.request.GET.get("search", "") - return context diff --git a/apps/operations/views_provider.py b/apps/operations/views_provider.py deleted file mode 100644 index 4b7dfc7..0000000 --- a/apps/operations/views_provider.py +++ /dev/null @@ -1,1736 +0,0 @@ -""" -运营管理 - 提供商后台视图 - -包含开户申请、云电脑用户、邀请令牌、访问授权、RDP域名路由、系统任务、 -产品管理、产品组管理等视图。 -所有视图均受提供商身份验证保护,并实施提供商数据隔离。 -""" - -import json -import logging - -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.core.paginator import Paginator -from django.db.models import Q -from django.http import HttpResponseRedirect, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse -from django.views import View -from django.views.generic import DetailView, ListView, TemplateView - -from apps.operations.models import ( - AccountOpeningRequest, - CloudComputerUser, - Product, - ProductGroup, - ProductInvitationToken, - ProductAccessGrant, - RdpDomainRoute, - SystemTask, -) -from apps.provider.decorators import is_provider, provider_required -from apps.provider.context_mixin import ProviderContextMixin -from utils.provider import get_provider_products - -from .forms_provider import ( - AccountOpeningRequestRejectForm, - CloudComputerUserDiskQuotaForm, - CloudComputerUserResetPasswordForm, - ProductForm, - ProductGroupForm, -) - -logger = logging.getLogger(__name__) -User = get_user_model() - - -# ========== 基础混入类 ========== - - -class ProviderOperationBaseView(View): - """ - 提供商运营管理基础视图混入类 - - - 验证提供商身份 - - 提供数据隔离的查询集 - """ - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - if not is_provider(request.user): - return redirect('accounts:login') - return super().dispatch(request, *args, **kwargs) - - def get_provider_queryset(self): - """ - 获取提供商可见的云电脑用户查询集 - 提供商只能看到自己产品下的用户 - """ - return CloudComputerUser.objects.filter( - product__created_by=self.request.user - ).select_related( - 'product', - 'product__host', - 'created_from_request', - 'created_from_request__applicant', - ) - - def get_provider_products(self): - """获取提供商创建的产品""" - return Product.objects.filter(created_by=self.request.user) - - -# ========== 用户列表视图 ========== - - -class CloudComputerUserListView(ProviderContextMixin, ProviderOperationBaseView, TemplateView): - """ - 云电脑用户列表视图 - - 支持搜索、状态筛选、分页、批量操作 - """ - template_name = 'admin_base/operations/user_list.html' - paginate_by = 20 - provider_url_namespace = 'provider:provider_operations' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - queryset = self.get_provider_queryset() - - # 搜索过滤 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - username__icontains=search - ) | queryset.filter( - fullname__icontains=search - ) | queryset.filter( - email__icontains=search - ) | queryset.filter( - product__display_name__icontains=search - ) - # 去重 - queryset = queryset.distinct() - - # 状态过滤 - status_filter = self.request.GET.get('status', '').strip() - if status_filter: - queryset = queryset.filter(status=status_filter) - - # 产品过滤 - product_filter = self.request.GET.get('product', '').strip() - if product_filter: - queryset = queryset.filter(product_id=product_filter) - - # 排序 - queryset = queryset.order_by('-created_at') - - # 分页 - paginator = Paginator(queryset, self.paginate_by) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - # 状态选项 - status_choices = CloudComputerUser._meta.get_field('status').choices - - context.update({ - 'page_obj': page_obj, - 'users': page_obj, - 'search': search, - 'status_filter': status_filter, - 'product_filter': product_filter, - 'status_choices': status_choices, - 'products': self.get_provider_products(), - 'page_title': '云电脑用户', - 'active_nav': 'cloud_users', - }) - - return context - - -# ========== 用户详情视图 ========== - - -class CloudComputerUserDetailView(ProviderContextMixin, ProviderOperationBaseView, DetailView): - """ - 云电脑用户详情视图 - - 显示用户信息、管理员状态、磁盘配额等 - """ - template_name = 'admin_base/operations/user_detail.html' - context_object_name = 'cloud_user' - pk_url_kwarg = 'pk' - provider_url_namespace = 'provider:provider_operations' - - def get_queryset(self): - return self.get_provider_queryset() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - cloud_user = self.object - - context.update({ - 'page_title': f'用户详情 - {cloud_user.username}', - 'active_nav': 'cloud_users', - 'disk_quota_json': json.dumps(cloud_user.disk_quota, ensure_ascii=False) if cloud_user.disk_quota else '{}', - }) - - return context - - -# ========== 同步管理员状态视图 ========== - - -class CloudComputerUserSyncAdminView(ProviderOperationBaseView, View): - """ - 同步管理员状态视图 - - POST 请求:切换用户的管理员权限(授予/撤销) - """ - - def get_queryset(self): - return self.get_provider_queryset() - - def post(self, request, pk): - cloud_user = get_object_or_404(self.get_queryset(), pk=pk) - - try: - from .services import update_user_admin_permission - - new_is_admin = not cloud_user.is_admin - update_user_admin_permission(cloud_user, new_is_admin) - - # 更新数据库 - cloud_user.is_admin = new_is_admin - cloud_user.save(update_fields=['is_admin', 'updated_at']) - - action = '授予' if new_is_admin else '撤销' - messages.success( - request, - f'成功{action}用户 {cloud_user.username} 的管理员权限' - ) - except Exception as e: - action = '授予' if not cloud_user.is_admin else '撤销' - messages.error( - request, - f'{action}用户 {cloud_user.username} 的管理员权限失败: {str(e)}' - ) - - return HttpResponseRedirect( - reverse('provider_operations:user_detail', kwargs={'pk': pk}) - ) - - -# ========== 设置磁盘配额视图 ========== - - -class CloudComputerUserSetDiskQuotaView(ProviderContextMixin, ProviderOperationBaseView, View): - """ - 设置磁盘配额视图 - - GET 请求:显示配额设置表单 - POST 请求:提交配额设置并远程执行 - """ - provider_url_namespace = 'provider:provider_operations' - - def get_queryset(self): - return self.get_provider_queryset() - - def get(self, request, pk): - cloud_user = get_object_or_404(self.get_queryset(), pk=pk) - initial_data = { - 'disk_quota': json.dumps(cloud_user.disk_quota, ensure_ascii=False, indent=2) if cloud_user.disk_quota else '{}', - } - form = CloudComputerUserDiskQuotaForm(initial=initial_data) - - return render(request, 'admin_base/operations/user_disk_quota.html', { - 'form': form, - 'cloud_user': cloud_user, - 'page_title': f'设置磁盘配额 - {cloud_user.username}', - 'active_nav': 'cloud_users', - }) - - def post(self, request, pk): - cloud_user = get_object_or_404(self.get_queryset(), pk=pk) - form = CloudComputerUserDiskQuotaForm(request.POST) - - if form.is_valid(): - disk_quota = form.cleaned_data['disk_quota'] - - try: - # 更新数据库 - cloud_user.disk_quota = disk_quota - cloud_user.save(update_fields=['disk_quota', 'updated_at']) - - # 远程设置磁盘配额 - if disk_quota and cloud_user.product.enable_disk_quota: - from apps.operations.tasks import remote_set_user_disk_quotas - remote_set_user_disk_quotas.delay( - cloud_user.pk, disk_quota, - operator_id=request.user.pk, - ) - messages.success( - request, - f'已保存用户 {cloud_user.username} 的磁盘配额配置,' - f'远程设置正在后台执行' - ) - else: - messages.success( - request, - f'已保存用户 {cloud_user.username} 的磁盘配额配置' - ) - - return HttpResponseRedirect( - reverse('provider_operations:user_detail', kwargs={'pk': pk}) - ) - - except Exception as e: - messages.error( - request, - f'设置用户 {cloud_user.username} 磁盘配额失败: {str(e)}' - ) - else: - messages.error(request, '表单数据无效,请检查后重试') - - return render(request, 'admin_base/operations/user_disk_quota.html', { - 'form': form, - 'cloud_user': cloud_user, - 'page_title': f'设置磁盘配额 - {cloud_user.username}', - 'active_nav': 'cloud_users', - }) - - -# ========== 重置密码视图 ========== - - -class CloudComputerUserResetPasswordView(ProviderContextMixin, ProviderOperationBaseView, View): - """ - 重置密码视图 - - GET 请求:显示密码重置表单 - POST 请求:提交新密码并远程执行 - """ - provider_url_namespace = 'provider:provider_operations' - - def get_queryset(self): - return self.get_provider_queryset() - - def get(self, request, pk): - cloud_user = get_object_or_404(self.get_queryset(), pk=pk) - form = CloudComputerUserResetPasswordForm() - - return render(request, 'admin_base/operations/user_reset_password.html', { - 'form': form, - 'cloud_user': cloud_user, - 'page_title': f'重置密码 - {cloud_user.username}', - 'active_nav': 'cloud_users', - }) - - def post(self, request, pk): - cloud_user = get_object_or_404(self.get_queryset(), pk=pk) - form = CloudComputerUserResetPasswordForm(request.POST) - - if form.is_valid(): - new_password = form.cleaned_data['new_password'] - - try: - cloud_user.reset_windows_password(new_password) - messages.success( - request, - f'成功重置用户 {cloud_user.username} 的密码' - ) - return HttpResponseRedirect( - reverse('provider_operations:user_detail', kwargs={'pk': pk}) - ) - except Exception as e: - messages.error( - request, - f'重置用户 {cloud_user.username} 的密码失败: {str(e)}' - ) - else: - messages.error(request, '表单数据无效,请检查后重试') - - return render(request, 'admin_base/operations/user_reset_password.html', { - 'form': form, - 'cloud_user': cloud_user, - 'page_title': f'重置密码 - {cloud_user.username}', - 'active_nav': 'cloud_users', - }) - - -# ========== 批量操作视图 ========== - - -class CloudComputerUserBatchActivateView(ProviderOperationBaseView, View): - """ - 批量激活用户视图 - - POST 请求:批量激活选中的用户 - """ - - def post(self, request): - user_ids = request.POST.getlist('selected_ids') - - if not user_ids: - messages.warning(request, '未选择任何用户') - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - queryset = self.get_provider_queryset().filter( - pk__in=user_ids, - status__in=['inactive', 'disabled'], - ) - - updated_count = queryset.update(status='active') - - if updated_count > 0: - messages.success(request, f'成功激活了 {updated_count} 个用户') - else: - messages.warning(request, '没有符合条件的用户需要激活') - - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - -class CloudComputerUserBatchDeactivateView(ProviderOperationBaseView, View): - """ - 批量停用用户视图 - - POST 请求:批量停用选中的用户 - """ - - def post(self, request): - user_ids = request.POST.getlist('selected_ids') - - if not user_ids: - messages.warning(request, '未选择任何用户') - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - queryset = self.get_provider_queryset().filter( - pk__in=user_ids, - status='active', - ) - - updated_count = queryset.update(status='inactive') - - if updated_count > 0: - messages.success(request, f'成功停用了 {updated_count} 个用户') - else: - messages.warning(request, '没有符合条件的用户需要停用') - - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - -class CloudComputerUserBatchDisableView(ProviderOperationBaseView, View): - """ - 批量禁用用户视图 - - POST 请求:批量禁用选中的用户 - """ - - def post(self, request): - user_ids = request.POST.getlist('selected_ids') - - if not user_ids: - messages.warning(request, '未选择任何用户') - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - queryset = self.get_provider_queryset().filter( - pk__in=user_ids, - ).exclude(status='deleted') - - updated_count = queryset.update(status='disabled') - - if updated_count > 0: - messages.success(request, f'成功禁用了 {updated_count} 个用户') - else: - messages.warning(request, '没有符合条件的用户需要禁用') - - return HttpResponseRedirect(reverse('provider_operations:user_list')) - - -# =========================================================================== -# 共享辅助函数 -# =========================================================================== - - -def _get_selected_ids(request): - """ - 从 POST 请求中提取选中的 ID 列表 - - 支持两种格式: - 1. 表单字段: selected_ids=1&selected_ids=2 - 2. JSON 字符串: selected_ids=[1,2,3] - """ - ids = request.POST.getlist('selected_ids') - if ids: - return [int(i) for i in ids if i.strip().isdigit()] - - raw = request.POST.get('selected_ids', '') - if raw: - try: - parsed = json.loads(raw) - if isinstance(parsed, list): - return [ - int(i) for i in parsed - if isinstance(i, (int, str)) - and str(i).isdigit() - ] - except (json.JSONDecodeError, ValueError): - pass - - return [] - - -# =========================================================================== -# 开户申请管理 -# =========================================================================== - - -class ProviderRequestMixin(ProviderContextMixin): - """ - 提供商开户申请数据隔离 Mixin - - - dispatch: 验证提供商身份 - - get_queryset: 限制为当前提供商产品下的申请 - """ - - provider_url_namespace = 'provider:provider_operations' - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - if not is_provider(request.user): - from django.http import HttpResponseForbidden - return HttpResponseForbidden( - '您没有提供商权限,无法访问此页面。' - ) - return super().dispatch(request, *args, **kwargs) - - def get_provider_queryset(self): - """获取当前提供商可见的开户申请查询集""" - return AccountOpeningRequest.objects.filter( - target_product__created_by=self.request.user - ).select_related( - 'applicant', 'target_product', - 'target_product__host', 'approved_by', - ) - - def get_queryset(self): - return self.get_provider_queryset() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'account_opening' - context['page_title'] = '开户申请' - return context - - -class AccountOpeningRequestListView(ProviderRequestMixin, ListView): - """ - 开户申请列表视图 - - - 分页展示 - - 状态筛选 - - 搜索 - - 批量操作(批准 / 驳回) - """ - - model = AccountOpeningRequest - template_name = 'admin_base/operations/request_list.html' - context_object_name = 'requests' - paginate_by = 20 - - def get_queryset(self): - qs = super().get_queryset() - - # 状态筛选 - status = self.request.GET.get('status', '').strip() - if status: - qs = qs.filter(status=status) - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - qs = qs.filter( - Q( - username__icontains=search[:50], - user_fullname__icontains=search[:50], - contact_email__icontains=search[:50], - applicant__username__icontains=search[:50], - ) - ) - - return qs.order_by('-created_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['status_choices'] = ( - AccountOpeningRequest._meta.get_field( - 'status' - ).choices - ) - context['current_status'] = ( - self.request.GET.get('status', '') - ) - context['current_search'] = ( - self.request.GET.get('search', '') - ) - base_qs = self.get_provider_queryset() - counts = { - 'pending': base_qs.filter( - status='pending' - ).count(), - 'approved': base_qs.filter( - status='approved' - ).count(), - 'rejected': base_qs.filter( - status='rejected' - ).count(), - 'processing': base_qs.filter( - status='processing' - ).count(), - 'completed': base_qs.filter( - status='completed' - ).count(), - 'failed': base_qs.filter( - status='failed' - ).count(), - } - context['status_choices_with_counts'] = [ - (v, l, counts.get(v, 0)) - for v, l in context['status_choices'] - ] - context['total_count'] = base_qs.count() - return context - - -class AccountOpeningRequestDetailView(ProviderRequestMixin, DetailView): - """ - 开户申请详情视图 - - 展示申请完整信息、状态时间线,以及批准/驳回/执行开户按钮。 - """ - - model = AccountOpeningRequest - template_name = 'admin_base/operations/request_detail.html' - context_object_name = 'request_obj' - - def get_queryset(self): - return self.get_provider_queryset() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - obj = self.object - # 构建状态时间线 - timeline = [] - timeline.append({ - 'label': '提交申请', - 'time': obj.created_at, - 'done': True, - }) - if obj.status in ( - 'approved', 'rejected', 'processing', - 'completed', 'failed', - ): - timeline.append({ - 'label': '审核完成', - 'time': obj.approval_date, - 'done': ( - obj.status != 'failed' - or obj.approval_date is not None - ), - 'detail': ( - '批准' - if obj.status != 'rejected' - else '驳回' - ), - }) - if obj.status in ('processing', 'completed', 'failed'): - timeline.append({ - 'label': '执行开户', - 'time': ( - obj.updated_at - if obj.status == 'completed' - else None - ), - 'done': obj.status in ('completed',), - 'detail': ( - obj.result_message - if obj.result_message - else None - ), - }) - context['timeline'] = timeline - context['reject_form'] = ( - AccountOpeningRequestRejectForm() - ) - return context - - -class AccountOpeningRequestApproveView(ProviderRequestMixin, View): - """批准单条开户申请 (POST)""" - - def post(self, request, pk): - obj = get_object_or_404( - self.get_provider_queryset(), pk=pk - ) - if obj.status != 'pending': - messages.warning( - request, - f'申请 {obj.username} 当前状态为' - f' {obj.get_status_display()},无法批准。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - obj.approve(approver=request.user, notes='') - messages.success( - request, f'已批准申请 {obj.username}。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - -class AccountOpeningRequestRejectView(ProviderRequestMixin, View): - """ - 驳回单条开户申请 - - GET: 展示驳回表单 - POST: 提交驳回(含驳回原因) - """ - - def get(self, request, pk): - obj = get_object_or_404( - self.get_provider_queryset(), pk=pk - ) - if obj.status != 'pending': - messages.warning( - request, - f'申请 {obj.username} 当前状态为' - f' {obj.get_status_display()},无法驳回。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - form = AccountOpeningRequestRejectForm() - return render( - request, - 'admin_base/operations/request_reject.html', - { - 'request_obj': obj, - 'form': form, - 'active_nav': 'account_opening', - 'page_title': '驳回申请', - }, - ) - - def post(self, request, pk): - obj = get_object_or_404( - self.get_provider_queryset(), pk=pk - ) - if obj.status != 'pending': - messages.warning( - request, - f'申请 {obj.username} 当前状态为' - f' {obj.get_status_display()},无法驳回。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - form = AccountOpeningRequestRejectForm(request.POST) - if form.is_valid(): - reason = form.cleaned_data['rejection_reason'] - obj.reject(approver=request.user, notes=reason) - messages.success( - request, f'已驳回申请 {obj.username}。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - return render( - request, - 'admin_base/operations/request_reject.html', - { - 'request_obj': obj, - 'form': form, - 'active_nav': 'account_opening', - 'page_title': '驳回申请', - }, - ) - - -class AccountOpeningRequestExecuteView(ProviderRequestMixin, View): - """ - 执行开户操作 (POST) - - 对已批准的申请执行实际的用户创建操作。 - """ - - def post(self, request, pk): - obj = get_object_or_404( - self.get_provider_queryset(), pk=pk - ) - if obj.status not in ('approved', 'pending'): - messages.warning( - request, - f'申请 {obj.username} 当前状态为' - f' {obj.get_status_display()},' - f'无法执行开户操作。', - ) - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - try: - from . import services - services.execute_account_opening(obj) - messages.success( - request, - f'申请 {obj.username} 开户操作已执行。', - ) - except Exception as e: - logger.error( - f'执行开户失败: {obj.username}, ' - f'错误: {str(e)}', - exc_info=True, - ) - messages.error( - request, - f'执行开户操作失败: {str(e)}', - ) - - return redirect( - 'provider_operations:request_detail', - pk=obj.pk, - ) - - -class AccountOpeningRequestBatchApproveView(ProviderRequestMixin, View): - """ - 批量批准开户申请 (POST) - - 请求体需包含 selected_ids。 - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何申请。') - return redirect('provider_operations:request_list') - - qs = self.get_provider_queryset().filter( - pk__in=selected_ids, status='pending', - ) - updated_count = 0 - for obj in qs: - obj.approve(approver=request.user, notes='') - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功批准了 {updated_count} 个开户申请。', - ) - else: - messages.warning( - request, - '没有符合条件的待审核申请需要批准。', - ) - - return redirect('provider_operations:request_list') - - -class AccountOpeningRequestBatchRejectView(ProviderRequestMixin, View): - """ - 批量驳回开户申请 (POST) - - 请求体需包含 selected_ids 和可选的 rejection_reason。 - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何申请。') - return redirect('provider_operations:request_list') - - rejection_reason = request.POST.get( - 'rejection_reason', '批量驳回', - ) - - qs = self.get_provider_queryset().filter( - pk__in=selected_ids, status='pending', - ) - updated_count = 0 - for obj in qs: - obj.reject( - approver=request.user, notes=rejection_reason, - ) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功驳回了 {updated_count} 个开户申请。', - ) - else: - messages.warning( - request, - '没有符合条件的待审核申请需要驳回。', - ) - - return redirect('provider_operations:request_list') - - -# =========================================================================== -# 邀请令牌管理 -# =========================================================================== - - -class ProductInvitationTokenListView(ProviderRequestMixin, ListView): - """ - 产品邀请令牌列表视图 - - - 提供商数据隔离:只看到自己创建的令牌 - - 支持批量启用/禁用 - - 显示邀请链接和复制按钮 - """ - - model = ProductInvitationToken - template_name = 'admin_base/operations/token_list.html' - context_object_name = 'tokens' - paginate_by = 20 - - def get_queryset(self): - return ProductInvitationToken.objects.filter( - created_by=self.request.user - ).select_related( - 'product', 'product_group', - ).order_by('-created_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'invitation_tokens' - context['page_title'] = '邀请令牌' - context['is_provider'] = True - - # 生成邀请链接 - from django.conf import settings - site_url = getattr(settings, 'SITE_URL', '') - for token in context['tokens']: - token.invite_link = ( - f'{site_url}/operations/invite/{token.token}/' - ) - return context - - -class ProductInvitationTokenDetailView(ProviderRequestMixin, DetailView): - """ - 产品邀请令牌详情视图 - - - 查看令牌基本信息 - - 查看所有使用该令牌的用户列表(ProductAccessGrant) - - 提供商数据隔离:只能查看自己创建的令牌 - """ - - model = ProductInvitationToken - template_name = 'admin_base/operations/token_detail.html' - context_object_name = 'token_obj' - - def get_queryset(self): - return ProductInvitationToken.objects.filter( - created_by=self.request.user, - ).select_related( - 'product', 'product_group', 'created_by', - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - token_obj = context['token_obj'] - context['active_nav'] = 'invitation_tokens' - context['page_title'] = '邀请令牌详情' - context['is_provider'] = True - - from django.conf import settings - site_url = getattr(settings, 'SITE_URL', '') - token_obj.invite_link = ( - f'{site_url}/operations/invite/{token_obj.token}/' - ) - - grants = ProductAccessGrant.objects.filter( - granted_by_token=token_obj, - ).select_related( - 'user', 'product', 'product_group', - ).order_by('-granted_at') - - from django.core.paginator import Paginator - paginator = Paginator(grants, 20) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - context['grants'] = page_obj - context['grant_count'] = grants.count() - from django.utils import timezone - context['effective_grant_count'] = grants.filter( - is_revoked=False, - ).exclude( - expires_at__lt=timezone.now(), - ).count() if grants.exists() else 0 - return context - - -class ProductInvitationTokenCreateView(ProviderRequestMixin, View): - """ - 创建邀请令牌视图 - - GET: 展示创建表单 - POST: 提交创建 - """ - - def get(self, request): - from .forms_provider import ProductInvitationTokenForm - form = ProductInvitationTokenForm(provider_user=request.user) - return render(request, 'admin_base/operations/token_create.html', { - 'form': form, - 'active_nav': 'invitation_tokens', - 'page_title': '创建邀请令牌', - }) - - def post(self, request): - from .forms_provider import ProductInvitationTokenForm - form = ProductInvitationTokenForm( - request.POST, provider_user=request.user, - ) - if form.is_valid(): - token_obj = form.save(commit=False) - token_obj.created_by = request.user - token_obj.save() - messages.success( - request, - f'邀请令牌创建成功:{token_obj.token[:8]}...', - ) - return redirect('provider_operations:token_list') - - return render(request, 'admin_base/operations/token_create.html', { - 'form': form, - 'active_nav': 'invitation_tokens', - 'page_title': '创建邀请令牌', - }) - - -class ProductInvitationTokenBatchEnableView(ProviderRequestMixin, View): - """ - 批量启用邀请令牌 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何令牌。') - return redirect('provider_operations:token_list') - - updated_count = ProductInvitationToken.objects.filter( - pk__in=selected_ids, - created_by=request.user, - is_active=False, - ).update(is_active=True) - - if updated_count > 0: - messages.success( - request, - f'成功启用了 {updated_count} 个邀请令牌。', - ) - else: - messages.warning( - request, - '没有需要启用的邀请令牌。', - ) - - return redirect('provider_operations:token_list') - - -class ProductInvitationTokenBatchDisableView(ProviderRequestMixin, View): - """ - 批量禁用邀请令牌 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何令牌。') - return redirect('provider_operations:token_list') - - updated_count = ProductInvitationToken.objects.filter( - pk__in=selected_ids, - created_by=request.user, - is_active=True, - ).update(is_active=False) - - if updated_count > 0: - messages.success( - request, - f'成功禁用了 {updated_count} 个邀请令牌。', - ) - else: - messages.warning( - request, - '没有需要禁用的邀请令牌。', - ) - - return redirect('provider_operations:token_list') - - -# =========================================================================== -# 访问授权管理 -# =========================================================================== - - -class ProductAccessGrantListView(ProviderRequestMixin, ListView): - """ - 产品访问授权列表视图 - - - 提供商数据隔离:只看到自己产品/产品组相关的授权 - - 支持批量撤销 - """ - - model = ProductAccessGrant - template_name = 'admin_base/operations/grant_list.html' - context_object_name = 'grants' - paginate_by = 20 - - def get_queryset(self): - return ProductAccessGrant.objects.filter( - Q(product__created_by=self.request.user) - | Q(product_group__created_by=self.request.user) - ).select_related( - 'user', 'product', 'product_group', - 'granted_by_token', - ).order_by('-granted_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'access_grants' - context['page_title'] = '访问授权' - context['is_provider'] = True - return context - - -class ProductAccessGrantBatchRevokeView(ProviderRequestMixin, View): - """ - 批量撤销访问授权 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何授权记录。') - return redirect('provider_operations:grant_list') - - from django.utils import timezone - qs = ProductAccessGrant.objects.filter( - pk__in=selected_ids, - is_revoked=False, - ).filter( - Q(product__created_by=request.user) - | Q(product_group__created_by=request.user), - ) - - updated_count = 0 - for grant in qs: - grant.is_revoked = True - grant.revoked_at = timezone.now() - grant.revoked_by = request.user - grant.save( - update_fields=['is_revoked', 'revoked_at', 'revoked_by'], - ) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功撤销了 {updated_count} 个授权。', - ) - else: - messages.warning( - request, - '没有需要撤销的授权。', - ) - - return redirect('provider_operations:grant_list') - - -# =========================================================================== -# RDP 域名路由管理 -# =========================================================================== - - -class RdpDomainRouteListView(ProviderRequestMixin, ListView): - """ - RDP域名路由列表视图 - - - 新增提供商数据隔离:通过 product__created_by 过滤 - - 只读视图,提供商无法修改路由 - """ - - model = RdpDomainRoute - template_name = 'admin_base/operations/route_list.html' - context_object_name = 'routes' - paginate_by = 20 - - def get_queryset(self): - return RdpDomainRoute.objects.filter( - product__created_by=self.request.user - ).select_related( - 'product', 'assigned_to', - ).order_by('-created_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'domain_routes' - context['page_title'] = '域名路由' - return context - - -# =========================================================================== -# 系统任务(只读参考) -# =========================================================================== - - -class SystemTaskListView(ProviderRequestMixin, ListView): - """ - 系统任务列表视图 - - - 只读参考视图,无独立管理页面 - - 提供商数据隔离:只看到自己创建的任务 - """ - - model = SystemTask - template_name = 'admin_base/operations/task_list.html' - context_object_name = 'tasks' - paginate_by = 20 - - def get_queryset(self): - return SystemTask.objects.filter( - created_by=self.request.user - ).order_by('-created_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'activity_log' - context['page_title'] = '系统任务' - return context - - -# =========================================================================== -# 产品管理 -# =========================================================================== - - -class ProviderProductMixin(ProviderContextMixin): - """ - 提供商产品数据隔离 Mixin - - - dispatch: 验证提供商身份 - - get_queryset: 限制为当前提供商创建的产品 - """ - - provider_url_namespace = 'provider:provider_operations' - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - if not is_provider(request.user): - from django.http import HttpResponseForbidden - return HttpResponseForbidden( - '您没有提供商权限,无法访问此页面。' - ) - return super().dispatch(request, *args, **kwargs) - - def get_provider_product_queryset(self): - """获取当前提供商可见的产品查询集""" - return Product.objects.filter( - created_by=self.request.user - ).select_related( - 'host', 'product_group', 'created_by', - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'products' - context['page_title'] = '产品管理' - return context - - -class ProductListView(ProviderProductMixin, TemplateView): - """ - 产品列表视图 - - 支持搜索、筛选、分页 - """ - - template_name = 'admin_base/operations/product_list.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - queryset = self.get_provider_product_queryset() - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(display_name__icontains=search) - | Q(host__name__icontains=search) - ) - - # 可用状态筛选 - available_filter = self.request.GET.get('is_available', '').strip() - if available_filter: - queryset = queryset.filter(is_available=available_filter == 'true') - - # 可见性筛选 - visibility_filter = self.request.GET.get('visibility', '').strip() - if visibility_filter: - queryset = queryset.filter(visibility=visibility_filter) - - # 排序 - queryset = queryset.order_by('-created_at') - - # 分页 - paginator = Paginator(queryset, 15) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'products': page_obj, - 'search': search, - 'available_filter': available_filter, - 'visibility_filter': visibility_filter, - 'visibility_choices': Product._meta.get_field('visibility').choices, - 'page_title': '产品管理', - 'active_nav': 'products', - }) - return context - - -class ProductDetailView(ProviderProductMixin, DetailView): - """ - 产品详情视图 - - 显示产品信息、磁盘配额配置、关联用户数等 - """ - - template_name = 'admin_base/operations/product_detail.html' - context_object_name = 'product' - pk_url_kwarg = 'pk' - - def get_queryset(self): - return self.get_provider_product_queryset() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - product = self.object - - # 获取关联用户数 - user_count = CloudComputerUser.objects.filter( - product=product - ).count() - - # 磁盘配额信息 - disk_quota_json = '{}' - if product.default_disk_quota: - disk_quota_json = json.dumps( - product.default_disk_quota, - ensure_ascii=False, - indent=2, - ) - - extra_disks_json = '[]' - if product.allow_extra_quota_disks: - extra_disks_json = json.dumps( - product.allow_extra_quota_disks, - ensure_ascii=False, - ) - - context.update({ - 'user_count': user_count, - 'disk_quota_json': disk_quota_json, - 'extra_disks_json': extra_disks_json, - 'page_title': f'产品 - {product.display_name}', - 'active_nav': 'products', - }) - return context - - -class ProductCreateView(ProviderProductMixin, TemplateView): - """ - 产品创建视图 - - 处理 GET 和 POST 请求,创建新产品。 - 自动设置 created_by 为当前用户。 - """ - - template_name = 'admin_base/operations/product_form.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'form': kwargs.get( - 'form', - ProductForm(provider_user=self.request.user), - ), - 'page_title': '创建产品', - 'active_nav': 'products', - 'is_create': True, - }) - return context - - def post(self, request, *args, **kwargs): - form = ProductForm( - request.POST, - provider_user=request.user, - ) - if form.is_valid(): - product = form.save(commit=False) - product.created_by = request.user - product.save() - - messages.success( - request, - f'产品 {product.display_name} 创建成功', - ) - return redirect( - 'provider_operations:product_detail', - pk=product.pk, - ) - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class ProductUpdateView(ProviderProductMixin, TemplateView): - """ - 产品编辑视图 - - 处理 GET 和 POST 请求,编辑产品信息。 - """ - - template_name = 'admin_base/operations/product_form.html' - - def get_product(self): - """获取当前编辑的产品,确保数据隔离""" - return get_object_or_404( - self.get_provider_product_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - product = self.get_product() - form = kwargs.get( - 'form', - ProductForm( - instance=product, - provider_user=self.request.user, - ), - ) - context.update({ - 'form': form, - 'product': product, - 'page_title': f'编辑产品 - {product.display_name}', - 'active_nav': 'products', - 'is_create': False, - }) - return context - - def post(self, request, *args, **kwargs): - product = self.get_product() - form = ProductForm( - request.POST, - instance=product, - provider_user=request.user, - ) - if form.is_valid(): - product = form.save() - messages.success( - request, - f'产品 {product.display_name} 更新成功', - ) - return redirect( - 'provider_operations:product_detail', - pk=product.pk, - ) - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class ProductDeleteView(ProviderProductMixin, TemplateView): - """ - 产品删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = 'admin_base/operations/product_confirm_delete.html' - - def get_product(self): - return get_object_or_404( - self.get_provider_product_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - product = self.get_product() - - # 获取关联用户数 - user_count = CloudComputerUser.objects.filter( - product=product - ).count() - - context.update({ - 'product': product, - 'user_count': user_count, - 'page_title': f'删除产品 - {product.display_name}', - 'active_nav': 'products', - }) - return context - - def post(self, request, *args, **kwargs): - product = self.get_product() - product_name = product.display_name - product.delete() - - messages.success( - request, - f'产品 {product_name} 已删除', - ) - return redirect('provider_operations:product_list') - - -# =========================================================================== -# 产品组管理 -# =========================================================================== - - -class ProviderProductGroupMixin(ProviderContextMixin): - """ - 提供商产品组数据隔离 Mixin - - - dispatch: 验证提供商身份 - - get_queryset: 限制为当前提供商创建的产品组 - """ - - provider_url_namespace = 'provider:provider_operations' - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - if not is_provider(request.user): - from django.http import HttpResponseForbidden - return HttpResponseForbidden( - '您没有提供商权限,无法访问此页面。' - ) - return super().dispatch(request, *args, **kwargs) - - def get_provider_productgroup_queryset(self): - """获取当前提供商可见的产品组查询集""" - return ProductGroup.objects.filter( - created_by=self.request.user - ).order_by('display_order', 'name') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'product_groups' - context['page_title'] = '产品组管理' - return context - - -class ProductGroupListView(ProviderProductGroupMixin, TemplateView): - """ - 产品组列表视图 - - 支持搜索、分页 - """ - - template_name = 'admin_base/operations/productgroup_list.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - queryset = self.get_provider_productgroup_queryset() - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(name__icontains=search) - | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(queryset, 15) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'productgroups': page_obj, - 'search': search, - 'page_title': '产品组管理', - 'active_nav': 'product_groups', - }) - return context - - -class ProductGroupCreateView(ProviderProductGroupMixin, TemplateView): - """ - 产品组创建视图 - - 处理 GET 和 POST 请求,创建新产品组。 - 自动设置 created_by 为当前用户。 - """ - - template_name = 'admin_base/operations/productgroup_form.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'form': kwargs.get( - 'form', - ProductGroupForm(), - ), - 'page_title': '创建产品组', - 'active_nav': 'product_groups', - 'is_create': True, - }) - return context - - def post(self, request, *args, **kwargs): - form = ProductGroupForm(request.POST) - if form.is_valid(): - productgroup = form.save(commit=False) - productgroup.created_by = request.user - productgroup.save() - - messages.success( - request, - f'产品组 {productgroup.name} 创建成功', - ) - return redirect('provider_operations:productgroup_list') - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class ProductGroupUpdateView(ProviderProductGroupMixin, TemplateView): - """ - 产品组编辑视图 - - 处理 GET 和 POST 请求,编辑产品组信息。 - """ - - template_name = 'admin_base/operations/productgroup_form.html' - - def get_productgroup(self): - """获取当前编辑的产品组,确保数据隔离""" - return get_object_or_404( - self.get_provider_productgroup_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - productgroup = self.get_productgroup() - form = kwargs.get( - 'form', - ProductGroupForm(instance=productgroup), - ) - context.update({ - 'form': form, - 'productgroup': productgroup, - 'page_title': f'编辑产品组 - {productgroup.name}', - 'active_nav': 'product_groups', - 'is_create': False, - }) - return context - - def post(self, request, *args, **kwargs): - productgroup = self.get_productgroup() - form = ProductGroupForm(request.POST, instance=productgroup) - if form.is_valid(): - productgroup = form.save() - messages.success( - request, - f'产品组 {productgroup.name} 更新成功', - ) - return redirect('provider_operations:productgroup_list') - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class ProductGroupDeleteView(ProviderProductGroupMixin, TemplateView): - """ - 产品组删除视图 - - 显示确认页面,处理删除请求。 - """ - - template_name = 'admin_base/operations/productgroup_confirm_delete.html' - - def get_productgroup(self): - return get_object_or_404( - self.get_provider_productgroup_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - productgroup = self.get_productgroup() - - # 获取关联产品数 - product_count = Product.objects.filter( - product_group=productgroup - ).count() - - context.update({ - 'productgroup': productgroup, - 'product_count': product_count, - 'page_title': f'删除产品组 - {productgroup.name}', - 'active_nav': 'product_groups', - }) - return context - - def post(self, request, *args, **kwargs): - productgroup = self.get_productgroup() - productgroup_name = productgroup.name - productgroup.delete() - - messages.success( - request, - f'产品组 {productgroup_name} 已删除', - ) - return redirect('provider_operations:productgroup_list') diff --git a/apps/provider/__init__.py b/apps/provider/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/provider/apps.py b/apps/provider/apps.py deleted file mode 100644 index 5661ef1..0000000 --- a/apps/provider/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class ProviderConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.provider' - verbose_name = '提供商后台' - label = 'provider' diff --git a/apps/provider/context_mixin.py b/apps/provider/context_mixin.py deleted file mode 100644 index 999b362..0000000 --- a/apps/provider/context_mixin.py +++ /dev/null @@ -1,20 +0,0 @@ -class ProviderContextMixin: - """ - 提供商视图上下文混入类 - - 为提供商视图注入通用上下文变量(active_nav 等)。 - 提供商和超管共用同一套模板和 URL,侧边栏通过 - {% if user.is_superuser %} 条件渲染实现差异化。 - """ - - provider_page_title = '2c2a 提供商后台' - - def get_provider_context(self): - return { - 'page_title': self.provider_page_title, - } - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update(self.get_provider_context()) - return context diff --git a/apps/provider/decorators.py b/apps/provider/decorators.py deleted file mode 100644 index 8861dae..0000000 --- a/apps/provider/decorators.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -提供商认证装饰器和辅助函数 - -集中管理提供商身份验证逻辑,供整个项目使用。 -替代 hosts/admin.py、operations/admin.py、tickets/admin.py 中重复的 is_provider 函数。 -""" - -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect -from functools import wraps - - -PROVIDER_GROUP_NAME = '主机提供商' - - -def is_provider(user): - """ - 检查用户是否属于提供商组 - - 超级管理员不属于提供商组,即使其权限更高。 - 此逻辑与 Admin 后台的数据隔离保持一致。 - - Args: - user: 用户对象 - - Returns: - bool: 如果用户属于提供商组且不是超级管理员,返回 True - """ - if user.is_superuser: - return False - return user.groups.filter(name=PROVIDER_GROUP_NAME).exists() - - -def provider_required(view_func): - """ - 装饰器:要求当前用户为提供商 - - - 未登录用户将被重定向到登录页 - - 非提供商用户将被重定向到登录页 - """ - @wraps(view_func) - @login_required - def wrapper(request, *args, **kwargs): - if not is_provider(request.user): - return redirect('accounts:login') - return view_func(request, *args, **kwargs) - return wrapper - - -def superuser_required(view_func): - """ - 装饰器:要求当前用户为超级管理员 - - - 未登录用户将被重定向到登录页 - - 非超级管理员将被重定向到提供商仪表盘 - """ - @wraps(view_func) - @login_required - def wrapper(request, *args, **kwargs): - if not request.user.is_superuser: - return redirect('provider:dashboard') - return view_func(request, *args, **kwargs) - return wrapper diff --git a/apps/provider/migrations/__init__.py b/apps/provider/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/provider/urls.py b/apps/provider/urls.py deleted file mode 100644 index 7e399db..0000000 --- a/apps/provider/urls.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -提供商后台 URL 配置 - -所有 URL 以 /provider/ 为前缀。 -""" - -from django.urls import path -from . import views - -app_name = 'provider' - -urlpatterns = [ - # 仪表盘 - path('', views.ProviderDashboardView.as_view(), name='dashboard'), - - # 主机管理 - path('hosts/', views.HostListView.as_view(), name='hosts'), - - # 主机组 - path('host-groups/', views.HostGroupListView.as_view(), name='host_groups'), - - # 产品管理 - path('products/', views.ProductListView.as_view(), name='products'), - - # 产品组 - path('product-groups/', views.ProductGroupListView.as_view(), - name='product_groups'), - - # 开户申请 - path('account-opening/', views.AccountOpeningListView.as_view(), - name='account_opening'), - - # 云电脑用户 - path('cloud-users/', views.CloudUserListView.as_view(), - name='cloud_users'), - - # 邀请令牌 - path('invitation-tokens/', views.InvitationTokenListView.as_view(), - name='invitation_tokens'), - - # 授权记录 - path('access-grants/', views.AccessGrantListView.as_view(), - name='access_grants'), - - # 工单管理 - path('tickets/', views.TicketListView.as_view(), name='tickets'), - - # 工单分类 - path('ticket-categories/', views.TicketCategoryListView.as_view(), - name='ticket_categories'), - - # 活动日志 - path('activity-log/', views.ActivityLogView.as_view(), - name='activity_log'), - - # 域名路由 - path('domain-routes/', views.DomainRouteListView.as_view(), - name='domain_routes'), - - # QQ验证 - path('qq-verify/', views.QQVerifyView.as_view(), name='qq_verify'), - - # 插件配置 - path('plugins/', views.PluginConfigView.as_view(), name='plugins'), -] diff --git a/apps/provider/views.py b/apps/provider/views.py deleted file mode 100644 index fa52760..0000000 --- a/apps/provider/views.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -提供商后台视图 - -包含仪表盘和各模块的占位视图。 -所有视图均使用 @provider_required 装饰器保护。 -""" - -from django.views.generic import TemplateView -from django.utils.decorators import method_decorator - -from .decorators import provider_required - - -class ProviderBaseView: - """ - 提供商后台基础视图混入类 - - 通过重写 dispatch 方法实现提供商身份验证, - 避免在混入类上使用 method_decorator(混入类无 dispatch 方法)。 - """ - - def dispatch(self, request, *args, **kwargs): - from django.shortcuts import redirect - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - from .decorators import is_provider - if not is_provider(request.user): - return redirect('accounts:login') - return super().dispatch(request, *args, **kwargs) - - -# ========== 仪表盘 ========== - -class ProviderDashboardView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/dashboard.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - # 统计数据 - from apps.hosts.models import Host, HostGroup - from apps.operations.models import ( - Product, ProductGroup, AccountOpeningRequest, - CloudComputerUser, ProductInvitationToken, - ProductAccessGrant, RdpDomainRoute, - ) - - stats = { - 'host_count': Host.objects.filter(providers=user).count(), - 'hostgroup_count': HostGroup.objects.filter(providers=user).count(), - 'product_count': Product.objects.filter(created_by=user).count(), - 'productgroup_count': ProductGroup.objects.filter(created_by=user).count(), - 'pending_request_count': AccountOpeningRequest.objects.filter( - target_product__created_by=user, status='pending' - ).count(), - 'active_user_count': CloudComputerUser.objects.filter( - product__created_by=user, status='active' - ).count(), - 'invitation_token_count': ProductInvitationToken.objects.filter( - created_by=user, is_active=True - ).count(), - 'access_grant_count': ProductAccessGrant.objects.filter( - product__created_by=user, is_revoked=False - ).count(), - 'rdp_route_count': RdpDomainRoute.objects.filter( - product__created_by=user - ).count(), - } - - context['stats'] = stats - context['page_title'] = '仪表盘' - context['active_nav'] = 'dashboard' - return context - - -# ========== 占位视图(后续实现具体功能) ========== - -class HostListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '主机管理' - context['active_nav'] = 'hosts' - context['feature_icon'] = 'dns' - context['feature_name'] = '主机管理' - return context - - -class HostGroupListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '主机组' - context['active_nav'] = 'host_groups' - context['feature_icon'] = 'folder' - context['feature_name'] = '主机组' - return context - - -class ProductListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '产品管理' - context['active_nav'] = 'products' - context['feature_icon'] = 'inventory_2' - context['feature_name'] = '产品管理' - return context - - -class ProductGroupListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '产品组' - context['active_nav'] = 'product_groups' - context['feature_icon'] = 'category' - context['feature_name'] = '产品组' - return context - - -class AccountOpeningListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '开户申请' - context['active_nav'] = 'account_opening' - context['feature_icon'] = 'how_to_reg' - context['feature_name'] = '开户申请' - return context - - -class CloudUserListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '云电脑用户' - context['active_nav'] = 'cloud_users' - context['feature_icon'] = 'people' - context['feature_name'] = '云电脑用户' - return context - - -class InvitationTokenListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '邀请令牌' - context['active_nav'] = 'invitation_tokens' - context['feature_icon'] = 'mail' - context['feature_name'] = '邀请令牌' - return context - - -class AccessGrantListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '授权记录' - context['active_nav'] = 'access_grants' - context['feature_icon'] = 'key' - context['feature_name'] = '授权记录' - return context - - -class TicketListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单管理' - context['active_nav'] = 'tickets' - context['feature_icon'] = 'confirmation_number' - context['feature_name'] = '工单管理' - return context - - -class TicketCategoryListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单分类' - context['active_nav'] = 'ticket_categories' - context['feature_icon'] = 'label' - context['feature_name'] = '工单分类' - return context - - -class ActivityLogView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '活动日志' - context['active_nav'] = 'activity_log' - context['feature_icon'] = 'history' - context['feature_name'] = '活动日志' - return context - - -class DomainRouteListView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '域名路由' - context['active_nav'] = 'domain_routes' - context['feature_icon'] = 'language' - context['feature_name'] = '域名路由' - return context - - -class QQVerifyView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = 'QQ验证' - context['active_nav'] = 'qq_verify' - context['feature_icon'] = 'verified' - context['feature_name'] = 'QQ验证' - return context - - -class PluginConfigView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/coming_soon.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '插件配置' - context['active_nav'] = 'plugins' - context['feature_icon'] = 'extension' - context['feature_name'] = '插件配置' - return context diff --git a/apps/provider_backend/__init__.py b/apps/provider_backend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/provider_backend/admin.py b/apps/provider_backend/admin.py deleted file mode 100644 index b2f16ba..0000000 --- a/apps/provider_backend/admin.py +++ /dev/null @@ -1 +0,0 @@ -# provider_backend 无需后台管理 diff --git a/apps/provider_backend/api.py b/apps/provider_backend/api.py deleted file mode 100644 index b147871..0000000 --- a/apps/provider_backend/api.py +++ /dev/null @@ -1,272 +0,0 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.utils.decorators import method_decorator - -from .decorators import provider_required, is_provider - - -@method_decorator(provider_required, name='dispatch') -class HostDeployAPI(APIView): - """ - 主机部署 API - - POST /api/hosts//deploy/ - 生成部署命令 - """ - - def post(self, request, pk): - from apps.hosts.models import Host - site_group = getattr(request, 'site_group', None) - host_qs = Host.objects.filter(pk=pk, providers=request.user) - if site_group: - host_qs = host_qs.filter(site_group=site_group) - else: - host_qs = host_qs.filter(site_group__isnull=True) - try: - host = host_qs.get() - except Host.DoesNotExist: - return Response( - {'error': '主机不存在'}, - status=status.HTTP_404_NOT_FOUND - ) - - # TODO: 实现部署命令生成逻辑 - return Response({ - 'status': 'success', - 'message': f'主机 {host.name} 部署命令生成功能即将上线', - 'host_id': host.pk, - 'host_name': host.name, - }) - - -@method_decorator(provider_required, name='dispatch') -class AccountRequestActionAPI(APIView): - """ - 开户申请操作 API - - POST /api/account-requests//action/ - 操作类型: approve / reject / process - """ - - def post(self, request, pk): - from apps.operations.models import AccountOpeningRequest - try: - account_request = AccountOpeningRequest.objects.get( - pk=pk, - target_product__created_by=request.user - ) - except AccountOpeningRequest.DoesNotExist: - return Response( - {'error': '开户申请不存在'}, - status=status.HTTP_404_NOT_FOUND - ) - - action = request.data.get('action') - if action not in ('approve', 'reject', 'process'): - return Response( - {'error': '无效的操作类型,支持: approve, reject, process'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # TODO: 实现开户申请操作逻辑 - return Response({ - 'status': 'success', - 'message': f'开户申请 {account_request.pk} 的 {action} 操作即将上线', - 'request_id': account_request.pk, - 'action': action, - }) - - -@method_decorator(provider_required, name='dispatch') -class CloudUserActionAPI(APIView): - """ - 云电脑用户操作 API - - POST /api/cloud-users//action/ - 操作类型: activate / deactivate / disable / reset-password - """ - - def post(self, request, pk): - from apps.operations.models import CloudComputerUser - site_group = getattr(request, 'site_group', None) - qs = CloudComputerUser.objects.filter( - pk=pk, - product__created_by=request.user - ) - if site_group: - qs = qs.filter(product__site_group=site_group) - else: - qs = qs.filter(product__site_group__isnull=True) - try: - cloud_user = qs.get() - except CloudComputerUser.DoesNotExist: - return Response( - {'error': '云电脑用户不存在'}, - status=status.HTTP_404_NOT_FOUND - ) - - action = request.data.get('action') - valid_actions = ('activate', 'deactivate', 'disable', 'reset-password') - if action not in valid_actions: - return Response( - {'error': f'无效的操作类型,支持: {", ".join(valid_actions)}'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # TODO: 实现云电脑用户操作逻辑 - return Response({ - 'status': 'success', - 'message': f'云电脑用户 {cloud_user.username} 的 {action} 操作即将上线', - 'user_id': cloud_user.pk, - 'username': cloud_user.username, - 'action': action, - }) - - -@method_decorator(provider_required, name='dispatch') -class InvitationTokenActionAPI(APIView): - """ - 邀请令牌操作 API - - POST /api/invitation-tokens//action/ - 操作类型: activate / deactivate - """ - - def post(self, request, pk): - from apps.operations.models import ProductInvitationToken - try: - token = ProductInvitationToken.objects.get( - pk=pk, - created_by=request.user - ) - except ProductInvitationToken.DoesNotExist: - return Response( - {'error': '邀请令牌不存在'}, - status=status.HTTP_404_NOT_FOUND - ) - - action = request.data.get('action') - if action not in ('activate', 'deactivate'): - return Response( - {'error': '无效的操作类型,支持: activate, deactivate'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # TODO: 实现邀请令牌操作逻辑 - return Response({ - 'status': 'success', - 'message': f'邀请令牌 {token.token[:8]}... 的 {action} 操作即将上线', - 'token_id': token.pk, - 'action': action, - }) - - -class ProductShareLinkAPI(APIView): - """ - 产品分享链接 API - - GET /api/products//share-link/ - 获取产品的邀请令牌信息(检查是否已有活跃令牌) - - POST /api/products//share-link/ - 为产品创建新的邀请令牌并返回分享链接 - """ - - permission_classes = [IsAuthenticated] - - def get(self, request, pk): - if not (request.user.is_superuser or is_provider(request.user)): - return Response( - {'error': '权限不足'}, - status=status.HTTP_403_FORBIDDEN, - ) - from apps.operations.models import Product, ProductInvitationToken - site_group = getattr(request, 'site_group', None) - product_qs = Product.objects.filter( - pk=pk, - created_by=request.user, - visibility='invite_only', - ) - if site_group: - product_qs = product_qs.filter(site_group=site_group) - else: - product_qs = product_qs.filter(site_group__isnull=True) - try: - product = product_qs.get() - except Product.DoesNotExist: - return Response( - {'error': '产品不存在或非邀请访问产品'}, - status=status.HTTP_404_NOT_FOUND, - ) - - active_token = ProductInvitationToken.objects.filter( - product=product, - created_by=request.user, - is_active=True, - ).order_by('-created_at').first() - - if active_token: - invite_link = request.build_absolute_uri( - f'/operations/invite/{active_token.token}/' - ) - return Response({ - 'has_existing': True, - 'invite_link': invite_link, - 'token': active_token.token[:8] + '...', - 'used_count': active_token.used_count, - 'max_uses': active_token.max_uses, - 'created_at': active_token.created_at.isoformat(), - 'expires_at': ( - active_token.expires_at.isoformat() - if active_token.expires_at else None - ), - }) - - return Response({'has_existing': False}) - - def post(self, request, pk): - if not (request.user.is_superuser or is_provider(request.user)): - return Response( - {'error': '权限不足'}, - status=status.HTTP_403_FORBIDDEN, - ) - from apps.operations.models import Product, ProductInvitationToken - site_group = getattr(request, 'site_group', None) - product_qs = Product.objects.filter( - pk=pk, - created_by=request.user, - visibility='invite_only', - ) - if site_group: - product_qs = product_qs.filter(site_group=site_group) - else: - product_qs = product_qs.filter(site_group__isnull=True) - try: - product = product_qs.get() - except Product.DoesNotExist: - return Response( - {'error': '产品不存在或非邀请访问产品'}, - status=status.HTTP_404_NOT_FOUND, - ) - - token_obj = ProductInvitationToken.objects.create( - product=product, - created_by=request.user, - is_active=True, - ) - - invite_link = request.build_absolute_uri( - f'/operations/invite/{token_obj.token}/' - ) - - return Response({ - 'has_existing': False, - 'invite_link': invite_link, - 'token': token_obj.token[:8] + '...', - 'used_count': 0, - 'max_uses': 0, - 'created_at': token_obj.created_at.isoformat(), - 'expires_at': None, - }) diff --git a/apps/provider_backend/api_urls.py b/apps/provider_backend/api_urls.py deleted file mode 100644 index e90d2c5..0000000 --- a/apps/provider_backend/api_urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.urls import path -from . import api - -app_name = 'provider_api' - -urlpatterns = [ - path('hosts//deploy/', api.HostDeployAPI.as_view(), name='host_deploy'), - path('account-requests//action/', api.AccountRequestActionAPI.as_view(), name='accountrequest_action'), - path('cloud-users//action/', api.CloudUserActionAPI.as_view(), name='clouduser_action'), - path( - 'invitation-tokens//action/', - api.InvitationTokenActionAPI.as_view(), - name='invitationtoken_action', - ), - path( - 'products//share-link/', - api.ProductShareLinkAPI.as_view(), - name='product_share_link', - ), -] diff --git a/apps/provider_backend/apps.py b/apps/provider_backend/apps.py deleted file mode 100644 index 450a63f..0000000 --- a/apps/provider_backend/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ProviderBackendConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.provider_backend' - verbose_name = '提供商后台' diff --git a/apps/provider_backend/decorators.py b/apps/provider_backend/decorators.py deleted file mode 100644 index ba9c1d2..0000000 --- a/apps/provider_backend/decorators.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect -from django.urls import reverse -from functools import wraps - - -PROVIDER_GROUP_NAME = '主机提供商' - - -def is_provider(user): - """检查用户是否属于提供商组""" - if user.is_superuser: - return False - return user.groups.filter(name=PROVIDER_GROUP_NAME).exists() - - -def provider_required(view_func): - """ - 装饰器:要求当前用户为提供商 - 非提供商用户将被重定向到登录页 - """ - @wraps(view_func) - @login_required - def wrapper(request, *args, **kwargs): - if not is_provider(request.user): - return redirect('accounts:login') - return view_func(request, *args, **kwargs) - return wrapper - - -def superadmin_required(view_func): - """ - 装饰器:要求当前用户为超级管理员 - 非超级管理员将被重定向到提供商仪表盘 - """ - @wraps(view_func) - @login_required - def wrapper(request, *args, **kwargs): - if not request.user.is_superuser: - return redirect('provider:dashboard') - return view_func(request, *args, **kwargs) - return wrapper diff --git a/apps/provider_backend/middleware.py b/apps/provider_backend/middleware.py deleted file mode 100644 index f09a871..0000000 --- a/apps/provider_backend/middleware.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.shortcuts import redirect -from django.urls import reverse - - -class ProviderRedirectMiddleware: - """ - 提供商重定向中间件 - - 将提供商用户从 /admin/ 重定向到 /provider/, - 防止提供商访问 Django Admin 后台。 - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - if request.path.startswith('/admin/') and not request.path.startswith('/admin/login/'): - if hasattr(request, 'user') and request.user.is_authenticated: - from .decorators import is_provider - if is_provider(request.user) and not request.user.is_superuser: - return redirect('provider:dashboard') - return self.get_response(request) diff --git a/apps/provider_backend/migrations/__init__.py b/apps/provider_backend/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/provider_backend/models.py b/apps/provider_backend/models.py deleted file mode 100644 index 71a8362..0000000 --- a/apps/provider_backend/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/apps/provider_backend/tests/__init__.py b/apps/provider_backend/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/provider_backend/urls.py b/apps/provider_backend/urls.py deleted file mode 100644 index 6f9b8f8..0000000 --- a/apps/provider_backend/urls.py +++ /dev/null @@ -1,69 +0,0 @@ -from django.urls import path, include -from . import views - -app_name = 'provider' - -urlpatterns = [ - # 仪表盘 - path('', views.DashboardView.as_view(), name='dashboard'), - - # 主机管理 - path('hosts/', views.HostListView.as_view(), name='host_list'), - path('hosts/create/', views.HostCreateWizard.as_view(), name='host_create'), - path('hosts//', views.HostDetailView.as_view(), name='host_detail'), - path('hosts//edit/', views.HostUpdateView.as_view(), name='host_update'), - path('hosts//deploy/', views.HostDeployView.as_view(), name='host_deploy'), - - # 主机组管理 - path('host-groups/', views.HostGroupListView.as_view(), name='hostgroup_list'), - path('host-groups/create/', views.HostGroupCreateView.as_view(), name='hostgroup_create'), - path('host-groups//edit/', views.HostGroupUpdateView.as_view(), name='hostgroup_update'), - - # 产品管理 - path('products/', views.ProductListView.as_view(), name='product_list'), - path('products/create/', views.ProductCreateView.as_view(), name='product_create'), - path('products//', views.ProductDetailView.as_view(), name='product_detail'), - path('products//edit/', views.ProductUpdateView.as_view(), name='product_update'), - - # 产品组管理 - path('product-groups/', views.ProductGroupListView.as_view(), name='productgroup_list'), - path('product-groups/create/', views.ProductGroupCreateView.as_view(), name='productgroup_create'), - path('product-groups//edit/', views.ProductGroupUpdateView.as_view(), name='productgroup_update'), - - # 开户申请管理 - path('account-requests/', views.AccountRequestListView.as_view(), name='accountrequest_list'), - path('account-requests//', views.AccountRequestDetailView.as_view(), name='accountrequest_detail'), - - # 云电脑用户管理 - path('cloud-users/', views.CloudUserListView.as_view(), name='clouduser_list'), - path('cloud-users//', views.CloudUserDetailView.as_view(), name='clouduser_detail'), - - # 邀请令牌管理 - path('invitation-tokens/', views.InvitationTokenListView.as_view(), name='invitationtoken_list'), - path('invitation-tokens/create/', views.InvitationTokenCreateView.as_view(), name='invitationtoken_create'), - - # 访问授权管理 - path('access-grants/', views.AccessGrantListView.as_view(), name='accessgrant_list'), - - # 工单管理 - path('tickets/', views.TicketListView.as_view(), name='ticket_list'), - path('tickets/create/', views.TicketCreateView.as_view(), name='ticket_create'), - path('tickets//', views.TicketDetailView.as_view(), name='ticket_detail'), - - # 工单分类 - path('ticket-categories/', views.TicketCategoryListView.as_view(), name='ticketcategory_list'), - - # 工单活动 - path('ticket-activities/', views.TicketActivityListView.as_view(), name='ticketactivity_list'), - - # RDP路由 - path('rdp-routes/', views.RdpRouteListView.as_view(), name='rdproute_list'), - - # QQ验证配置 - path('qq-config/', views.QQConfigListView.as_view(), name='qqconfig_list'), - path('qq-config/create/', views.QQConfigCreateView.as_view(), name='qqconfig_create'), - path('qq-config//edit/', views.QQConfigUpdateView.as_view(), name='qqconfig_update'), - - # API 端点 - path('api/', include('apps.provider_backend.api_urls')), -] diff --git a/apps/provider_backend/views.py b/apps/provider_backend/views.py deleted file mode 100644 index 664b39b..0000000 --- a/apps/provider_backend/views.py +++ /dev/null @@ -1,575 +0,0 @@ -from django.views.generic import TemplateView, ListView, CreateView, UpdateView, DetailView, FormView -from django.utils.decorators import method_decorator -from django.shortcuts import redirect - -from .decorators import provider_required - - -@method_decorator(provider_required, name='dispatch') -class ProviderBaseView: - """提供商后台基础视图混入类,自动应用 provider_required 装饰器""" - pass - - -# ========== 仪表盘 ========== - -class DashboardView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/dashboard.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '仪表盘' - return context - - -# ========== 主机管理 ========== - -from apps.hosts.models import Host, HostGroup - - -class HostListView(ProviderBaseView, ListView): - model = Host - template_name = 'admin_base/provider/host_list.html' - context_object_name = 'hosts' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Host.objects.filter( - providers=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '主机管理' - return context - - -class HostCreateWizard(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/host_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建主机' - return context - - -class HostDetailView(ProviderBaseView, DetailView): - model = Host - template_name = 'admin_base/provider/host_detail.html' - context_object_name = 'host' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Host.objects.filter(providers=self.request.user) - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '主机详情' - return context - - -class HostUpdateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/host_update.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '编辑主机' - site_group = getattr(self.request, 'site_group', None) - qs = Host.objects.filter( - pk=kwargs['pk'], providers=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - context['host'] = qs.first() - return context - - -class HostDeployView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/host_deploy.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '部署主机' - site_group = getattr(self.request, 'site_group', None) - qs = Host.objects.filter( - pk=kwargs['pk'], providers=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - context['host'] = qs.first() - return context - - -# ========== 主机组管理 ========== - -class HostGroupListView(ProviderBaseView, ListView): - model = HostGroup - template_name = 'admin_base/provider/hostgroup_list.html' - context_object_name = 'hostgroups' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = HostGroup.objects.filter( - providers=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '主机组管理' - return context - - -class HostGroupCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/hostgroup_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建主机组' - return context - - -class HostGroupUpdateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/hostgroup_update.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '编辑主机组' - site_group = getattr(self.request, 'site_group', None) - qs = HostGroup.objects.filter( - pk=kwargs['pk'], providers=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - context['hostgroup'] = qs.first() - return context - - -# ========== 产品管理 ========== - -from apps.operations.models import ( - Product, ProductGroup, AccountOpeningRequest, - CloudComputerUser, ProductInvitationToken, ProductAccessGrant, - RdpDomainRoute, -) - - -class ProductListView(ProviderBaseView, ListView): - model = Product - template_name = 'admin_base/provider/product_list.html' - context_object_name = 'products' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Product.objects.filter( - created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '产品管理' - return context - - -class ProductCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/product_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建产品' - return context - - -class ProductDetailView(ProviderBaseView, DetailView): - model = Product - template_name = 'admin_base/provider/product_detail.html' - context_object_name = 'product' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Product.objects.filter(created_by=self.request.user) - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '产品详情' - return context - - -class ProductUpdateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/product_update.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '编辑产品' - site_group = getattr(self.request, 'site_group', None) - qs = Product.objects.filter( - pk=kwargs['pk'], created_by=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - context['product'] = qs.first() - return context - - -# ========== 产品组管理 ========== - -class ProductGroupListView(ProviderBaseView, ListView): - model = ProductGroup - template_name = 'admin_base/provider/productgroup_list.html' - context_object_name = 'productgroups' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = ProductGroup.objects.filter( - created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '产品组管理' - return context - - -class ProductGroupCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/productgroup_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建产品组' - return context - - -class ProductGroupUpdateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/productgroup_update.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '编辑产品组' - site_group = getattr(self.request, 'site_group', None) - qs = ProductGroup.objects.filter( - pk=kwargs['pk'], created_by=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - context['productgroup'] = qs.first() - return context - - -# ========== 开户申请管理 ========== - -class AccountRequestListView(ProviderBaseView, ListView): - model = AccountOpeningRequest - template_name = 'admin_base/provider/accountrequest_list.html' - context_object_name = 'account_requests' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = AccountOpeningRequest.objects.filter( - target_product__created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(target_product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '开户申请' - return context - - -class AccountRequestDetailView(ProviderBaseView, DetailView): - model = AccountOpeningRequest - template_name = 'admin_base/provider/accountrequest_detail.html' - context_object_name = 'account_request' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = AccountOpeningRequest.objects.filter( - target_product__created_by=self.request.user - ) - if site_group is not None: - qs = qs.filter(target_product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '开户申请详情' - return context - - -# ========== 云电脑用户管理 ========== - -class CloudUserListView(ProviderBaseView, ListView): - model = CloudComputerUser - template_name = 'admin_base/provider/clouduser_list.html' - context_object_name = 'cloud_users' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = CloudComputerUser.objects.filter( - product__created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '云电脑用户' - return context - - -class CloudUserDetailView(ProviderBaseView, DetailView): - model = CloudComputerUser - template_name = 'admin_base/provider/clouduser_detail.html' - context_object_name = 'cloud_user' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = CloudComputerUser.objects.filter( - product__created_by=self.request.user - ) - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '云电脑用户详情' - return context - - -# ========== 邀请令牌管理 ========== - -class InvitationTokenListView(ProviderBaseView, ListView): - model = ProductInvitationToken - template_name = 'admin_base/provider/invitationtoken_list.html' - context_object_name = 'invitation_tokens' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = ProductInvitationToken.objects.filter( - created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '邀请令牌' - return context - - -class InvitationTokenCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/invitationtoken_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建邀请令牌' - return context - - -# ========== 访问授权管理 ========== - -class AccessGrantListView(ProviderBaseView, ListView): - model = ProductAccessGrant - template_name = 'admin_base/provider/accessgrant_list.html' - context_object_name = 'access_grants' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = ProductAccessGrant.objects.filter( - product__created_by=self.request.user - ).order_by('-granted_at') - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '访问授权' - return context - - -# ========== 工单管理 ========== - -from apps.tickets.models import Ticket, TicketCategory, TicketActivity - - -class TicketListView(ProviderBaseView, ListView): - model = Ticket - template_name = 'admin_base/provider/ticket_list.html' - context_object_name = 'tickets' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Ticket.objects.filter( - assignee=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单管理' - return context - - -class TicketCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/ticket_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建工单' - return context - - -class TicketDetailView(ProviderBaseView, DetailView): - model = Ticket - template_name = 'admin_base/provider/ticket_detail.html' - context_object_name = 'ticket' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = Ticket.objects.filter( - assignee=self.request.user - ) - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单详情' - return context - - -# ========== 工单分类 ========== - -class TicketCategoryListView(ProviderBaseView, ListView): - model = TicketCategory - template_name = 'admin_base/provider/ticketcategory_list.html' - context_object_name = 'ticket_categories' - - def get_queryset(self): - return TicketCategory.objects.filter( - is_active=True - ).order_by('display_order', 'name') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单分类' - return context - - -# ========== 工单活动 ========== - -class TicketActivityListView(ProviderBaseView, ListView): - model = TicketActivity - template_name = 'admin_base/provider/ticketactivity_list.html' - context_object_name = 'ticket_activities' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = TicketActivity.objects.filter( - ticket__assignee=self.request.user - ).order_by('-created_at')[:50] - if site_group is not None: - qs = qs.filter(ticket__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '工单活动' - return context - - -# ========== RDP路由 ========== - -class RdpRouteListView(ProviderBaseView, ListView): - model = RdpDomainRoute - template_name = 'admin_base/provider/rdproute_list.html' - context_object_name = 'rdp_routes' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = RdpDomainRoute.objects.filter( - product__created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = 'RDP路由' - return context - - -# ========== QQ验证配置 ========== - -from plugins.models import QQVerificationConfig - - -class QQConfigListView(ProviderBaseView, ListView): - model = QQVerificationConfig - template_name = 'admin_base/provider/qqconfig_list.html' - context_object_name = 'qq_configs' - - def get_queryset(self): - site_group = getattr(self.request, 'site_group', None) - qs = QQVerificationConfig.objects.filter( - product__created_by=self.request.user - ).order_by('-created_at') - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = 'QQ验证配置' - return context - - -class QQConfigCreateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/qqconfig_create.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '创建QQ验证配置' - return context - - -class QQConfigUpdateView(ProviderBaseView, TemplateView): - template_name = 'admin_base/provider/qqconfig_update.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['page_title'] = '编辑QQ验证配置' - site_group = getattr(self.request, 'site_group', None) - qs = QQVerificationConfig.objects.filter( - pk=kwargs['pk'], product__created_by=self.request.user - ) - if site_group is not None: - qs = qs.filter(product__site_group=site_group) - context['qq_config'] = qs.first() - return context diff --git a/apps/tasks/migrations/0001_initial.py b/apps/tasks/migrations/0001_initial.py deleted file mode 100755 index b2de847..0000000 --- a/apps/tasks/migrations/0001_initial.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-01 11:58 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AsyncTask', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('task_id', models.CharField(max_length=255, unique=True, verbose_name='任务ID')), - ('name', models.CharField(max_length=255, verbose_name='任务名称')), - ('status', models.CharField(choices=[('pending', '待处理'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('cancelled', '已取消')], default='pending', max_length=20, verbose_name='任务状态')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('started_at', models.DateTimeField(blank=True, null=True, verbose_name='开始时间')), - ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), - ('progress', models.IntegerField(default=0, verbose_name='进度百分比')), - ('result', models.JSONField(blank=True, null=True, verbose_name='任务结果')), - ('error_message', models.TextField(blank=True, null=True, verbose_name='错误信息')), - ('target_object_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='目标对象ID')), - ('target_content_type', models.CharField(blank=True, max_length=100, null=True, verbose_name='目标对象类型')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), - ], - options={ - 'verbose_name': '异步任务', - 'verbose_name_plural': '异步任务', - 'db_table': 'async_task', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='TaskProgress', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('progress', models.IntegerField(verbose_name='进度百分比')), - ('message', models.TextField(blank=True, null=True, verbose_name='进度消息')), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='时间戳')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_updates', to='tasks.asynctask')), - ], - options={ - 'verbose_name': '任务进度', - 'verbose_name_plural': '任务进度', - 'db_table': 'task_progress', - 'ordering': ['-timestamp'], - }, - ), - ] diff --git a/apps/tasks/migrations/__init__.py b/apps/tasks/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/apps/tasks/models.py b/apps/tasks/models.py deleted file mode 100755 index fc2eb29..0000000 --- a/apps/tasks/models.py +++ /dev/null @@ -1,100 +0,0 @@ -from django.db import models -from django.utils import timezone - - -class AsyncTask(models.Model): - """异步任务状态跟踪模型""" - STATUS_CHOICES = [ - ('pending', '待处理'), - ('running', '执行中'), - ('success', '成功'), - ('failed', '失败'), - ('cancelled', '已取消'), - ] - - task_id = models.CharField(max_length=255, unique=True, verbose_name="任务ID") - name = models.CharField(max_length=255, verbose_name="任务名称") - status = models.CharField( - max_length=20, - choices=STATUS_CHOICES, - default='pending', - verbose_name="任务状态" - ) - created_by = models.ForeignKey( - 'accounts.User', - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name="创建者" - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - started_at = models.DateTimeField(null=True, blank=True, verbose_name="开始时间") - completed_at = models.DateTimeField(null=True, blank=True, verbose_name="完成时间") - progress = models.IntegerField(default=0, verbose_name="进度百分比") # 进度百分比 0-100 - result = models.JSONField(null=True, blank=True, verbose_name="任务结果") # 任务结果 - error_message = models.TextField(null=True, blank=True, verbose_name="错误信息") - target_object_id = models.PositiveIntegerField(null=True, blank=True, verbose_name="目标对象ID") - target_content_type = models.CharField(max_length=100, null=True, blank=True, verbose_name="目标对象类型") - - class Meta: - verbose_name = "异步任务" - verbose_name_plural = "异步任务" - db_table = "async_task" - ordering = ['-created_at'] - - def __str__(self): - return f"{self.name} - {self.status}" - - def start_execution(self): - """标记任务开始执行""" - self.status = 'running' - self.started_at = timezone.now() - self.save() - - def complete_success(self, result_data=None): - """标记任务执行成功""" - self.status = 'success' - self.completed_at = timezone.now() - self.progress = 100 - if result_data: - self.result = result_data - self.save() - - def complete_failure(self, error_msg): - """标记任务执行失败""" - self.status = 'failed' - self.completed_at = timezone.now() - self.error_message = error_msg - self.save() - - def cancel_task(self): - """取消任务""" - self.status = 'cancelled' - self.completed_at = timezone.now() - self.save() - - @property - def duration(self): - """任务执行时长""" - if self.completed_at and self.started_at: - return self.completed_at - self.started_at - elif self.started_at: - return timezone.now() - self.started_at - return None - - -class TaskProgress(models.Model): - """任务进度详情模型""" - task = models.ForeignKey(AsyncTask, on_delete=models.CASCADE, related_name='progress_updates') - progress = models.IntegerField(verbose_name="进度百分比") - message = models.TextField(blank=True, null=True, verbose_name="进度消息") - timestamp = models.DateTimeField(auto_now_add=True, verbose_name="时间戳") - - class Meta: - verbose_name = "任务进度" - verbose_name_plural = "任务进度" - db_table = "task_progress" - ordering = ['-timestamp'] - - def __str__(self): - return f"{self.task.name} - {self.progress}%" \ No newline at end of file diff --git a/apps/tasks/urls.py b/apps/tasks/urls.py deleted file mode 100644 index 2c9102b..0000000 --- a/apps/tasks/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path( - '/', - views.async_task_status, - name='async_task_status', - ), -] diff --git a/apps/tasks/views.py b/apps/tasks/views.py deleted file mode 100644 index 366bdee..0000000 --- a/apps/tasks/views.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging - -from django.http import JsonResponse -from django.contrib.auth.decorators import login_required - -from apps.tasks.models import AsyncTask - -logger = logging.getLogger(__name__) - - -@login_required -def async_task_status(request, task_id): - try: - task = AsyncTask.objects.get(task_id=task_id) - except AsyncTask.DoesNotExist: - return JsonResponse( - {'success': False, 'error': '任务不存在'}, - status=404, - ) - - data = { - 'success': True, - 'task_id': task.task_id, - 'name': task.name, - 'status': task.status, - 'progress': task.progress, - 'created_at': task.created_at.isoformat() if task.created_at else None, - 'started_at': task.started_at.isoformat() if task.started_at else None, - 'completed_at': task.completed_at.isoformat() if task.completed_at else None, - 'error_message': task.error_message, - } - - if task.status in ('success', 'failed') and task.result: - if isinstance(task.result, dict): - data.update(task.result) - - return JsonResponse(data) diff --git a/apps/themes/__init__.py b/apps/themes/__init__.py deleted file mode 100755 index b967415..0000000 --- a/apps/themes/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -主题应用模块 - 提供多主题系统支持 - -功能: -1. Material Design 3 和 Neumorphism (新拟态) 主题 -2. 可编辑的页面内容管理 -3. 仪表盘组件布局配置 -4. CSS 变量系统 -5. 移动端响应式适配 -""" -default_app_config = 'apps.themes.apps.ThemesConfig' diff --git a/apps/themes/admin.py b/apps/themes/admin.py deleted file mode 100755 index 2d74e98..0000000 --- a/apps/themes/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.themes.views_admin) diff --git a/apps/themes/apps.py b/apps/themes/apps.py deleted file mode 100755 index 343ee6d..0000000 --- a/apps/themes/apps.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -主题应用配置 -""" -from django.apps import AppConfig - - -class ThemesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.themes' - verbose_name = '主题管理' - - def ready(self): - """应用启动时执行""" - # 可以在这里注册信号等 - pass diff --git a/apps/themes/context_processors.py b/apps/themes/context_processors.py deleted file mode 100755 index 75e035d..0000000 --- a/apps/themes/context_processors.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -主题上下文处理器 - -将主题配置和页面内容注入到所有模板上下文中 -使用缓存优化性能 -""" -from .models import ThemeConfig, PageContent - - -def theme_context(request): - """ - 主题上下文处理器 - - 注入以下变量到模板: - - theme_config: ThemeConfig 实例 - - page_contents: {position: PageContent} 字典 - - theme_css_url: 当前主题的 CSS 文件路径 - - custom_css_vars: 自定义 CSS 变量字符串 - - Returns: - dict: 模板上下文变量 - """ - # 获取主题配置(带缓存) - config = ThemeConfig.get_config() - - # 获取所有启用的页面内容(带缓存) - contents = PageContent.get_all_enabled() - - # 构建 CSS 文件路径 - theme_css_url = f'css/themes/{config.active_theme}.css' - - # 生成自定义 CSS 变量 - custom_css_vars = config.generate_css_variables() - - return { - 'theme_config': config, - 'page_contents': contents, - 'theme_css_url': theme_css_url, - 'custom_css_vars': custom_css_vars, - } diff --git a/apps/themes/forms_admin.py b/apps/themes/forms_admin.py deleted file mode 100644 index 7dac635..0000000 --- a/apps/themes/forms_admin.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -主题系统超级管理员表单 -""" - -from django import forms -from .models import ThemeConfig, PageContent, WidgetLayout - - -class ThemeConfigForm(forms.ModelForm): - """主题配置表单(单例)""" - - class Meta: - model = ThemeConfig - fields = [ - 'active_theme', - 'branding', - 'custom_colors', - 'css_overrides', - 'enable_mobile_optimization', - ] - - def clean_branding(self): - """验证 branding 为有效 JSON""" - import json - data = self.cleaned_data.get('branding') - if data and isinstance(data, str): - try: - json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('品牌资源必须是有效的 JSON 格式') - return data - - def clean_custom_colors(self): - """验证 custom_colors 为有效 JSON""" - import json - data = self.cleaned_data.get('custom_colors') - if data and isinstance(data, str): - try: - json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('自定义颜色必须是有效的 JSON 格式') - return data - - -class PageContentForm(forms.ModelForm): - """页面内容表单""" - - class Meta: - model = PageContent - fields = [ - 'position', - 'title', - 'content', - 'is_enabled', - 'metadata', - ] - - def clean_metadata(self): - """验证 metadata 为有效 JSON""" - import json - data = self.cleaned_data.get('metadata') - if data and isinstance(data, str): - try: - json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('元数据必须是有效的 JSON 格式') - return data - - -class WidgetLayoutForm(forms.ModelForm): - """组件布局表单""" - - class Meta: - model = WidgetLayout - fields = [ - 'widget_type', - 'display_order', - 'column_span', - 'row_span', - 'is_visible', - 'responsive', - ] - - def clean_responsive(self): - """验证 responsive 为有效 JSON""" - import json - data = self.cleaned_data.get('responsive') - if data and isinstance(data, str): - try: - json.loads(data) - except json.JSONDecodeError: - raise forms.ValidationError('响应式配置必须是有效的 JSON 格式') - return data diff --git a/apps/themes/migrations/0001_initial.py b/apps/themes/migrations/0001_initial.py deleted file mode 100644 index 1dab1a0..0000000 --- a/apps/themes/migrations/0001_initial.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-04 08:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='ThemeConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('active_theme', models.CharField(choices=[('material-design-3', 'Material Design 3'), ('neumorphism', '新拟态')], db_index=True, default='material-design-3', max_length=50, verbose_name='当前主题')), - ('branding', models.JSONField(blank=True, default=dict, help_text='存储品牌资源路径:logo, logo_dark, favicon, login_bg', verbose_name='品牌资源')), - ('custom_colors', models.JSONField(blank=True, default=dict, help_text='自定义 CSS 颜色变量', verbose_name='自定义颜色')), - ('css_overrides', models.TextField(blank=True, help_text='自定义 CSS 样式覆盖', verbose_name='自定义 CSS')), - ('enable_mobile_optimization', models.BooleanField(default=True, verbose_name='启用移动端优化')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '主题配置', - 'verbose_name_plural': '主题配置', - }, - ), - migrations.CreateModel( - name='WidgetLayout', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('widget_type', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='组件类型')), - ('display_order', models.PositiveIntegerField(db_index=True, default=0, verbose_name='显示顺序')), - ('column_span', models.PositiveSmallIntegerField(choices=[(1, '1列'), (2, '2列'), (3, '3列'), (4, '全宽')], default=1, help_text='在12列栅格中占据的列数比例', verbose_name='列跨度')), - ('row_span', models.PositiveSmallIntegerField(choices=[(1, '1行'), (2, '2行')], default=1, verbose_name='行跨度')), - ('is_visible', models.BooleanField(db_index=True, default=True, verbose_name='是否显示')), - ('responsive', models.JSONField(blank=True, default=dict, help_text='各设备的显示配置', verbose_name='响应式配置')), - ], - options={ - 'verbose_name': '组件布局', - 'verbose_name_plural': '组件布局', - 'ordering': ['display_order'], - 'indexes': [models.Index(fields=['display_order', 'is_visible'], name='themes_widg_display_b4e1ac_idx')], - }, - ), - migrations.CreateModel( - name='PageContent', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('position', models.CharField(choices=[('login_welcome', '登录页欢迎语'), ('login_subtitle', '登录页副标题'), ('dashboard_notice', '仪表盘公告'), ('footer_text', '页脚文字'), ('footer_copyright', '版权信息'), ('maintenance_message', '维护提示'), ('register_terms', '注册条款')], db_index=True, max_length=50, unique=True, verbose_name='位置标识')), - ('title', models.CharField(blank=True, max_length=200, verbose_name='标题')), - ('content', models.TextField(blank=True, help_text='支持 HTML 格式', verbose_name='内容')), - ('is_enabled', models.BooleanField(db_index=True, default=True, verbose_name='是否启用')), - ('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外配置:icon, color, link 等', verbose_name='元数据')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '页面内容', - 'verbose_name_plural': '页面内容', - 'indexes': [models.Index(fields=['position', 'is_enabled'], name='themes_page_positio_508e18_idx')], - }, - ), - ] diff --git a/apps/themes/migrations/__init__.py b/apps/themes/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/themes/models.py b/apps/themes/models.py deleted file mode 100755 index 34d51b0..0000000 --- a/apps/themes/models.py +++ /dev/null @@ -1,320 +0,0 @@ -""" -主题系统数据模型 - -优化设计原则: -1. 使用 JSONField 存储灵活配置,减少字段膨胀 -2. 利用 Django 缓存机制,避免重复查询 -3. 单例模式通过 get_or_create 实现,无需强制 pk=1 -""" -from django.db import models -from django.core.cache import cache -from django.utils import timezone - - -class ThemeConfig(models.Model): - """ - 主题配置模型(单例) - - 存储全局主题设置、品牌资源和自定义 CSS 变量 - 使用缓存优化查询性能 - """ - - THEME_CHOICES = [ - ('material-design-3', 'Material Design 3'), - ('neumorphism', '新拟态'), - ] - - CACHE_KEY = 'theme_config_singleton' - CACHE_TIMEOUT = 3600 # 1小时缓存 - - # 基础主题设置 - active_theme = models.CharField( - '当前主题', - max_length=50, - choices=THEME_CHOICES, - default='material-design-3', - db_index=True - ) - - # 品牌资源 - 使用单一 JSONField 存储路径 - # 结构: {"logo": "path", "logo_dark": "path", "favicon": "path", "login_bg": "path"} - branding = models.JSONField( - '品牌资源', - default=dict, - blank=True, - help_text='存储品牌资源路径:logo, logo_dark, favicon, login_bg' - ) - - # 自定义颜色 - JSONField 存储所有颜色变量 - # 结构: {"primary": "#xxx", "secondary": "#xxx", "accent": "#xxx", ...} - custom_colors = models.JSONField( - '自定义颜色', - default=dict, - blank=True, - help_text='自定义 CSS 颜色变量' - ) - - # 高级 CSS 变量覆盖 - css_overrides = models.TextField( - '自定义 CSS', - blank=True, - help_text='自定义 CSS 样式覆盖' - ) - - # 移动端适配开关 - enable_mobile_optimization = models.BooleanField( - '启用移动端优化', - default=True - ) - - # 时间戳 - updated_at = models.DateTimeField('更新时间', auto_now=True) - - class Meta: - verbose_name = '主题配置' - verbose_name_plural = verbose_name - - def __str__(self): - return f'主题配置 - {self.get_active_theme_display()}' - - def save(self, *args, **kwargs): - """保存时清除缓存""" - super().save(*args, **kwargs) - cache.delete(self.CACHE_KEY) - - def delete(self, *args, **kwargs): - """删除时清除缓存""" - cache.delete(self.CACHE_KEY) - super().delete(*args, **kwargs) - - @classmethod - def get_config(cls): - """ - 获取配置单例(带缓存) - - Returns: - ThemeConfig: 配置实例 - """ - config = cache.get(cls.CACHE_KEY) - if config is None: - config, _ = cls.objects.get_or_create(pk=1) - cache.set(cls.CACHE_KEY, config, cls.CACHE_TIMEOUT) - return config - - @classmethod - def invalidate_cache(cls): - """手动清除缓存""" - cache.delete(cls.CACHE_KEY) - - def get_branding(self, key, default=''): - """安全获取品牌资源路径""" - return self.branding.get(key, default) if self.branding else default - - def get_color(self, key, default=None): - """安全获取自定义颜色""" - return self.custom_colors.get(key, default) if self.custom_colors else default - - def generate_css_variables(self): - """ - 生成 CSS 变量字符串 - - Returns: - str: CSS 变量定义 - """ - if not self.custom_colors: - return '' - - lines = [':root {'] - for key, value in self.custom_colors.items(): - css_key = f'--theme-{key.replace("_", "-")}' - lines.append(f' {css_key}: {value};') - lines.append('}') - return '\n'.join(lines) - - -class PageContent(models.Model): - """ - 可编辑页面内容 - - 使用 position 作为唯一标识符,支持多语言扩展 - """ - - POSITION_CHOICES = [ - ('login_welcome', '登录页欢迎语'), - ('login_subtitle', '登录页副标题'), - ('dashboard_notice', '仪表盘公告'), - ('footer_text', '页脚文字'), - ('footer_copyright', '版权信息'), - ('maintenance_message', '维护提示'), - ('register_terms', '注册条款'), - ] - - CACHE_KEY_PREFIX = 'page_content_' - CACHE_TIMEOUT = 3600 - - position = models.CharField( - '位置标识', - max_length=50, - choices=POSITION_CHOICES, - unique=True, - db_index=True - ) - title = models.CharField( - '标题', - max_length=200, - blank=True - ) - content = models.TextField( - '内容', - blank=True, - help_text='支持 HTML 格式' - ) - is_enabled = models.BooleanField( - '是否启用', - default=True, - db_index=True - ) - # 元数据 - 存储额外配置 - metadata = models.JSONField( - '元数据', - default=dict, - blank=True, - help_text='存储额外配置:icon, color, link 等' - ) - updated_at = models.DateTimeField('更新时间', auto_now=True) - - class Meta: - verbose_name = '页面内容' - verbose_name_plural = verbose_name - indexes = [ - models.Index(fields=['position', 'is_enabled']), - ] - - def __str__(self): - return f'{self.get_position_display()}' - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - cache.delete(f'{self.CACHE_KEY_PREFIX}{self.position}') - cache.delete(f'{self.CACHE_KEY_PREFIX}all') - - def delete(self, *args, **kwargs): - cache.delete(f'{self.CACHE_KEY_PREFIX}{self.position}') - cache.delete(f'{self.CACHE_KEY_PREFIX}all') - super().delete(*args, **kwargs) - - @classmethod - def get_content(cls, position, default=''): - """ - 获取指定位置的内容(带缓存) - - Args: - position: 位置标识 - default: 默认值 - - Returns: - str: 内容文本 - """ - cache_key = f'{cls.CACHE_KEY_PREFIX}{position}' - result = cache.get(cache_key) - - if result is None: - try: - obj = cls.objects.get(position=position, is_enabled=True) - result = obj.content - except cls.DoesNotExist: - result = default - cache.set(cache_key, result, cls.CACHE_TIMEOUT) - - return result - - @classmethod - def get_all_enabled(cls): - """ - 获取所有启用的内容(带缓存) - - Returns: - dict: {position: PageContent} - """ - cache_key = f'{cls.CACHE_KEY_PREFIX}all' - result = cache.get(cache_key) - - if result is None: - result = { - obj.position: obj - for obj in cls.objects.filter(is_enabled=True) - } - cache.set(cache_key, result, cls.CACHE_TIMEOUT) - - return result - - -class WidgetLayout(models.Model): - """ - 仪表盘组件布局配置 - - 与 dashboard.DashboardWidget 配合使用,提供布局控制 - 此模型只存储布局信息,不复制组件数据 - """ - - # 关联到 dashboard.DashboardWidget 的 widget_type - widget_type = models.CharField( - '组件类型', - max_length=50, - unique=True, - db_index=True - ) - - # 布局配置 - display_order = models.PositiveIntegerField( - '显示顺序', - default=0, - db_index=True - ) - column_span = models.PositiveSmallIntegerField( - '列跨度', - default=1, - choices=[(1, '1列'), (2, '2列'), (3, '3列'), (4, '全宽')], - help_text='在12列栅格中占据的列数比例' - ) - row_span = models.PositiveSmallIntegerField( - '行跨度', - default=1, - choices=[(1, '1行'), (2, '2行')], - ) - is_visible = models.BooleanField( - '是否显示', - default=True, - db_index=True - ) - - # 响应式配置 - JSONField 存储各断点的显示设置 - # 结构: {"mobile": true, "tablet": true, "desktop": true} - responsive = models.JSONField( - '响应式配置', - default=dict, - blank=True, - help_text='各设备的显示配置' - ) - - class Meta: - verbose_name = '组件布局' - verbose_name_plural = verbose_name - ordering = ['display_order'] - indexes = [ - models.Index(fields=['display_order', 'is_visible']), - ] - - def __str__(self): - return f'{self.widget_type} - 顺序:{self.display_order}' - - def get_responsive(self, device, default=True): - """获取特定设备的显示设置""" - if not self.responsive: - return default - return self.responsive.get(device, default) - - def get_column_class(self): - """获取 Bootstrap 列 CSS 类""" - span_map = {1: 'col-md-3', 2: 'col-md-6', 3: 'col-md-9', 4: 'col-12'} - return span_map.get(self.column_span, 'col-md-3') diff --git a/apps/themes/templatetags/__init__.py b/apps/themes/templatetags/__init__.py deleted file mode 100755 index bed1cf1..0000000 --- a/apps/themes/templatetags/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# 模板标签包 diff --git a/apps/themes/templatetags/theme_tags.py b/apps/themes/templatetags/theme_tags.py deleted file mode 100755 index f608c0c..0000000 --- a/apps/themes/templatetags/theme_tags.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -主题系统模板标签 - -提供便捷的模板标签获取主题配置和页面内容 -""" -from django import template -from django.utils.safestring import mark_safe -from ..models import ThemeConfig, PageContent - -register = template.Library() - - -@register.simple_tag -def get_content(position, default=''): - """ - 获取指定位置的页面内容 - - 用法: - {% load theme_tags %} - {% get_content 'login_welcome' as welcome_msg %} - {{ welcome_msg }} - - 或直接输出: - {% get_content 'footer_text' '默认页脚' %} - - Args: - position: 位置标识符 - default: 默认值 - - Returns: - str: 内容文本 - """ - return PageContent.get_content(position, default) - - -@register.simple_tag -def get_content_obj(position): - """ - 获取指定位置的 PageContent 对象 - - 用法: - {% get_content_obj 'dashboard_notice' as notice %} - {% if notice %} -

{{ notice.title }}

- {{ notice.content|safe }} - {% endif %} - - Args: - position: 位置标识符 - - Returns: - PageContent 或 None - """ - contents = PageContent.get_all_enabled() - return contents.get(position) - - -@register.simple_tag -def theme_css_url(): - """ - 获取当前主题的 CSS 文件 URL - - 用法: - - - Returns: - str: CSS 文件路径 - """ - config = ThemeConfig.get_config() - return f'css/themes/{config.active_theme}.css' - - -@register.simple_tag -def theme_data_attribute(): - """ - 获取用于 HTML 标签的 data-theme 属性值 - - 用法: - - - Returns: - str: 主题标识符 - """ - config = ThemeConfig.get_config() - return config.active_theme - - -@register.simple_tag -def branding(key, default=''): - """ - 获取品牌资源路径 - - 用法: - - - Args: - key: 资源键名 (logo, logo_dark, favicon, login_bg) - default: 默认路径 - - Returns: - str: 资源路径 - """ - config = ThemeConfig.get_config() - return config.get_branding(key, default) - - -@register.simple_tag -def theme_color(key, default=''): - """ - 获取自定义颜色值 - - 用法: -
- - Args: - key: 颜色键名 - default: 默认颜色值 - - Returns: - str: 颜色值 - """ - config = ThemeConfig.get_config() - return config.get_color(key, default) - - -@register.simple_tag -def custom_css_variables(): - """ - 输出自定义 CSS 变量样式块 - - 用法: - - - - - Returns: - str: CSS 变量定义 - """ - config = ThemeConfig.get_config() - css = config.generate_css_variables() - if config.css_overrides: - css += '\n' + config.css_overrides - return mark_safe(css) - - -@register.inclusion_tag('themes/partials/theme_head.html') -def theme_head(): - """ - 包含主题相关的 内容 - - 用法: - - {% theme_head %} - - - Returns: - dict: 模板上下文 - """ - config = ThemeConfig.get_config() - return { - 'theme_config': config, - 'theme_css_url': f'css/themes/{config.active_theme}.css', - 'custom_css': config.generate_css_variables(), - 'css_overrides': config.css_overrides, - } - - -@register.filter -def split(value, delimiter=","): - return value.split(delimiter) - - -@register.filter -def trim(value): - if isinstance(value, str): - return value.strip() - return value - - -@register.filter -def is_mobile_enabled(config): - """ - 检查是否启用移动端优化 - - 用法: - {% if theme_config|is_mobile_enabled %} - - {% endif %} - - Args: - config: ThemeConfig 实例 - - Returns: - bool - """ - if config is None: - return True - return getattr(config, 'enable_mobile_optimization', True) diff --git a/apps/themes/urls_admin.py b/apps/themes/urls_admin.py deleted file mode 100644 index f80e05a..0000000 --- a/apps/themes/urls_admin.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.urls import path - -from .views_admin import ( - themeconfig_edit, - themeconfig_clear_cache, - pagecontent_list, - pagecontent_create, - pagecontent_edit, - pagecontent_delete, - widgetlayout_list, - widgetlayout_create, - widgetlayout_edit, - widgetlayout_delete, -) - -app_name = 'admin_themes' - -urlpatterns = [ - path('config/', themeconfig_edit, name='themeconfig_edit'), - path( - 'config/clear-cache/', - themeconfig_clear_cache, - name='themeconfig_clear_cache', - ), - path('pages/', pagecontent_list, name='pagecontent_list'), - path('pages/create/', pagecontent_create, name='pagecontent_create'), - path( - 'pages//edit/', - pagecontent_edit, - name='pagecontent_edit', - ), - path( - 'pages//delete/', - pagecontent_delete, - name='pagecontent_delete', - ), - path('layouts/', widgetlayout_list, name='widgetlayout_list'), - path( - 'layouts/create/', - widgetlayout_create, - name='widgetlayout_create', - ), - path( - 'layouts//edit/', - widgetlayout_edit, - name='widgetlayout_edit', - ), - path( - 'layouts//delete/', - widgetlayout_delete, - name='widgetlayout_delete', - ), -] diff --git a/apps/themes/views_admin.py b/apps/themes/views_admin.py deleted file mode 100644 index ffbcb06..0000000 --- a/apps/themes/views_admin.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -主题系统超级管理员视图 - -包含: -- ThemeConfig 单例编辑 + 清除缓存 -- PageContent CRUD -- WidgetLayout CRUD -""" - -import logging - -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.core.paginator import Paginator -from django.core.cache import cache -from django.views.decorators.http import require_POST - -from apps.accounts.provider_decorators import superadmin_required -from .models import ThemeConfig, PageContent, WidgetLayout -from .forms_admin import ThemeConfigForm, PageContentForm, WidgetLayoutForm - -logger = logging.getLogger('2c2a') - - -# ============================================================ -# ThemeConfig 单例编辑 + 清除缓存 -# ============================================================ - -@superadmin_required -def themeconfig_edit(request): - """主题配置编辑(单例,自动 get_or_create)""" - config, _ = ThemeConfig.objects.get_or_create(pk=1) - - if request.method == 'POST': - form = ThemeConfigForm(request.POST, instance=config) - if form.is_valid(): - form.save() - messages.success(request, '主题配置已更新,缓存已自动清除。') - return redirect('admin_themes:themeconfig_edit') - else: - form = ThemeConfigForm(instance=config) - - context = { - 'form': form, - 'config': config, - 'active_nav': 'themes_config', - } - return render( - request, 'admin_base/themes/themeconfig_edit.html', context - ) - - -@superadmin_required -@require_POST -def themeconfig_clear_cache(request): - """清除主题缓存""" - ThemeConfig.invalidate_cache() - # 尝试清除页面内容缓存 - if hasattr(cache, 'delete_pattern'): - try: - cache.delete_pattern('page_content_*') - except Exception: - pass - else: - # 手动清除已知位置的页面内容缓存 - for key, _ in PageContent.POSITION_CHOICES: - cache.delete(f'{PageContent.CACHE_KEY_PREFIX}{key}') - cache.delete(f'{PageContent.CACHE_KEY_PREFIX}all') - - messages.success(request, '主题缓存已清除。') - return redirect('admin_themes:themeconfig_edit') - - -# ============================================================ -# PageContent CRUD -# ============================================================ - -@superadmin_required -def pagecontent_list(request): - """页面内容列表""" - queryset = PageContent.objects.order_by('position') - - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - title__icontains=search - ) | queryset.filter( - content__icontains=search - ) - - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'search': search, - 'active_nav': 'themes_pages', - } - return render( - request, 'admin_base/themes/pagecontent_list.html', context - ) - - -@superadmin_required -def pagecontent_create(request): - """创建页面内容""" - if request.method == 'POST': - form = PageContentForm(request.POST) - if form.is_valid(): - page = form.save() - messages.success( - request, f'页面内容「{page}」创建成功。' - ) - return redirect('admin_themes:pagecontent_list') - else: - form = PageContentForm() - - context = { - 'form': form, - 'active_nav': 'themes_pages', - 'is_create': True, - } - return render( - request, 'admin_base/themes/pagecontent_form.html', context - ) - - -@superadmin_required -def pagecontent_edit(request, pk): - """编辑页面内容""" - page = get_object_or_404(PageContent, pk=pk) - - if request.method == 'POST': - form = PageContentForm(request.POST, instance=page) - if form.is_valid(): - form.save() - messages.success( - request, f'页面内容「{page}」更新成功。' - ) - return redirect('admin_themes:pagecontent_list') - else: - form = PageContentForm(instance=page) - - context = { - 'form': form, - 'page': page, - 'active_nav': 'themes_pages', - 'is_create': False, - } - return render( - request, 'admin_base/themes/pagecontent_form.html', context - ) - - -@superadmin_required -def pagecontent_delete(request, pk): - """删除页面内容""" - page = get_object_or_404(PageContent, pk=pk) - - if request.method == 'POST': - label = str(page) - page.delete() - messages.success( - request, f'页面内容「{label}」已删除。' - ) - return redirect('admin_themes:pagecontent_list') - - context = { - 'page': page, - 'active_nav': 'themes_pages', - } - return render( - request, 'admin_base/themes/pagecontent_confirm_delete.html', context - ) - - -# ============================================================ -# WidgetLayout CRUD -# ============================================================ - -@superadmin_required -def widgetlayout_list(request): - """组件布局列表""" - queryset = WidgetLayout.objects.order_by('display_order') - - search = request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - widget_type__icontains=search - ) - - paginator = Paginator(queryset, 25) - page_number = request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context = { - 'page_obj': page_obj, - 'search': search, - 'active_nav': 'themes_layouts', - } - return render( - request, 'admin_base/themes/widgetlayout_list.html', context - ) - - -@superadmin_required -def widgetlayout_create(request): - """创建组件布局""" - if request.method == 'POST': - form = WidgetLayoutForm(request.POST) - if form.is_valid(): - layout = form.save() - messages.success( - request, f'组件布局「{layout}」创建成功。' - ) - return redirect('admin_themes:widgetlayout_list') - else: - form = WidgetLayoutForm() - - context = { - 'form': form, - 'active_nav': 'themes_layouts', - 'is_create': True, - } - return render( - request, 'admin_base/themes/widgetlayout_form.html', context - ) - - -@superadmin_required -def widgetlayout_edit(request, pk): - """编辑组件布局""" - layout = get_object_or_404(WidgetLayout, pk=pk) - - if request.method == 'POST': - form = WidgetLayoutForm(request.POST, instance=layout) - if form.is_valid(): - form.save() - messages.success( - request, f'组件布局「{layout}」更新成功。' - ) - return redirect('admin_themes:widgetlayout_list') - else: - form = WidgetLayoutForm(instance=layout) - - context = { - 'form': form, - 'layout': layout, - 'active_nav': 'themes_layouts', - 'is_create': False, - } - return render( - request, 'admin_base/themes/widgetlayout_form.html', context - ) - - -@superadmin_required -def widgetlayout_delete(request, pk): - """删除组件布局""" - layout = get_object_or_404(WidgetLayout, pk=pk) - - if request.method == 'POST': - label = str(layout) - layout.delete() - messages.success( - request, f'组件布局「{label}」已删除。' - ) - return redirect('admin_themes:widgetlayout_list') - - context = { - 'layout': layout, - 'active_nav': 'themes_layouts', - } - return render( - request, 'admin_base/themes/widgetlayout_confirm_delete.html', - context, - ) diff --git a/apps/tickets/__init__.py b/apps/tickets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tickets/admin.py b/apps/tickets/admin.py deleted file mode 100644 index 69d4025..0000000 --- a/apps/tickets/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (apps.tickets.views_admin) 和提供商后台 (apps.tickets.views_provider) diff --git a/apps/tickets/apps.py b/apps/tickets/apps.py deleted file mode 100644 index dac76d3..0000000 --- a/apps/tickets/apps.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.apps import AppConfig - - -class TicketsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.tickets' - verbose_name = '工单系统' - - def ready(self): - """ - 应用就绪时导入信号处理器 - """ - import apps.tickets.signals # noqa: F401 diff --git a/apps/tickets/audit_integration.py b/apps/tickets/audit_integration.py deleted file mode 100644 index f2f949c..0000000 --- a/apps/tickets/audit_integration.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -工单系统审计日志集成 - -将工单相关操作记录到审计日志系统 -""" - -from django.contrib.contenttypes.models import ContentType -from apps.audit.models import AuditLog - - -def log_ticket_action(ticket, user, action, ip_address=None, details=None, success=True, result=None): - """ - 记录工单操作到审计日志 - - Args: - ticket: 工单实例 - user: 操作用户 - action: 操作类型(需要在 AuditLog.ACTION_CHOICES 中定义) - ip_address: 操作IP地址 - details: 操作详情(字典) - success: 操作是否成功 - result: 操作结果 - """ - # 获取工单的内容类型 - ticket_content_type = ContentType.objects.get_for_model(ticket) - - # 构建操作详情 - log_details = { - 'ticket_no': ticket.ticket_no, - 'ticket_title': ticket.title, - 'ticket_status': ticket.status, - 'ticket_priority': ticket.priority, - } - - if details: - log_details.update(details) - - # 创建审计日志 - AuditLog.objects.create( - user=user, - action=action, - ip_address=ip_address, - success=success, - details=log_details, - result=result, - content_type=ticket_content_type, - object_id=ticket.pk - ) - - -def log_ticket_created(ticket, user, ip_address=None): - """记录工单创建""" - log_ticket_action( - ticket=ticket, - user=user, - action='create_ticket', - ip_address=ip_address, - details={'action': '创建工单'} - ) - - -def log_ticket_updated(ticket, user, ip_address=None, changes=None): - """记录工单更新""" - log_ticket_action( - ticket=ticket, - user=user, - action='update_ticket', - ip_address=ip_address, - details={'action': '更新工单', 'changes': changes or {}} - ) - - -def log_ticket_assigned(ticket, user, old_assignee, new_assignee, - old_assigned_group=None, new_assigned_group=None, - ip_address=None): - """记录工单分配""" - details = { - 'action': '分配工单', - 'old_assignee': str(old_assignee) if old_assignee else None, - 'new_assignee': str(new_assignee) if new_assignee else None, - 'old_assigned_group': str(old_assigned_group) if old_assigned_group else None, - 'new_assigned_group': str(new_assigned_group) if new_assigned_group else None, - } - log_ticket_action( - ticket=ticket, - user=user, - action='assign_ticket', - ip_address=ip_address, - details=details - ) - - -def log_ticket_status_changed(ticket, user, old_status, new_status, ip_address=None): - """记录工单状态变更""" - log_ticket_action( - ticket=ticket, - user=user, - action='change_ticket_status', - ip_address=ip_address, - details={ - 'action': '变更工单状态', - 'old_status': old_status, - 'new_status': new_status, - } - ) - - -def log_ticket_closed(ticket, user, ip_address=None, satisfaction=None): - """记录工单关闭""" - log_ticket_action( - ticket=ticket, - user=user, - action='close_ticket', - ip_address=ip_address, - details={ - 'action': '关闭工单', - 'satisfaction': satisfaction, - } - ) - - -def log_ticket_comment_added(comment, user, ip_address=None): - """记录工单评论添加""" - log_ticket_action( - ticket=comment.ticket, - user=user, - action='add_ticket_comment', - ip_address=ip_address, - details={ - 'action': '添加评论', - 'comment_id': comment.pk, - 'is_internal': comment.is_internal, - } - ) diff --git a/apps/tickets/forms.py b/apps/tickets/forms.py deleted file mode 100644 index 00dc2b5..0000000 --- a/apps/tickets/forms.py +++ /dev/null @@ -1,317 +0,0 @@ -""" -工单系统表单 -""" -from django import forms -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group - -from .models import Ticket, TicketComment, TicketCategory -from apps.accounts.models import UserBan - -User = get_user_model() - - -MD3_INPUT_CLASS = ( - 'w-full bg-md-surface/50 border border-md-outline/50 rounded-md px-4 py-3 ' - 'text-md-on-surface placeholder-md-outline focus:outline-none focus:ring-2 ' - 'focus:ring-md-primary transition' -) - - -class TicketForm(forms.ModelForm): - """ - 工单创建/编辑表单 - """ - title = forms.CharField( - widget=forms.TextInput(attrs={ - 'class': MD3_INPUT_CLASS, - 'placeholder': '请输入工单标题' - }), - label=_('标题'), - help_text=_('请简要描述工单主题') - ) - - description = forms.CharField( - widget=forms.Textarea(attrs={ - 'class': MD3_INPUT_CLASS, - 'rows': 6, - 'placeholder': '请详细描述您的问题或需求' - }), - label=_('详细描述'), - help_text=_('请提供尽可能详细的信息,以便我们更好地帮助您') - ) - - category = forms.ModelChoiceField( - queryset=TicketCategory.objects.filter(is_active=True), - widget=forms.Select(attrs={'class': MD3_INPUT_CLASS}), - label=_('分类'), - help_text=_('请选择工单分类'), - required=False - ) - - priority = forms.ChoiceField( - choices=Ticket.PRIORITY_CHOICES, - widget=forms.Select(attrs={'class': MD3_INPUT_CLASS}), - label=_('优先级'), - help_text=_('请选择工单优先级'), - initial='medium' - ) - - related_cloud_computer = forms.ModelChoiceField( - queryset=None, - widget=forms.Select(attrs={'class': MD3_INPUT_CLASS}), - label=_('关联云电脑'), - help_text=_('选择您拥有的云电脑(可选)'), - required=False - ) - - related_request = forms.ModelChoiceField( - queryset=None, - widget=forms.Select(attrs={'class': MD3_INPUT_CLASS}), - label=_('关联申请'), - help_text=_('选择您提交的申请(可选)'), - required=False - ) - - class Meta: - model = Ticket - fields = ['title', 'description', 'category', 'priority', 'related_cloud_computer', 'related_request'] - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) - super().__init__(*args, **kwargs) - from apps.operations.models import CloudComputerUser, AccountOpeningRequest - - # 关联云电脑:只显示当前用户拥有的、激活状态的云电脑 - if self.user: - self.fields['related_cloud_computer'].queryset = CloudComputerUser.objects.filter( - owner=self.user, status='active' - ).select_related('product') - # 关联申请:只显示当前用户提交的申请 - self.fields['related_request'].queryset = AccountOpeningRequest.objects.filter( - applicant=self.user - ).select_related('target_product').order_by('-created_at') - else: - self.fields['related_cloud_computer'].queryset = CloudComputerUser.objects.none() - self.fields['related_request'].queryset = AccountOpeningRequest.objects.none() - - # 封禁用户只能选择允许封禁用户提交的分类 - if self.user and UserBan.objects.filter(user=self.user).exists(): - self.fields['category'].queryset = TicketCategory.objects.filter( - is_active=True, allow_banned_users=True - ) - # 封禁用户不需要关联云电脑和申请 - self.fields['related_cloud_computer'].queryset = CloudComputerUser.objects.none() - self.fields['related_request'].queryset = AccountOpeningRequest.objects.none() - - # 如果有分类,设置默认优先级 - if self.data.get('category'): - try: - category = TicketCategory.objects.get(pk=self.data['category']) - self.fields['priority'].initial = category.default_priority - except TicketCategory.DoesNotExist: - pass - - def clean(self): - cleaned_data = super().clean() - - # 如果有分类,继承分类的默认优先级(如果用户未修改) - category = cleaned_data.get('category') - priority = cleaned_data.get('priority') - - if category and priority == 'medium' and category.default_priority != 'medium': - # 如果用户没有显式修改优先级,使用分类默认值 - if not self.data.get('priority') or self.data.get('priority') == 'medium': - cleaned_data['priority'] = category.default_priority - - return cleaned_data - - -class TicketCommentForm(forms.ModelForm): - """ - 工单评论表单 - """ - content = forms.CharField( - widget=forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 3, - 'placeholder': '请输入评论内容' - }), - label=_('内容'), - help_text=_('请输入您的回复') - ) - - is_internal = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}), - label=_('内部备注'), - help_text=_('仅工作人员可见') - ) - - class Meta: - model = TicketComment - fields = ['content', 'is_internal'] - - -class TicketAssignForm(forms.Form): - """ - 工单分配表单 - """ - assignee = forms.ModelChoiceField( - queryset=User.objects.filter(is_active=True), - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('处理人'), - help_text=_('请选择要分配的处理人'), - required=False, - ) - - assigned_group = forms.ModelChoiceField( - queryset=Group.objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('处理组'), - help_text=_('请选择要分配的处理组'), - required=False, - ) - - notes = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 2, - 'placeholder': '可选:添加分配备注' - }), - label=_('备注'), - help_text=_('可选:添加分配备注') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['assignee'].queryset = User.objects.filter( - is_active=True - ).filter( - models.Q(is_staff=True) | models.Q(is_superuser=True) | models.Q(groups__name='主机提供商') - ).distinct() - - def clean(self): - cleaned_data = super().clean() - assignee = cleaned_data.get('assignee') - assigned_group = cleaned_data.get('assigned_group') - if not assignee and not assigned_group: - raise forms.ValidationError(_('请至少选择一个处理人或处理组')) - return cleaned_data - - -class TicketStatusForm(forms.Form): - """ - 工单状态变更表单 - """ - status = forms.ChoiceField( - choices=[ - ('pending', _('待处理')), - ('processing', _('处理中')), - ('waiting_feedback', _('待反馈')), - ('resolved', _('已解决')), - ('closed', _('已关闭')), - ('rejected', _('已驳回')), - ], - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('新状态'), - help_text=_('请选择新的工单状态') - ) - - notes = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 2, - 'placeholder': '可选:添加状态变更备注' - }), - label=_('备注'), - help_text=_('可选:添加状态变更备注') - ) - - -class TicketCloseForm(forms.Form): - """ - 工单关闭表单(含满意度评价) - """ - satisfaction = forms.ChoiceField( - required=False, - choices=[ - ('', _('不评价')), - ('5', _('非常满意')), - ('4', _('满意')), - ('3', _('一般')), - ('2', _('不满意')), - ('1', _('非常不满意')), - ], - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('满意度评分'), - help_text=_('请对工单处理进行评价') - ) - - satisfaction_comment = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'form-control', - 'rows': 3, - 'placeholder': '可选:请输入您的评价内容' - }), - label=_('评价内容'), - help_text=_('可选:请输入您的评价内容') - ) - - -class TicketFilterForm(forms.Form): - """ - 工单筛选表单 - """ - status = forms.ChoiceField( - required=False, - choices=[('', _('全部'))] + Ticket.STATUS_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('状态') - ) - - priority = forms.ChoiceField( - required=False, - choices=[('', _('全部'))] + Ticket.PRIORITY_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('优先级') - ) - - category = forms.ModelChoiceField( - required=False, - queryset=TicketCategory.objects.filter(is_active=True), - widget=forms.Select(attrs={'class': 'form-control'}), - label=_('分类') - ) - - search = forms.CharField( - required=False, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': '搜索工单编号、标题或描述' - }), - label=_('搜索') - ) - - start_date = forms.DateField( - required=False, - widget=forms.DateInput(attrs={ - 'class': 'form-control', - 'type': 'date' - }), - label=_('开始日期') - ) - - end_date = forms.DateField( - required=False, - widget=forms.DateInput(attrs={ - 'class': 'form-control', - 'type': 'date' - }), - label=_('结束日期') - ) diff --git a/apps/tickets/forms_admin.py b/apps/tickets/forms_admin.py deleted file mode 100644 index 50acd56..0000000 --- a/apps/tickets/forms_admin.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -工单系统 - 超管后台表单 - -超管可管理所有数据,无数据隔离。 -""" - -from django import forms -from django.contrib.auth.models import Group -from django.utils.translation import gettext_lazy as _ - -from .models import TicketCategory, TicketComment - - -class AdminTicketCategoryForm(forms.ModelForm): - """ - 工单分类表单(超管后台) - - 超管可设置 auto_assign_to 字段,created_by 在视图中自动设置。 - """ - - name = forms.CharField( - widget=forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '请输入分类名称', - }), - label=_('分类名称'), - ) - - description = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '请输入分类描述(可选)', - }), - label=_('分类描述'), - ) - - icon = forms.CharField( - required=False, - widget=forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': 'Material Icons 图标名称,如 help_outline', - }), - label=_('图标'), - ) - - default_priority = forms.ChoiceField( - choices=TicketCategory._meta.get_field('default_priority').choices, - widget=forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - label=_('默认优先级'), - ) - - auto_assign_to = forms.ModelChoiceField( - required=False, - queryset=None, - widget=forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - label=_('自动分配给'), - help_text=_('该分类的工单自动分配给指定用户'), - ) - - auto_assign_to_group = forms.ModelChoiceField( - required=False, - queryset=Group.objects.all(), - widget=forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - label=_('自动分配给用户组'), - help_text=_('该分类的工单自动分配给指定用户组'), - ) - - sla_hours = forms.IntegerField( - min_value=1, - widget=forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '24', - }), - label=_('SLA时限(小时)'), - ) - - is_active = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('是否启用'), - ) - - allow_banned_users = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('允许封禁用户提交'), - help_text=_('勾选后,被封禁的用户可以在此分类下提交工单'), - ) - - display_order = forms.IntegerField( - initial=0, - widget=forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '0', - }), - label=_('显示顺序'), - ) - - class Meta: - model = TicketCategory - fields = [ - 'name', 'description', 'icon', - 'default_priority', 'auto_assign_to', 'auto_assign_to_group', - 'sla_hours', - 'is_active', 'allow_banned_users', 'display_order', - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - from django.contrib.auth import get_user_model - User = get_user_model() - self.fields['auto_assign_to'].queryset = User.objects.filter( - is_staff=True - ).order_by('username') - - -class AdminTicketCommentForm(forms.ModelForm): - """ - 工单评论表单(超管后台) - - 超管可添加内部备注。 - """ - - content = forms.CharField( - widget=forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '请输入评论内容...', - }), - label=_('评论内容'), - ) - - is_internal = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('内部备注'), - help_text=_('仅工作人员可见'), - ) - - class Meta: - model = TicketComment - fields = ['content', 'is_internal'] diff --git a/apps/tickets/forms_provider.py b/apps/tickets/forms_provider.py deleted file mode 100644 index e2b2bc5..0000000 --- a/apps/tickets/forms_provider.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -工单系统 - 提供商后台表单 - -使用 Tailwind MD3 样式,不使用 Bootstrap。 -""" - -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import TicketCategory, TicketComment, TicketAttachment - - -class TicketCategoryForm(forms.ModelForm): - """ - 工单分类表单(提供商后台) - - created_by 在视图中自动设置为当前用户。 - """ - - name = forms.CharField( - widget=forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '请输入分类名称', - }), - label=_('分类名称'), - ) - - description = forms.CharField( - required=False, - widget=forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '请输入分类描述(可选)', - }), - label=_('分类描述'), - ) - - icon = forms.CharField( - required=False, - widget=forms.TextInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': 'Material Icons 图标名称,如 help_outline', - }), - label=_('图标'), - ) - - default_priority = forms.ChoiceField( - choices=TicketCategory._meta.get_field('default_priority').choices, - widget=forms.Select(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface appearance-none ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer', - }), - label=_('默认优先级'), - ) - - sla_hours = forms.IntegerField( - min_value=1, - widget=forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '24', - }), - label=_('SLA时限(小时)'), - ) - - is_active = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('是否启用'), - ) - - allow_banned_users = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('允许封禁用户提交'), - help_text=_('勾选后,被封禁的用户可以在此分类下提交工单'), - ) - - display_order = forms.IntegerField( - initial=0, - widget=forms.NumberInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition', - 'placeholder': '0', - }), - label=_('显示顺序'), - ) - - class Meta: - model = TicketCategory - fields = [ - 'name', 'description', 'icon', - 'default_priority', 'sla_hours', - 'is_active', 'allow_banned_users', 'display_order', - ] - - -class TicketCommentForm(forms.ModelForm): - """ - 工单评论表单(提供商后台) - - 支持内部备注标记。 - """ - - content = forms.CharField( - widget=forms.Textarea(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'placeholder-md-outline focus:outline-none ' - 'focus:ring-2 focus:ring-md-primary transition resize-y', - 'rows': 3, - 'placeholder': '请输入评论内容...', - }), - label=_('评论内容'), - ) - - is_internal = forms.BooleanField( - required=False, - widget=forms.CheckboxInput(attrs={ - 'class': 'w-5 h-5 rounded border-md-outline/50 bg-md-surface/50 ' - 'text-md-primary focus:ring-md-primary focus:ring-2 ' - 'transition cursor-pointer accent-md-primary', - }), - label=_('内部备注'), - help_text=_('仅工作人员可见'), - ) - - class Meta: - model = TicketComment - fields = ['content', 'is_internal'] - - -class TicketAttachmentForm(forms.ModelForm): - """ - 工单附件上传表单(提供商后台) - """ - - file = forms.FileField( - widget=forms.FileInput(attrs={ - 'class': 'w-full bg-md-surface/50 border border-md-outline/50 ' - 'rounded-md px-4 py-3 text-md-on-surface ' - 'focus:outline-none focus:ring-2 focus:ring-md-primary ' - 'transition cursor-pointer file:mr-4 file:py-1 file:px-4 ' - 'file:rounded-md file:border-0 file:text-sm ' - 'file:font-medium file:bg-md-primary/20 ' - 'file:text-md-primary hover:file:bg-md-primary/30', - }), - label=_('选择文件'), - ) - - class Meta: - model = TicketAttachment - fields = ['file'] diff --git a/apps/tickets/migrations/0001_initial.py b/apps/tickets/migrations/0001_initial.py deleted file mode 100644 index 1e61dfd..0000000 --- a/apps/tickets/migrations/0001_initial.py +++ /dev/null @@ -1,180 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-26 15:23 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('operations', '0011_rename_operations_rdp_domain_idx_operations__domain_5c01f5_idx_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('hosts', '0010_remove_host_host_type'), - ] - - operations = [ - migrations.CreateModel( - name='Ticket', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ticket_no', models.CharField(help_text='自动生成的唯一工单编号', max_length=20, unique=True, verbose_name='工单编号')), - ('title', models.CharField(help_text='工单的简要标题', max_length=200, verbose_name='标题')), - ('description', models.TextField(help_text='工单的详细描述', verbose_name='详细描述')), - ('status', models.CharField(choices=[('pending', '待处理'), ('processing', '处理中'), ('waiting_feedback', '待反馈'), ('resolved', '已解决'), ('closed', '已关闭'), ('rejected', '已驳回')], default='pending', help_text='工单的当前状态', max_length=20, verbose_name='状态')), - ('priority', models.CharField(choices=[('urgent', '紧急'), ('high', '高'), ('medium', '中'), ('low', '低')], default='medium', help_text='工单的优先级', max_length=20, verbose_name='优先级')), - ('source', models.CharField(choices=[('web', 'Web提交'), ('api', 'API创建'), ('system', '系统自动生成'), ('email', '邮件导入')], default='web', help_text='工单的创建来源', max_length=20, verbose_name='来源')), - ('due_at', models.DateTimeField(blank=True, help_text='根据SLA计算的工单处理截止时间', null=True, verbose_name='截止时间')), - ('resolved_at', models.DateTimeField(blank=True, help_text='工单被标记为已解决的时间', null=True, verbose_name='解决时间')), - ('closed_at', models.DateTimeField(blank=True, help_text='工单被关闭的时间', null=True, verbose_name='关闭时间')), - ('satisfaction', models.PositiveSmallIntegerField(blank=True, help_text='用户对工单处理的满意度评分(1-5)', null=True, verbose_name='满意度评分')), - ('satisfaction_comment', models.TextField(blank=True, help_text='用户对工单处理的评价内容', verbose_name='满意度评价')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('assignee', models.ForeignKey(blank=True, help_text='负责处理此工单的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tickets', to=settings.AUTH_USER_MODEL, verbose_name='处理人')), - ], - options={ - 'verbose_name': '工单', - 'verbose_name_plural': '工单', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='TicketCategory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='工单分类的名称', max_length=100, verbose_name='分类名称')), - ('description', models.TextField(blank=True, help_text='分类的详细描述', verbose_name='分类描述')), - ('icon', models.CharField(blank=True, default='help_outline', help_text='Material Icons 图标名称', max_length=50, verbose_name='图标')), - ('default_priority', models.CharField(choices=[('urgent', '紧急'), ('high', '高'), ('medium', '中'), ('low', '低')], default='medium', help_text='该分类下工单的默认优先级', max_length=20, verbose_name='默认优先级')), - ('sla_hours', models.PositiveIntegerField(default=24, help_text='工单处理的服务级别协议时限', verbose_name='SLA时限(小时)')), - ('is_active', models.BooleanField(default=True, help_text='是否在前端展示此分类', verbose_name='是否启用')), - ('display_order', models.IntegerField(default=0, help_text='分类在前端展示的顺序,数字越小越靠前', verbose_name='显示顺序')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('auto_assign_to', models.ForeignKey(blank=True, help_text='该分类的工单自动分配给指定用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='auto_assigned_categories', to=settings.AUTH_USER_MODEL, verbose_name='自动分配给')), - ], - options={ - 'verbose_name': '工单分类', - 'verbose_name_plural': '工单分类', - 'ordering': ['display_order', 'name'], - }, - ), - migrations.CreateModel( - name='TicketAttachment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', models.FileField(help_text='上传的附件文件', upload_to='ticket_attachments/%Y/%m/', verbose_name='文件')), - ('filename', models.CharField(help_text='文件的原始名称', max_length=255, verbose_name='原始文件名')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('ticket', models.ForeignKey(help_text='附件所属的工单', on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='tickets.ticket', verbose_name='关联工单')), - ('uploaded_by', models.ForeignKey(help_text='上传此附件的用户', on_delete=django.db.models.deletion.CASCADE, related_name='ticket_attachments', to=settings.AUTH_USER_MODEL, verbose_name='上传人')), - ], - options={ - 'verbose_name': '工单附件', - 'verbose_name_plural': '工单附件', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='TicketActivity', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('action', models.CharField(choices=[('create', '创建'), ('assign', '分配'), ('status_change', '状态变更'), ('comment', '评论'), ('close', '关闭'), ('update', '更新')], help_text='活动的操作类型', max_length=20, verbose_name='操作类型')), - ('old_value', models.CharField(blank=True, help_text='变更前的值', max_length=255, verbose_name='旧值')), - ('new_value', models.CharField(blank=True, help_text='变更后的值', max_length=255, verbose_name='新值')), - ('description', models.TextField(blank=True, help_text='活动的详细描述', verbose_name='描述')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('actor', models.ForeignKey(blank=True, help_text='执行此操作的用户', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_activities', to=settings.AUTH_USER_MODEL, verbose_name='操作人')), - ('ticket', models.ForeignKey(help_text='活动所属的工单', on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='tickets.ticket', verbose_name='关联工单')), - ], - options={ - 'verbose_name': '工单活动', - 'verbose_name_plural': '工单活动', - 'ordering': ['-created_at'], - }, - ), - migrations.AddField( - model_name='ticket', - name='category', - field=models.ForeignKey(blank=True, help_text='工单所属的分类', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='tickets.ticketcategory', verbose_name='分类'), - ), - migrations.AddField( - model_name='ticket', - name='creator', - field=models.ForeignKey(help_text='提交工单的用户', on_delete=django.db.models.deletion.CASCADE, related_name='created_tickets', to=settings.AUTH_USER_MODEL, verbose_name='创建者'), - ), - migrations.AddField( - model_name='ticket', - name='related_host', - field=models.ForeignKey(blank=True, help_text='工单关联的主机', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='hosts.host', verbose_name='关联主机'), - ), - migrations.AddField( - model_name='ticket', - name='related_product', - field=models.ForeignKey(blank=True, help_text='工单关联的云电脑产品', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets', to='operations.product', verbose_name='关联产品'), - ), - migrations.CreateModel( - name='TicketComment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(help_text='评论的详细内容', verbose_name='内容')), - ('is_internal', models.BooleanField(default=False, help_text='是否为仅工作人员可见的内部备注', verbose_name='内部备注')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('author', models.ForeignKey(help_text='评论的作者', on_delete=django.db.models.deletion.CASCADE, related_name='ticket_comments', to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('ticket', models.ForeignKey(help_text='评论所属的工单', on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.ticket', verbose_name='关联工单')), - ], - options={ - 'verbose_name': '工单评论', - 'verbose_name_plural': '工单评论', - 'ordering': ['created_at'], - 'indexes': [models.Index(fields=['ticket', 'created_at'], name='tickets_tic_ticket__254c10_idx')], - }, - ), - migrations.AddIndex( - model_name='ticketcategory', - index=models.Index(fields=['is_active'], name='tickets_tic_is_acti_d49b55_idx'), - ), - migrations.AddIndex( - model_name='ticketcategory', - index=models.Index(fields=['display_order'], name='tickets_tic_display_3ed776_idx'), - ), - migrations.AddIndex( - model_name='ticketactivity', - index=models.Index(fields=['ticket', 'created_at'], name='tickets_tic_ticket__de2dd5_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['ticket_no'], name='tickets_tic_ticket__c7390a_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['status'], name='tickets_tic_status_0e5646_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['priority'], name='tickets_tic_priorit_0bec9b_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['assignee'], name='tickets_tic_assigne_8f75bf_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['creator'], name='tickets_tic_creator_3210bf_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['category'], name='tickets_tic_categor_3266ff_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['created_at'], name='tickets_tic_created_5dd600_idx'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['due_at'], name='tickets_tic_due_at_b8e2ab_idx'), - ), - ] diff --git a/apps/tickets/migrations/0002_add_created_by_to_ticketcategory.py b/apps/tickets/migrations/0002_add_created_by_to_ticketcategory.py deleted file mode 100644 index 2073775..0000000 --- a/apps/tickets/migrations/0002_add_created_by_to_ticketcategory.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.27 on 2026-04-30 14:07 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tickets', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='ticketcategory', - name='created_by', - field=models.ForeignKey(blank=True, help_text='创建此分类的用户,用于提供商数据隔离', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_ticket_categories', to=settings.AUTH_USER_MODEL, verbose_name='创建者'), - ), - ] diff --git a/apps/tickets/migrations/0003_add_group_assignment.py b/apps/tickets/migrations/0003_add_group_assignment.py deleted file mode 100644 index a069f23..0000000 --- a/apps/tickets/migrations/0003_add_group_assignment.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-02 09:54 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('tickets', '0002_add_created_by_to_ticketcategory'), - ] - - operations = [ - migrations.AddField( - model_name='ticket', - name='assigned_group', - field=models.ForeignKey(blank=True, help_text='负责处理此工单的用户组', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tickets', to='auth.group', verbose_name='处理组'), - ), - migrations.AddField( - model_name='ticketcategory', - name='auto_assign_to_group', - field=models.ForeignKey(blank=True, help_text='该分类的工单自动分配给指定用户组', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='auto_assigned_categories', to='auth.group', verbose_name='自动分配给用户组'), - ), - migrations.AddIndex( - model_name='ticket', - index=models.Index(fields=['assigned_group'], name='tickets_tic_assigne_161ff4_idx'), - ), - ] diff --git a/apps/tickets/migrations/0004_ticketcategory_allow_banned_users.py b/apps/tickets/migrations/0004_ticketcategory_allow_banned_users.py deleted file mode 100644 index 7cc7cbf..0000000 --- a/apps/tickets/migrations/0004_ticketcategory_allow_banned_users.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-07 13:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("tickets", "0003_add_group_assignment"), - ] - - operations = [ - migrations.AddField( - model_name="ticketcategory", - name="allow_banned_users", - field=models.BooleanField( - default=False, - help_text="封禁用户是否可以在此分类下提交工单", - verbose_name="允许封禁用户提交", - ), - ), - ] diff --git a/apps/tickets/migrations/0005_replace_related_product_host_with_cloud_computer_request.py b/apps/tickets/migrations/0005_replace_related_product_host_with_cloud_computer_request.py deleted file mode 100644 index 13d8d7b..0000000 --- a/apps/tickets/migrations/0005_replace_related_product_host_with_cloud_computer_request.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 4.2.27 on 2026-06-09 14:46 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("operations", "0019_alter_product_site_group_and_more"), - ("tickets", "0004_ticketcategory_allow_banned_users"), - ] - - operations = [ - migrations.RemoveField( - model_name="ticket", - name="related_host", - ), - migrations.RemoveField( - model_name="ticket", - name="related_product", - ), - migrations.AddField( - model_name="ticket", - name="related_cloud_computer", - field=models.ForeignKey( - blank=True, - help_text="工单关联的我拥有的云电脑", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="tickets", - to="operations.cloudcomputeruser", - verbose_name="关联云电脑", - ), - ), - migrations.AddField( - model_name="ticket", - name="related_request", - field=models.ForeignKey( - blank=True, - help_text="工单关联的我提交的申请", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="tickets", - to="operations.accountopeningrequest", - verbose_name="关联申请", - ), - ), - ] diff --git a/apps/tickets/migrations/__init__.py b/apps/tickets/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tickets/models.py b/apps/tickets/models.py deleted file mode 100644 index ebaceb6..0000000 --- a/apps/tickets/models.py +++ /dev/null @@ -1,651 +0,0 @@ -""" -工单系统模型 -""" -import os -import secrets -import string -from django.db import models -from django.utils.translation import gettext_lazy as _ -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.utils import timezone -from django.dispatch import Signal - -User = get_user_model() - -# 定义工单信号 -ticket_created = Signal() -ticket_assigned = Signal() -ticket_status_changed = Signal() -ticket_closed = Signal() -ticket_comment_added = Signal() - - -def generate_ticket_no(): - """ - 生成工单编号 - 格式: T + 年月日 + 4位随机字符 - """ - date_str = timezone.now().strftime('%Y%m%d') - random_str = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4)) - return f"T{date_str}{random_str}" - - -class TicketCategory(models.Model): - """ - 工单分类模型 - """ - name = models.CharField( - max_length=100, - verbose_name=_('分类名称'), - help_text=_('工单分类的名称') - ) - description = models.TextField( - blank=True, - verbose_name=_('分类描述'), - help_text=_('分类的详细描述') - ) - icon = models.CharField( - max_length=50, - blank=True, - default='help_outline', - verbose_name=_('图标'), - help_text=_('Material Icons 图标名称') - ) - default_priority = models.CharField( - max_length=20, - choices=[ - ('urgent', _('紧急')), - ('high', _('高')), - ('medium', _('中')), - ('low', _('低')), - ], - default='medium', - verbose_name=_('默认优先级'), - help_text=_('该分类下工单的默认优先级') - ) - auto_assign_to = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='auto_assigned_categories', - verbose_name=_('自动分配给'), - help_text=_('该分类的工单自动分配给指定用户') - ) - auto_assign_to_group = models.ForeignKey( - Group, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='auto_assigned_categories', - verbose_name=_('自动分配给用户组'), - help_text=_('该分类的工单自动分配给指定用户组') - ) - sla_hours = models.PositiveIntegerField( - default=24, - verbose_name=_('SLA时限(小时)'), - help_text=_('工单处理的服务级别协议时限') - ) - is_active = models.BooleanField( - default=True, - verbose_name=_('是否启用'), - help_text=_('是否在前端展示此分类') - ) - allow_banned_users = models.BooleanField( - default=False, - verbose_name=_('允许封禁用户提交'), - help_text=_('封禁用户是否可以在此分类下提交工单') - ) - display_order = models.IntegerField( - default=0, - verbose_name=_('显示顺序'), - help_text=_('分类在前端展示的顺序,数字越小越靠前') - ) - created_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='created_ticket_categories', - verbose_name=_('创建者'), - help_text=_('创建此分类的用户,用于提供商数据隔离') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('工单分类') - verbose_name_plural = _('工单分类') - ordering = ['display_order', 'name'] - indexes = [ - models.Index(fields=['is_active']), - models.Index(fields=['display_order']), - ] - - def __str__(self): - return self.name - - -class Ticket(models.Model): - """ - 工单模型 - """ - STATUS_CHOICES = [ - ('pending', _('待处理')), - ('processing', _('处理中')), - ('waiting_feedback', _('待反馈')), - ('resolved', _('已解决')), - ('closed', _('已关闭')), - ('rejected', _('已驳回')), - ] - - PRIORITY_CHOICES = [ - ('urgent', _('紧急')), - ('high', _('高')), - ('medium', _('中')), - ('low', _('低')), - ] - - SOURCE_CHOICES = [ - ('web', _('Web提交')), - ('api', _('API创建')), - ('system', _('系统自动生成')), - ('email', _('邮件导入')), - ] - - ticket_no = models.CharField( - max_length=20, - unique=True, - verbose_name=_('工单编号'), - help_text=_('自动生成的唯一工单编号') - ) - title = models.CharField( - max_length=200, - verbose_name=_('标题'), - help_text=_('工单的简要标题') - ) - description = models.TextField( - verbose_name=_('详细描述'), - help_text=_('工单的详细描述') - ) - category = models.ForeignKey( - TicketCategory, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='tickets', - verbose_name=_('分类'), - help_text=_('工单所属的分类') - ) - status = models.CharField( - max_length=20, - choices=STATUS_CHOICES, - default='pending', - verbose_name=_('状态'), - help_text=_('工单的当前状态') - ) - priority = models.CharField( - max_length=20, - choices=PRIORITY_CHOICES, - default='medium', - verbose_name=_('优先级'), - help_text=_('工单的优先级') - ) - source = models.CharField( - max_length=20, - choices=SOURCE_CHOICES, - default='web', - verbose_name=_('来源'), - help_text=_('工单的创建来源') - ) - creator = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='created_tickets', - verbose_name=_('创建者'), - help_text=_('提交工单的用户') - ) - assignee = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='assigned_tickets', - verbose_name=_('处理人'), - help_text=_('负责处理此工单的用户') - ) - assigned_group = models.ForeignKey( - Group, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='assigned_tickets', - verbose_name=_('处理组'), - help_text=_('负责处理此工单的用户组') - ) - related_cloud_computer = models.ForeignKey( - 'operations.CloudComputerUser', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='tickets', - verbose_name=_('关联云电脑'), - help_text=_('工单关联的我拥有的云电脑') - ) - related_request = models.ForeignKey( - 'operations.AccountOpeningRequest', - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='tickets', - verbose_name=_('关联申请'), - help_text=_('工单关联的我提交的申请') - ) - due_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('截止时间'), - help_text=_('根据SLA计算的工单处理截止时间') - ) - resolved_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('解决时间'), - help_text=_('工单被标记为已解决的时间') - ) - closed_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_('关闭时间'), - help_text=_('工单被关闭的时间') - ) - satisfaction = models.PositiveSmallIntegerField( - null=True, - blank=True, - verbose_name=_('满意度评分'), - help_text=_('用户对工单处理的满意度评分(1-5)') - ) - satisfaction_comment = models.TextField( - blank=True, - verbose_name=_('满意度评价'), - help_text=_('用户对工单处理的评价内容') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - updated_at = models.DateTimeField( - auto_now=True, - verbose_name=_('更新时间') - ) - - class Meta: - verbose_name = _('工单') - verbose_name_plural = _('工单') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['ticket_no']), - models.Index(fields=['status']), - models.Index(fields=['priority']), - models.Index(fields=['assignee']), - models.Index(fields=['assigned_group']), - models.Index(fields=['creator']), - models.Index(fields=['category']), - models.Index(fields=['created_at']), - models.Index(fields=['due_at']), - ] - - def __str__(self): - return f"[{self.ticket_no}] {self.title}" - - def save(self, *args, **kwargs): - """ - 重写save方法,自动生成工单编号,计算SLA截止时间 - """ - is_new = not self.pk - - if is_new and not self.ticket_no: - # 确保工单编号唯一 - while True: - ticket_no = generate_ticket_no() - if not Ticket.objects.filter(ticket_no=ticket_no).exists(): - self.ticket_no = ticket_no - break - - # 计算SLA截止时间 - if is_new and self.category and self.category.sla_hours and not self.due_at: - self.due_at = timezone.now() + timezone.timedelta(hours=self.category.sla_hours) - - # 记录状态变更前的旧状态 - old_status = None - old_assignee_id = None - old_assigned_group_id = None - if self.pk: - try: - old_ticket = Ticket.objects.get(pk=self.pk) - old_status = old_ticket.status - old_assignee_id = old_ticket.assignee_id - old_assigned_group_id = old_ticket.assigned_group_id - except Ticket.DoesNotExist: - pass - - super().save(*args, **kwargs) - - # 发送信号 - if is_new: - ticket_created.send(sender=self.__class__, instance=self) - - if old_status and old_status != self.status: - ticket_status_changed.send( - sender=self.__class__, - instance=self, - old_status=old_status, - new_status=self.status - ) - - # 记录活动 - TicketActivity.objects.create( - ticket=self, - actor=self.creator if hasattr(self, '_current_user') else None, - action='status_change', - old_value=old_status, - new_value=self.status, - description=f'状态从 "{self.get_status_display_old(old_status)}" 变更为 "{self.get_status_display()}"' - ) - - if old_assignee_id != self.assignee_id or old_assigned_group_id != self.assigned_group_id: - assign_desc_parts = [] - if self.assignee: - assign_desc_parts.append(f'用户 {self.assignee.username}') - if self.assigned_group: - assign_desc_parts.append(f'用户组 {self.assigned_group.name}') - if assign_desc_parts: - ticket_assigned.send(sender=self.__class__, instance=self) - TicketActivity.objects.create( - ticket=self, - actor=self.creator if hasattr(self, '_current_user') else None, - action='assign', - new_value=' / '.join(assign_desc_parts), - description=f'工单分配给 {" / ".join(assign_desc_parts)}' - ) - - def get_status_display_old(self, status): - """获取指定状态的显示文本""" - for code, name in self.STATUS_CHOICES: - if code == status: - return name - return status - - def assign_to(self, user=None, group=None, actor=None): - """ - 分配工单给指定用户和/或用户组 - """ - if user is not None: - self.assignee = user - if group is not None: - self.assigned_group = group - if actor: - self._current_user = actor - self.save() - - def update_status(self, new_status, actor=None, notes=''): - """ - 更新工单状态 - """ - if new_status not in [s[0] for s in self.STATUS_CHOICES]: - raise ValueError(f'无效的状态: {new_status}') - - self.status = new_status - - if new_status == 'resolved': - self.resolved_at = timezone.now() - elif new_status == 'closed': - self.closed_at = timezone.now() - - if actor: - self._current_user = actor - self.save() - - def close(self, actor=None, satisfaction=None, comment=''): - """ - 关闭工单 - """ - self.status = 'closed' - self.closed_at = timezone.now() - if satisfaction is not None: - self.satisfaction = satisfaction - if comment: - self.satisfaction_comment = comment - if actor: - self._current_user = actor - self.save() - ticket_closed.send(sender=self.__class__, instance=self) - - def is_overdue(self): - """ - 检查工单是否已超时 - """ - if self.due_at and self.status not in ['resolved', 'closed', 'rejected']: - return timezone.now() > self.due_at - return False - - @property - def assignee_display(self): - """ - 获取处理人显示文本(包含用户和用户组) - """ - parts = [] - if self.assignee: - parts.append(self.assignee.username) - if self.assigned_group: - parts.append(f'{self.assigned_group.name}(组)') - return ' / '.join(parts) if parts else None - - @property - def status_badge_class(self): - """ - 获取状态对应的Bootstrap徽章样式类 - """ - badge_map = { - 'pending': 'bg-secondary', - 'processing': 'bg-primary', - 'waiting_feedback': 'bg-warning', - 'resolved': 'bg-success', - 'closed': 'bg-dark', - 'rejected': 'bg-danger', - } - return badge_map.get(self.status, 'bg-secondary') - - @property - def priority_badge_class(self): - """ - 获取优先级对应的Bootstrap徽章样式类 - """ - badge_map = { - 'urgent': 'bg-danger', - 'high': 'bg-warning', - 'medium': 'bg-info', - 'low': 'bg-success', - } - return badge_map.get(self.priority, 'bg-info') - - -class TicketComment(models.Model): - """ - 工单评论/回复模型 - """ - ticket = models.ForeignKey( - Ticket, - on_delete=models.CASCADE, - related_name='comments', - verbose_name=_('关联工单'), - help_text=_('评论所属的工单') - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='ticket_comments', - verbose_name=_('作者'), - help_text=_('评论的作者') - ) - content = models.TextField( - verbose_name=_('内容'), - help_text=_('评论的详细内容') - ) - is_internal = models.BooleanField( - default=False, - verbose_name=_('内部备注'), - help_text=_('是否为仅工作人员可见的内部备注') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - - class Meta: - verbose_name = _('工单评论') - verbose_name_plural = _('工单评论') - ordering = ['created_at'] - indexes = [ - models.Index(fields=['ticket', 'created_at']), - ] - - def __str__(self): - return f'{self.author.username} - {self.ticket.ticket_no}' - - def save(self, *args, **kwargs): - is_new = not self.pk - super().save(*args, **kwargs) - if is_new: - ticket_comment_added.send(sender=self.__class__, instance=self) - # 记录活动 - TicketActivity.objects.create( - ticket=self.ticket, - actor=self.author, - action='comment', - description=f'{"添加内部备注" if self.is_internal else "添加评论"}' - ) - - -class TicketActivity(models.Model): - """ - 工单活动记录模型 - """ - ACTION_CHOICES = [ - ('create', _('创建')), - ('assign', _('分配')), - ('status_change', _('状态变更')), - ('comment', _('评论')), - ('close', _('关闭')), - ('update', _('更新')), - ] - - ticket = models.ForeignKey( - Ticket, - on_delete=models.CASCADE, - related_name='activities', - verbose_name=_('关联工单'), - help_text=_('活动所属的工单') - ) - actor = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='ticket_activities', - verbose_name=_('操作人'), - help_text=_('执行此操作的用户') - ) - action = models.CharField( - max_length=20, - choices=ACTION_CHOICES, - verbose_name=_('操作类型'), - help_text=_('活动的操作类型') - ) - old_value = models.CharField( - max_length=255, - blank=True, - verbose_name=_('旧值'), - help_text=_('变更前的值') - ) - new_value = models.CharField( - max_length=255, - blank=True, - verbose_name=_('新值'), - help_text=_('变更后的值') - ) - description = models.TextField( - blank=True, - verbose_name=_('描述'), - help_text=_('活动的详细描述') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - - class Meta: - verbose_name = _('工单活动') - verbose_name_plural = _('工单活动') - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['ticket', 'created_at']), - ] - - def __str__(self): - actor_name = self.actor.username if self.actor else _('系统') - return f'[{actor_name}] {self.get_action_display()} - {self.ticket.ticket_no}' - - -class TicketAttachment(models.Model): - """ - 工单附件模型 - """ - ticket = models.ForeignKey( - Ticket, - on_delete=models.CASCADE, - related_name='attachments', - verbose_name=_('关联工单'), - help_text=_('附件所属的工单') - ) - file = models.FileField( - upload_to='ticket_attachments/%Y/%m/', - verbose_name=_('文件'), - help_text=_('上传的附件文件') - ) - filename = models.CharField( - max_length=255, - verbose_name=_('原始文件名'), - help_text=_('文件的原始名称') - ) - uploaded_by = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='ticket_attachments', - verbose_name=_('上传人'), - help_text=_('上传此附件的用户') - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name=_('创建时间') - ) - - class Meta: - verbose_name = _('工单附件') - verbose_name_plural = _('工单附件') - ordering = ['-created_at'] - - def __str__(self): - return self.filename - - def save(self, *args, **kwargs): - if self.file and not self.filename: - self.filename = os.path.basename(self.file.name) - super().save(*args, **kwargs) diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py deleted file mode 100644 index b59ba44..0000000 --- a/apps/tickets/notifications.py +++ /dev/null @@ -1,231 +0,0 @@ -""" -工单系统通知模块 - -支持邮件通知和站内通知(邮件通过 Celery 异步发送) -""" - - -def _get_system_config(): - """获取系统配置""" - from apps.dashboard.models import SystemConfig - try: - return SystemConfig.get_config() - except Exception: - return None - - -def _get_site_url(): - """获取站点URL""" - from django.conf import settings - return getattr(settings, 'SITE_URL', 'http://localhost:8000') - - -def send_ticket_email(subject, template_name, context, recipient_list): - """ - 异步发送工单相关邮件(通过 Celery 任务) - - Args: - subject: 邮件主题 - template_name: 邮件模板名称 - context: 模板上下文 - recipient_list: 收件人列表 - """ - if not recipient_list: - return - - from apps.accounts.tasks import send_ticket_email_task - send_ticket_email_task.delay( - subject=subject, - template_name=template_name, - context=context, - recipient_list=recipient_list, - ) - - -def notify_ticket_created(ticket): - """ - 通知工单创建 - - 通知管理员 - - 通知自动分配的处理人 - - 通知自动分配的处理组成员 - """ - context = { - 'ticket': ticket, - 'site_url': _get_site_url(), - } - - recipients = [] - - if ticket.assignee and ticket.assignee.email: - recipients.append(ticket.assignee.email) - - if ticket.assigned_group: - group_users = ticket.assigned_group.user_set.filter( - is_active=True - ).exclude(email='') - for user in group_users: - if user.email not in recipients: - recipients.append(user.email) - - if recipients: - send_ticket_email( - subject=f'[2c2a] 新工单分配 - {ticket.ticket_no}', - template_name='tickets/email/assigned.html', - context=context, - recipient_list=recipients - ) - - -def notify_ticket_assigned(ticket, old_assignee=None): - """ - 通知工单分配 - - 通知新的处理人 - - 通知处理组成员 - """ - context = { - 'ticket': ticket, - 'site_url': _get_site_url(), - 'old_assignee': old_assignee, - } - - recipients = [] - - if ticket.assignee and ticket.assignee.email: - recipients.append(ticket.assignee.email) - - if ticket.assigned_group: - group_users = ticket.assigned_group.user_set.filter( - is_active=True - ).exclude(email='') - for user in group_users: - if user.email not in recipients: - recipients.append(user.email) - - if not recipients: - return - - send_ticket_email( - subject=f'[2c2a] 工单分配通知 - {ticket.ticket_no}', - template_name='tickets/email/assigned.html', - context=context, - recipient_list=recipients - ) - - -def notify_ticket_status_changed(ticket, old_status, new_status): - """ - 通知工单状态变更 - - 通知创建者 - """ - if not ticket.creator or not ticket.creator.email: - return - - # 如果创建者自己变更状态,不发送通知 - # 这里简化处理,实际应该传入操作人参数 - - context = { - 'ticket': ticket, - 'site_url': _get_site_url(), - 'old_status': old_status, - 'new_status': new_status, - } - - send_ticket_email( - subject=f'[2c2a] 工单状态更新 - {ticket.ticket_no}', - template_name='tickets/email/status_update.html', - context=context, - recipient_list=[ticket.creator.email] - ) - - -def notify_ticket_closed(ticket): - """ - 通知工单关闭 - - 通知创建者 - - 邀请评价 - """ - if not ticket.creator or not ticket.creator.email: - return - - context = { - 'ticket': ticket, - 'site_url': _get_site_url(), - } - - send_ticket_email( - subject=f'[2c2a] 工单已关闭 - {ticket.ticket_no}', - template_name='tickets/email/closed.html', - context=context, - recipient_list=[ticket.creator.email] - ) - - -def notify_new_comment(comment): - """ - 通知新评论 - - 通知工单相关人员 - """ - ticket = comment.ticket - - # 收集需要通知的用户 - recipients = [] - - # 通知创建者(如果不是评论者自己) - if ticket.creator and ticket.creator.email and ticket.creator != comment.author: - recipients.append(ticket.creator.email) - - # 通知处理人(如果不是评论者自己) - if ticket.assignee and ticket.assignee.email and ticket.assignee != comment.author: - recipients.append(ticket.assignee.email) - - if not recipients: - return - - context = { - 'ticket': ticket, - 'comment': comment, - 'site_url': _get_site_url(), - } - - send_ticket_email( - subject=f'[2c2a] 工单新评论 - {ticket.ticket_no}', - template_name='tickets/email/new_comment.html', - context=context, - recipient_list=recipients - ) - - -def notify_overdue_ticket(ticket): - """ - 通知工单即将超时或已超时 - - 通知处理人 - - 通知处理组成员 - - 通知管理员 - """ - context = { - 'ticket': ticket, - 'site_url': _get_site_url(), - } - - recipients = [] - - if ticket.assignee and ticket.assignee.email: - recipients.append(ticket.assignee.email) - - if ticket.assigned_group: - group_users = ticket.assigned_group.user_set.filter( - is_active=True - ).exclude(email='') - for user in group_users: - if user.email not in recipients: - recipients.append(user.email) - - if not recipients: - return - - send_ticket_email( - subject=f'[2c2a] 工单即将超时 - {ticket.ticket_no}', - template_name='tickets/email/overdue.html', - context=context, - recipient_list=recipients - ) diff --git a/apps/tickets/signals.py b/apps/tickets/signals.py deleted file mode 100644 index efccc56..0000000 --- a/apps/tickets/signals.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -工单系统信号处理 - -处理工单相关的信号,包括: -- 工单创建通知 -- 工单分配通知 -- 工单状态变更通知 -- 工单关闭通知 -- 评论添加通知 -- 审计日志记录 -""" - -from django.dispatch import receiver -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ - -from .models import ( - ticket_created, ticket_assigned, ticket_status_changed, - ticket_closed, ticket_comment_added, TicketActivity -) - -User = get_user_model() - - -@receiver(ticket_created) -def on_ticket_created(sender, instance, **kwargs): - """ - 工单创建时的信号处理 - - 记录创建活动 - - 发送通知给管理员 - """ - # 创建活动记录已在模型 save 方法中处理 - # 这里可以添加额外的通知逻辑 - pass - - -@receiver(ticket_assigned) -def on_ticket_assigned(sender, instance, **kwargs): - """ - 工单分配时的信号处理 - - 发送通知给处理人 - """ - if instance.assignee: - # 可以在这里发送邮件通知或站内通知 - pass - - -@receiver(ticket_status_changed) -def on_ticket_status_changed(sender, instance, old_status, new_status, **kwargs): - """ - 工单状态变更时的信号处理 - - 发送通知给创建者 - - 检查SLA - """ - # 状态变更活动记录已在模型 save 方法中处理 - # 可以在这里添加通知逻辑 - pass - - -@receiver(ticket_closed) -def on_ticket_closed(sender, instance, **kwargs): - """ - 工单关闭时的信号处理 - - 发送满意度评价邀请 - """ - # 可以在这里发送评价邀请邮件 - pass - - -@receiver(ticket_comment_added) -def on_ticket_comment_added(sender, instance, **kwargs): - """ - 评论添加时的信号处理 - - 通知相关人员 - """ - # 评论活动记录已在模型 save 方法中处理 - # 可以在这里添加通知逻辑 - pass diff --git a/apps/tickets/urls.py b/apps/tickets/urls.py deleted file mode 100644 index 26d8426..0000000 --- a/apps/tickets/urls.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -工单系统URL配置 -""" -from django.urls import path -from . import views - -app_name = 'tickets' - -urlpatterns = [ - # 工单列表 - path('', views.TicketListView.as_view(), name='ticket_list'), - path('my/', views.MyTicketsView.as_view(), name='my_tickets'), - path('pending/', views.PendingTicketsView.as_view(), name='pending_tickets'), - - # 工单CRUD - path('create/', views.TicketCreateView.as_view(), name='ticket_create'), - path('/', views.TicketDetailView.as_view(), name='ticket_detail'), - - # 工单操作 - path('/assign/', views.ticket_assign, name='ticket_assign'), - path('/status/', views.ticket_status_update, name='ticket_status_update'), - path('/close/', views.ticket_close, name='ticket_close'), - path('/comment/', views.ticket_comment, name='ticket_comment'), - - # 仪表盘 - path('dashboard/', views.TicketDashboardView.as_view(), name='dashboard'), -] diff --git a/apps/tickets/urls_admin.py b/apps/tickets/urls_admin.py deleted file mode 100644 index fc5c09c..0000000 --- a/apps/tickets/urls_admin.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -超管后台 - 工单系统 URL 配置 - -命名空间: admin_tickets -超管可查看所有工单数据,无数据隔离。 -""" - -from django.urls import path - -from . import views_admin - -app_name = 'admin_tickets' - -urlpatterns = [ - # 工单管理 - path('', views_admin.admin_ticket_list, name='ticket_list'), - path('/', views_admin.admin_ticket_detail, name='ticket_detail'), - path('/comment/', views_admin.admin_ticket_comment_create, name='ticket_comment_create'), - - # 工单批量操作 - path('batch/processing/', views_admin.admin_ticket_batch_processing, name='ticket_batch_processing'), - path('batch/resolved/', views_admin.admin_ticket_batch_resolved, name='ticket_batch_resolved'), - path('batch/closed/', views_admin.admin_ticket_batch_closed, name='ticket_batch_closed'), - - # 工单分类管理 - path('categories/', views_admin.admin_category_list, name='category_list'), - path('categories/create/', views_admin.admin_category_create, name='category_create'), - path('categories//edit/', views_admin.admin_category_update, name='category_edit'), - path('categories//delete/', views_admin.admin_category_delete, name='category_delete'), - - # 活动日志(只读) - path('activities/', views_admin.admin_activity_list, name='activity_list'), -] diff --git a/apps/tickets/urls_provider.py b/apps/tickets/urls_provider.py deleted file mode 100644 index cfac877..0000000 --- a/apps/tickets/urls_provider.py +++ /dev/null @@ -1,87 +0,0 @@ -from django.urls import path - -from .views_provider import ( - TicketActivityListView, - TicketAttachmentDownloadView, - TicketAttachmentUploadView, - TicketBatchClosedView, - TicketBatchProcessingView, - TicketBatchResolvedView, - TicketCategoryCreateView, - TicketCategoryDeleteView, - TicketCategoryListView, - TicketCategoryUpdateView, - TicketCommentCreateView, - TicketDetailView, - TicketListView, -) - -app_name = 'provider_tickets' - -urlpatterns = [ - path( - 'categories/', - TicketCategoryListView.as_view(), - name='category_list', - ), - path( - 'categories/create/', - TicketCategoryCreateView.as_view(), - name='category_create', - ), - path( - 'categories//edit/', - TicketCategoryUpdateView.as_view(), - name='category_edit', - ), - path( - 'categories//delete/', - TicketCategoryDeleteView.as_view(), - name='category_delete', - ), - path( - '', - TicketListView.as_view(), - name='ticket_list', - ), - path( - '/', - TicketDetailView.as_view(), - name='ticket_detail', - ), - path( - 'batch-processing/', - TicketBatchProcessingView.as_view(), - name='ticket_batch_processing', - ), - path( - 'batch-resolved/', - TicketBatchResolvedView.as_view(), - name='ticket_batch_resolved', - ), - path( - 'batch-closed/', - TicketBatchClosedView.as_view(), - name='ticket_batch_closed', - ), - path( - '/comment/', - TicketCommentCreateView.as_view(), - name='ticket_comment_create', - ), - path( - 'activities/', - TicketActivityListView.as_view(), - name='activity_list', - ), - path( - '/attachments/upload/', - TicketAttachmentUploadView.as_view(), - name='attachment_upload', - ), - path( - 'attachments//download/', - TicketAttachmentDownloadView.as_view(), - name='attachment_download', - ), -] diff --git a/apps/tickets/views.py b/apps/tickets/views.py deleted file mode 100644 index 2cc2a86..0000000 --- a/apps/tickets/views.py +++ /dev/null @@ -1,478 +0,0 @@ -""" -工单系统视图 -""" -from django.shortcuts import render, get_object_or_404, redirect -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.urls import reverse_lazy -from django.views.generic import ListView, CreateView, DetailView, UpdateView -from django.utils.decorators import method_decorator -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse, HttpResponseForbidden, HttpResponseRedirect -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.http import require_POST -from django.utils.translation import gettext_lazy as _ -from django.db.models import Q -from django.utils import timezone -from datetime import timedelta - -from .models import Ticket, TicketComment, TicketActivity, TicketCategory -from .forms import ( - TicketForm, TicketCommentForm, TicketAssignForm, - TicketStatusForm, TicketCloseForm, TicketFilterForm -) - - -class TicketListView(LoginRequiredMixin, ListView): - """ - 工单列表视图 - """ - model = Ticket - template_name = 'tickets/ticket_list.html' - context_object_name = 'tickets' - paginate_by = 20 - - def get_queryset(self): - """获取查询集""" - queryset = Ticket.objects.all() - user = self.request.user - - # 权限过滤 - if not (user.is_staff or user.is_superuser): - # 普通用户只能看到自己创建的工单 - queryset = queryset.filter(creator=user) - - # 应用过滤条件 - form = TicketFilterForm(self.request.GET) - if form.is_valid(): - status = form.cleaned_data.get('status') - priority = form.cleaned_data.get('priority') - category = form.cleaned_data.get('category') - search = form.cleaned_data.get('search') - start_date = form.cleaned_data.get('start_date') - end_date = form.cleaned_data.get('end_date') - - if status: - queryset = queryset.filter(status=status) - if priority: - queryset = queryset.filter(priority=priority) - if category: - queryset = queryset.filter(category=category) - if search: - queryset = queryset.filter( - Q(ticket_no__icontains=search[:50]) | - Q(title__icontains=search[:50]) | - Q(description__icontains=search[:50]) - ) - if start_date: - queryset = queryset.filter(created_at__gte=start_date) - if end_date: - end_date = end_date + timedelta(days=1) - queryset = queryset.filter(created_at__lt=end_date) - - return queryset.select_related('creator', 'assignee', 'category').order_by('-created_at') - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context['filter_form'] = TicketFilterForm(self.request.GET) - context['statuses'] = Ticket.STATUS_CHOICES - context['priorities'] = Ticket.PRIORITY_CHOICES - context['categories'] = TicketCategory.objects.filter(is_active=True) - return context - - -class MyTicketsView(LoginRequiredMixin, ListView): - """ - 我的工单视图 - """ - model = Ticket - template_name = 'tickets/my_tickets.html' - context_object_name = 'tickets' - paginate_by = 20 - - def get_queryset(self): - """获取当前用户的工单""" - queryset = Ticket.objects.filter(creator=self.request.user) - - # 应用过滤条件 - form = TicketFilterForm(self.request.GET) - if form.is_valid(): - status = form.cleaned_data.get('status') - if status: - queryset = queryset.filter(status=status) - - return queryset.select_related('assignee', 'category').order_by('-created_at') - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context['filter_form'] = TicketFilterForm(self.request.GET) - context['statuses'] = Ticket.STATUS_CHOICES - return context - - -class PendingTicketsView(LoginRequiredMixin, ListView): - """ - 待处理工单视图(管理员/处理人) - """ - model = Ticket - template_name = 'tickets/pending_list.html' - context_object_name = 'tickets' - paginate_by = 20 - - def get_queryset(self): - """获取待处理工单""" - user = self.request.user - queryset = Ticket.objects.filter(status__in=['pending', 'processing', 'waiting_feedback']) - - if not (user.is_staff or user.is_superuser): - # 非管理员只能看到分配给自己或自己所在用户组的 - user_groups = user.groups.all() - queryset = queryset.filter( - Q(assignee=user) | Q(assigned_group__in=user_groups) - ) - - return queryset.select_related( - 'creator', 'assignee', 'assigned_group', 'category' - ).order_by('due_at', '-priority', '-created_at') - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - context['statuses'] = Ticket.STATUS_CHOICES - return context - - -class TicketCreateView(LoginRequiredMixin, CreateView): - """ - 创建工单视图 - """ - model = Ticket - form_class = TicketForm - template_name = 'tickets/ticket_form.html' - success_url = reverse_lazy('tickets:my_tickets') - - def get_form_kwargs(self): - """获取表单初始化参数""" - kwargs = super().get_form_kwargs() - kwargs['user'] = self.request.user - return kwargs - - def form_valid(self, form): - """表单验证成功后的处理""" - # 封禁用户只能提交允许的分类 - from apps.accounts.models import UserBan - if UserBan.objects.filter(user=self.request.user).exists(): - category = form.instance.category - if not category or not category.allow_banned_users: - messages.error(self.request, '您只能在被允许的分类下提交工单') - return self.form_invalid(form) - - form.instance.creator = self.request.user - form.instance.status = 'pending' - form.instance.source = 'web' - - # 如果有分类,自动分配 - if form.instance.category: - if form.instance.category.auto_assign_to: - form.instance.assignee = form.instance.category.auto_assign_to - if form.instance.category.auto_assign_to_group: - form.instance.assigned_group = form.instance.category.auto_assign_to_group - - response = super().form_valid(form) - - # 记录创建活动 - TicketActivity.objects.create( - ticket=self.object, - actor=self.request.user, - action='create', - description='创建工单' - ) - - messages.success(self.request, f'工单 {self.object.ticket_no} 已成功创建!') - return response - - def form_invalid(self, form): - """表单验证失败后的处理""" - messages.error(self.request, '工单信息填写有误,请检查输入信息。') - return super().form_invalid(form) - - -class TicketDetailView(LoginRequiredMixin, DetailView): - """ - 工单详情视图 - """ - model = Ticket - template_name = 'tickets/ticket_detail.html' - context_object_name = 'ticket' - - def get_queryset(self): - """获取查询集""" - return Ticket.objects.select_related( - 'creator', 'assignee', 'assigned_group', 'category', - 'related_cloud_computer', 'related_cloud_computer__product', - 'related_request', 'related_request__target_product' - ).prefetch_related('comments', 'activities') - - def get_context_data(self, **kwargs): - """获取模板上下文数据""" - context = super().get_context_data(**kwargs) - ticket = self.object - user = self.request.user - - # 权限检查 - can_view = ( - ticket.creator == user or - ticket.assignee == user or - user.is_staff or - user.is_superuser or - (ticket.assigned_group and user.groups.filter(pk=ticket.assigned_group.pk).exists()) - ) - - if not can_view: - context['forbidden'] = True - return context - - # 评论表单 - context['comment_form'] = TicketCommentForm() - - # 分配表单(仅管理员/工作人员) - if user.is_staff or user.is_superuser: - context['assign_form'] = TicketAssignForm() - context['status_form'] = TicketStatusForm() - - # 关闭表单(创建者或管理员) - if ticket.creator == user or user.is_staff or user.is_superuser: - context['close_form'] = TicketCloseForm() - - # 评论列表(过滤内部备注) - comments = ticket.comments.all() - if not (user.is_staff or user.is_superuser): - comments = comments.filter(is_internal=False) - context['comments'] = comments - - # 活动记录 - context['activities'] = ticket.activities.all()[:20] - - # 状态流转选项 - context['status_choices'] = Ticket.STATUS_CHOICES - - return context - - -@login_required -def ticket_assign(request, pk): - """ - 分配工单 - """ - ticket = get_object_or_404(Ticket, pk=pk) - - # 权限检查 - if not (request.user.is_staff or request.user.is_superuser): - return HttpResponseForbidden('无权操作') - - if request.method == 'POST': - form = TicketAssignForm(request.POST) - if form.is_valid(): - assignee = form.cleaned_data.get('assignee') - assigned_group = form.cleaned_data.get('assigned_group') - notes = form.cleaned_data['notes'] - - old_assignee = ticket.assignee - old_assigned_group = ticket.assigned_group - ticket.assign_to( - user=assignee, - group=assigned_group, - actor=request.user, - ) - - # 构建分配描述 - assign_desc_parts = [] - if assignee: - assign_desc_parts.append(f'{assignee.username}') - if assigned_group: - assign_desc_parts.append(f'{assigned_group.name}(组)') - - # 记录活动 - TicketActivity.objects.create( - ticket=ticket, - actor=request.user, - action='assign', - old_value=str(old_assignee) if old_assignee else '', - new_value=' / '.join(assign_desc_parts), - description=f'工单分配给 {" / ".join(assign_desc_parts)}' + (f',备注: {notes}' if notes else '') - ) - - messages.success(request, f'工单已分配给 {" / ".join(assign_desc_parts)}') - return redirect('tickets:ticket_detail', pk=ticket.pk) - - return redirect('tickets:ticket_detail', pk=ticket.pk) - - -@login_required -def ticket_status_update(request, pk): - """ - 更新工单状态 - """ - ticket = get_object_or_404(Ticket, pk=pk) - - # 权限检查 - if not (request.user.is_staff or request.user.is_superuser or request.user == ticket.assignee or - (ticket.assigned_group and request.user.groups.filter(pk=ticket.assigned_group.pk).exists())): - return HttpResponseForbidden('无权操作') - - if request.method == 'POST': - form = TicketStatusForm(request.POST) - if form.is_valid(): - new_status = form.cleaned_data['status'] - notes = form.cleaned_data['notes'] - - old_status = ticket.status - ticket.update_status(new_status, actor=request.user) - - # 记录活动 - TicketActivity.objects.create( - ticket=ticket, - actor=request.user, - action='status_change', - old_value=old_status, - new_value=new_status, - description=f'状态变更为 {ticket.get_status_display()}' + (f',备注: {notes}' if notes else '') - ) - - messages.success(request, f'工单状态已更新为 {ticket.get_status_display()}') - return redirect('tickets:ticket_detail', pk=ticket.pk) - - return redirect('tickets:ticket_detail', pk=ticket.pk) - - -@login_required -def ticket_close(request, pk): - """ - 关闭工单 - """ - ticket = get_object_or_404(Ticket, pk=pk) - - # 权限检查 - if not (request.user == ticket.creator or request.user.is_staff or request.user.is_superuser): - return HttpResponseForbidden('无权操作') - - if request.method == 'POST': - form = TicketCloseForm(request.POST) - if form.is_valid(): - satisfaction = form.cleaned_data['satisfaction'] - comment = form.cleaned_data['satisfaction_comment'] - - satisfaction_int = int(satisfaction) if satisfaction else None - - ticket.close( - actor=request.user, - satisfaction=satisfaction_int, - comment=comment - ) - - # 记录活动 - TicketActivity.objects.create( - ticket=ticket, - actor=request.user, - action='close', - description='关闭工单' + (f',满意度: {satisfaction}' if satisfaction else '') - ) - - messages.success(request, '工单已关闭') - return redirect('tickets:ticket_detail', pk=ticket.pk) - - return redirect('tickets:ticket_detail', pk=ticket.pk) - - -@login_required -@require_POST -def ticket_comment(request, pk): - """ - 添加工单评论 - """ - ticket = get_object_or_404(Ticket, pk=pk) - - # 权限检查 - can_comment = ( - ticket.creator == request.user or - ticket.assignee == request.user or - request.user.is_staff or - request.user.is_superuser or - (ticket.assigned_group and request.user.groups.filter(pk=ticket.assigned_group.pk).exists()) - ) - - if not can_comment: - return HttpResponseForbidden('无权评论') - - form = TicketCommentForm(request.POST) - if form.is_valid(): - comment = form.save(commit=False) - comment.ticket = ticket - comment.author = request.user - - # 非工作人员不能添加内部备注 - if comment.is_internal and not (request.user.is_staff or request.user.is_superuser): - comment.is_internal = False - - comment.save() - - messages.success(request, '评论已添加') - else: - messages.error(request, '评论内容无效') - - return redirect('tickets:ticket_detail', pk=ticket.pk) - - -class TicketDashboardView(LoginRequiredMixin, ListView): - """ - 工单仪表盘视图 - """ - model = Ticket - template_name = 'tickets/dashboard.html' - context_object_name = 'tickets' - paginate_by = 10 - - def get_queryset(self): - """获取最近工单""" - user = self.request.user - queryset = Ticket.objects.all() - - if not (user.is_staff or user.is_superuser): - queryset = queryset.filter(creator=user) - - return queryset.select_related('creator', 'assignee', 'category').order_by('-created_at')[:10] - - def get_context_data(self, **kwargs): - """获取统计信息""" - context = super().get_context_data(**kwargs) - user = self.request.user - - if user.is_staff or user.is_superuser: - # 管理员统计 - context['total_tickets'] = Ticket.objects.count() - context['pending_count'] = Ticket.objects.filter(status='pending').count() - context['processing_count'] = Ticket.objects.filter(status='processing').count() - context['resolved_count'] = Ticket.objects.filter(status='resolved').count() - context['closed_count'] = Ticket.objects.filter(status='closed').count() - context['overdue_count'] = Ticket.objects.filter( - due_at__lt=timezone.now() - ).exclude(status__in=['resolved', 'closed', 'rejected']).count() - else: - # 用户统计 - context['total_tickets'] = Ticket.objects.filter(creator=user).count() - context['pending_count'] = Ticket.objects.filter(creator=user, status='pending').count() - context['processing_count'] = Ticket.objects.filter(creator=user, status='processing').count() - context['resolved_count'] = Ticket.objects.filter(creator=user, status='resolved').count() - context['closed_count'] = Ticket.objects.filter(creator=user, status='closed').count() - - # 优先级分布 - context['priority_distribution'] = { - 'urgent': Ticket.objects.filter(priority='urgent').count() if (user.is_staff or user.is_superuser) else Ticket.objects.filter(creator=user, priority='urgent').count(), - 'high': Ticket.objects.filter(priority='high').count() if (user.is_staff or user.is_superuser) else Ticket.objects.filter(creator=user, priority='high').count(), - 'medium': Ticket.objects.filter(priority='medium').count() if (user.is_staff or user.is_superuser) else Ticket.objects.filter(creator=user, priority='medium').count(), - 'low': Ticket.objects.filter(priority='low').count() if (user.is_staff or user.is_superuser) else Ticket.objects.filter(creator=user, priority='low').count(), - } - - return context diff --git a/apps/tickets/views_admin.py b/apps/tickets/views_admin.py deleted file mode 100644 index a32d31f..0000000 --- a/apps/tickets/views_admin.py +++ /dev/null @@ -1,578 +0,0 @@ -""" -工单系统 - 超管后台视图 - -超管可查看所有数据;提供商仅可查看自己创建的分类及关联的工单。 -包含: -- AdminTicketListView: 所有工单列表,搜索、筛选、批量操作 -- AdminTicketDetailView: 工单详情,含评论和附件 -- AdminTicketCommentCreateView: 添加评论 (POST) -- AdminCategoryListView: 所有分类列表 -- AdminCategoryCreateView: 创建分类 -- AdminCategoryUpdateView: 编辑分类 -- AdminCategoryDeleteView: 删除分类 -- AdminActivityListView: 所有活动记录(只读) -""" - -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.db.models import Q -from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone -from django.views.decorators.http import require_POST -from django.core.paginator import Paginator - -from apps.accounts.provider_decorators import admin_required -from utils.provider import get_provider_products - -from .forms_admin import AdminTicketCategoryForm, AdminTicketCommentForm -from .models import ( - Ticket, - TicketActivity, - TicketCategory, - TicketComment, -) - -User = get_user_model() - - -def _ticket_filter_for_user(user, site_group): - if user.is_superuser: - return Q() - if site_group and user.is_site_group_admin(site_group): - return ( - Q(related_cloud_computer__product__site_group=site_group) - | Q(creator=user) - ) - provider_products = get_provider_products(user) - return Q(related_cloud_computer__product__in=provider_products) | Q(creator=user) - - -def _category_filter_for_user(user, site_group): - if user.is_superuser: - return Q() - if site_group and user.is_site_group_admin(site_group): - return Q() - return Q(created_by=user) - - -# =========================================================================== -# 工单管理 -# =========================================================================== - - -@admin_required -def admin_ticket_list(request): - """ - 超管工单列表视图 - - - 无数据隔离,查看所有工单 - - 支持状态筛选、优先级筛选、搜索、批量操作 - """ - queryset = Ticket.objects.select_related( - "category", - "creator", - "assignee", - "assigned_group", - "related_cloud_computer", - "related_cloud_computer__product", - "related_request", - "related_request__target_product", - ) - - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - queryset = queryset.filter(ticket_filter).distinct() - - # 状态筛选 - status_filter = request.GET.get("status", "").strip() - if status_filter: - queryset = queryset.filter(status=status_filter) - - # 优先级筛选 - priority_filter = request.GET.get("priority", "").strip() - if priority_filter: - queryset = queryset.filter(priority=priority_filter) - - # 搜索 - search = request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(ticket_no__icontains=search) - | Q(title__icontains=search) - | Q(description__icontains=search) - | Q(creator__username__icontains=search) - ) - - # 排序 - queryset = queryset.order_by("-created_at") - - # 分页 - paginator = Paginator(queryset, 20) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - # 统计各状态数量 - if request.user.is_superuser: - base_qs = Ticket.objects.all() - else: - base_qs = Ticket.objects.filter(ticket_filter).distinct() - status_counts = { - "pending": base_qs.filter(status="pending").count(), - "processing": base_qs.filter(status="processing").count(), - "waiting_feedback": base_qs.filter(status="waiting_feedback").count(), - "resolved": base_qs.filter(status="resolved").count(), - "closed": base_qs.filter(status="closed").count(), - "rejected": base_qs.filter(status="rejected").count(), - } - - context = { - "page_obj": page_obj, - "tickets": page_obj, - "search": search, - "status_filter": status_filter, - "priority_filter": priority_filter, - "status_counts": status_counts, - "status_choices": Ticket.STATUS_CHOICES, - "priority_choices": Ticket.PRIORITY_CHOICES, - "page_title": "工单管理", - "active_nav": "admin_tickets", - } - - return render(request, "admin_base/tickets/ticket_list.html", context) - - -@admin_required -def admin_ticket_detail(request, pk): - """ - 超管工单详情视图 - - 显示工单信息、评论列表(含内部备注)、附件列表、活动记录。 - """ - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - ticket = get_object_or_404( - Ticket.objects.select_related( - "category", - "creator", - "assignee", - "assigned_group", - "related_cloud_computer", - "related_cloud_computer__product", - "related_request", - "related_request__target_product", - ).filter(ticket_filter), - pk=pk, - ) - else: - ticket = get_object_or_404( - Ticket.objects.select_related( - "category", - "creator", - "assignee", - "assigned_group", - "related_cloud_computer", - "related_cloud_computer__product", - "related_request", - "related_request__target_product", - ), - pk=pk, - ) - - # 评论列表(超管可见内部备注) - comments = ticket.comments.select_related("author").order_by("created_at") - - # 附件列表 - attachments = ticket.attachments.select_related("uploaded_by").order_by( - "-created_at" - ) - - # 活动记录 - activities = ticket.activities.select_related("actor").order_by("-created_at")[:10] - - context = { - "ticket": ticket, - "comments": comments, - "attachments": attachments, - "activities": activities, - "comment_form": AdminTicketCommentForm(), - "page_title": f"工单 {ticket.ticket_no}", - "active_nav": "admin_tickets", - } - - return render(request, "admin_base/tickets/ticket_detail.html", context) - - -@admin_required -@require_POST -def admin_ticket_comment_create(request, pk): - """ - 超管添加工单评论 (POST) - - 超管添加的评论自动标记作者为当前用户。 - """ - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - ticket = get_object_or_404( - Ticket.objects.filter(ticket_filter), - pk=pk, - ) - else: - ticket = get_object_or_404(Ticket, pk=pk) - - form = AdminTicketCommentForm(request.POST) - if form.is_valid(): - comment = form.save(commit=False) - comment.ticket = ticket - comment.author = request.user - comment.save() - - messages.success(request, "评论已添加。") - else: - for field, errors in form.errors.items(): - for error in errors: - messages.error(request, error) - - return redirect("admin:admin_tickets:ticket_detail", pk=ticket.pk) - - -# =========================================================================== -# 工单批量操作 -# =========================================================================== - - -def _get_selected_ids(request): - """从 POST 请求中获取选中的工单 ID 列表""" - selected = request.POST.getlist("selected_ids") - return [int(pk) for pk in selected if pk.isdigit()] - - -@admin_required -@require_POST -def admin_ticket_batch_processing(request): - """批量标记工单为处理中""" - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, "未选择任何工单。") - return redirect("admin:admin_tickets:ticket_list") - - qs = Ticket.objects.filter( - pk__in=selected_ids, - status="pending", - ) - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - qs = qs.filter(ticket_filter).distinct() - - updated_count = 0 - for ticket in qs: - ticket.status = "processing" - ticket.assignee = request.user - ticket._current_user = request.user - ticket.save(update_fields=["status", "assignee", "updated_at"]) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f"成功将 {updated_count} 个工单标记为处理中。", - ) - else: - messages.warning(request, "没有可标记为处理中的工单。") - - return redirect("admin:admin_tickets:ticket_list") - - -@admin_required -@require_POST -def admin_ticket_batch_resolved(request): - """批量标记工单为已解决""" - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, "未选择任何工单。") - return redirect("admin:admin_tickets:ticket_list") - - qs = Ticket.objects.filter( - pk__in=selected_ids, - status__in=["pending", "processing", "waiting_feedback"], - ) - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - qs = qs.filter(ticket_filter).distinct() - - updated_count = 0 - now = timezone.now() - for ticket in qs: - ticket.status = "resolved" - ticket.resolved_at = now - ticket._current_user = request.user - ticket.save(update_fields=["status", "resolved_at", "updated_at"]) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f"成功将 {updated_count} 个工单标记为已解决。", - ) - else: - messages.warning(request, "没有可标记为已解决的工单。") - - return redirect("admin:admin_tickets:ticket_list") - - -@admin_required -@require_POST -def admin_ticket_batch_closed(request): - """批量关闭工单""" - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, "未选择任何工单。") - return redirect("admin:admin_tickets:ticket_list") - - qs = Ticket.objects.filter( - pk__in=selected_ids, - ).exclude(status="closed") - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - qs = qs.filter(ticket_filter).distinct() - - updated_count = 0 - now = timezone.now() - for ticket in qs: - ticket.status = "closed" - ticket.closed_at = now - ticket._current_user = request.user - ticket.save(update_fields=["status", "closed_at", "updated_at"]) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f"成功关闭了 {updated_count} 个工单。", - ) - else: - messages.warning(request, "没有可关闭的工单。") - - return redirect("admin:admin_tickets:ticket_list") - - -# =========================================================================== -# 工单分类管理 -# =========================================================================== - - -@admin_required -def admin_category_list(request): - """ - 超管工单分类列表视图 - - - 无数据隔离,查看所有分类 - - 支持搜索、分页 - """ - site_group = getattr(request, "site_group", None) - category_filter = _category_filter_for_user(request.user, site_group) - queryset = TicketCategory.objects.filter(category_filter).order_by( - "display_order", "name" - ) - - # 搜索 - search = request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(name__icontains=search) | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(queryset, 15) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context = { - "page_obj": page_obj, - "categories": page_obj, - "search": search, - "page_title": "工单分类", - "active_nav": "ticket_categories", - } - - return render(request, "admin_base/tickets/category_list.html", context) - - -@admin_required -def admin_category_create(request): - """ - 超管创建工单分类 - - created_by 在视图中自动设置为当前用户。 - """ - if request.method == "POST": - form = AdminTicketCategoryForm(request.POST) - if form.is_valid(): - category = form.save(commit=False) - category.created_by = request.user - category.save() - - messages.success( - request, - f"工单分类 {category.name} 创建成功", - ) - return redirect("admin:admin_tickets:category_list") - else: - form = AdminTicketCategoryForm() - - context = { - "form": form, - "page_title": "创建工单分类", - "active_nav": "ticket_categories", - "is_create": True, - } - - return render(request, "admin_base/tickets/category_form.html", context) - - -@admin_required -def admin_category_update(request, pk): - """ - 超管编辑工单分类 - - 无数据隔离,可编辑所有分类。 - """ - site_group = getattr(request, "site_group", None) - category_filter = _category_filter_for_user(request.user, site_group) - if category_filter: - category = get_object_or_404( - TicketCategory.objects.filter(category_filter), - pk=pk, - ) - else: - category = get_object_or_404(TicketCategory, pk=pk) - - if request.method == "POST": - form = AdminTicketCategoryForm(request.POST, instance=category) - if form.is_valid(): - category = form.save() - messages.success( - request, - f"工单分类 {category.name} 更新成功", - ) - return redirect("admin:admin_tickets:category_list") - else: - form = AdminTicketCategoryForm(instance=category) - - context = { - "form": form, - "category": category, - "page_title": f"编辑分类 - {category.name}", - "active_nav": "ticket_categories", - "is_create": False, - } - - return render(request, "admin_base/tickets/category_form.html", context) - - -@admin_required -def admin_category_delete(request, pk): - """ - 超管删除工单分类 - - 无数据隔离,可删除所有分类。 - """ - site_group = getattr(request, "site_group", None) - category_filter = _category_filter_for_user(request.user, site_group) - if category_filter: - category = get_object_or_404( - TicketCategory.objects.filter(category_filter), - pk=pk, - ) - else: - category = get_object_or_404(TicketCategory, pk=pk) - - if request.method == "POST": - category_name = category.name - category.delete() - - messages.success( - request, - f"工单分类 {category_name} 已删除", - ) - return redirect("admin:admin_tickets:category_list") - - # 获取关联工单数 - ticket_count = Ticket.objects.filter(category=category).count() - - context = { - "category": category, - "ticket_count": ticket_count, - "page_title": f"删除分类 - {category.name}", - "active_nav": "ticket_categories", - } - - return render(request, "admin_base/tickets/category_confirm_delete.html", context) - - -# =========================================================================== -# 工单活动记录(只读) -# =========================================================================== - - -@admin_required -def admin_activity_list(request): - """ - 超管工单活动记录列表视图(只读) - - - 无数据隔离,查看所有活动记录 - - 支持按操作类型筛选、搜索 - """ - site_group = getattr(request, "site_group", None) - ticket_filter = _ticket_filter_for_user(request.user, site_group) - if ticket_filter: - queryset = ( - TicketActivity.objects.filter(ticket_filter) - .select_related( - "ticket", - "actor", - ) - .order_by("-created_at") - .distinct() - ) - else: - queryset = TicketActivity.objects.select_related( - "ticket", - "actor", - ).order_by("-created_at") - - # 操作类型筛选 - action_filter = request.GET.get("action", "").strip() - if action_filter: - queryset = queryset.filter(action=action_filter) - - # 搜索 - search = request.GET.get("search", "").strip() - if search: - queryset = queryset.filter( - Q(ticket__ticket_no__icontains=search) - | Q(actor__username__icontains=search) - | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(queryset, 20) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) - - context = { - "page_obj": page_obj, - "activities": page_obj, - "search": search, - "action_filter": action_filter, - "action_choices": TicketActivity.ACTION_CHOICES, - "page_title": "活动日志", - "active_nav": "ticket_activities", - } - - return render(request, "admin_base/tickets/activity_list.html", context) diff --git a/apps/tickets/views_provider.py b/apps/tickets/views_provider.py deleted file mode 100644 index b98a9ff..0000000 --- a/apps/tickets/views_provider.py +++ /dev/null @@ -1,688 +0,0 @@ -""" -工单系统 - 提供商后台视图 - -包含数据隔离功能: -- TicketCategory: 按 created_by 过滤(新增提供商隔离) -- Ticket: 按关联产品/主机过滤 -- TicketComment: 按关联工单过滤 -- TicketActivity: 按关联工单过滤(只读) -- TicketAttachment: 按关联工单过滤 -""" - -import os - -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.db.models import Q -from django.http import FileResponse, Http404, HttpResponseForbidden -from django.shortcuts import get_object_or_404, redirect -from django.utils import timezone -from django.views import View -from django.views.generic import DetailView, TemplateView -from django.core.paginator import Paginator - -from utils.provider import is_provider -from apps.provider.context_mixin import ProviderContextMixin - -from .forms_provider import ( - TicketAttachmentForm, - TicketCategoryForm, - TicketCommentForm, -) -from .models import ( - Ticket, - TicketActivity, - TicketAttachment, - TicketCategory, - TicketComment, -) - -User = get_user_model() - - -# =========================================================================== -# 通用 Mixin -# =========================================================================== - - -class ProviderTicketMixin(ProviderContextMixin): - """ - 提供商工单数据隔离 Mixin - - - dispatch: 验证提供商身份 - - get_provider_ticket_queryset: 获取当前提供商可见的工单查询集 - - get_provider_category_queryset: 获取当前提供商创建的分类查询集 - """ - - provider_url_namespace = 'provider:provider_tickets' - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - from django.contrib.auth.views import redirect_to_login - return redirect_to_login(request.get_full_path()) - if not is_provider(request.user): - return HttpResponseForbidden( - '您没有提供商权限,无法访问此页面。' - ) - return super().dispatch(request, *args, **kwargs) - - def get_provider_ticket_queryset(self): - """ - 获取当前提供商可见的工单查询集 - - 提供商可以看到: - - 关联云电脑所属产品由自己创建的工单 - """ - return Ticket.objects.filter( - Q(related_cloud_computer__product__created_by=self.request.user) - ).distinct().select_related( - 'category', 'creator', 'assignee', 'assigned_group', - 'related_cloud_computer', 'related_cloud_computer__product', - 'related_request', 'related_request__target_product', - ) - - def get_provider_category_queryset(self): - """ - 获取当前提供商创建的分类查询集 - - 新增提供商隔离:按 created_by 过滤 - """ - return TicketCategory.objects.filter( - created_by=self.request.user - ).order_by('display_order', 'name') - - def get_provider_activity_queryset(self): - """ - 获取当前提供商可见的活动记录查询集 - """ - return TicketActivity.objects.filter( - Q(ticket__related_cloud_computer__product__created_by=self.request.user) - ).distinct().select_related( - 'ticket', 'actor', - ).order_by('-created_at') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['active_nav'] = 'tickets' - context['page_title'] = '工单管理' - return context - - -# =========================================================================== -# 工单分类管理 -# =========================================================================== - - -class TicketCategoryListView(ProviderTicketMixin, TemplateView): - """ - 工单分类列表视图 - - - 提供商数据隔离:只看到自己创建的分类(created_by=request.user) - - 支持搜索、分页 - """ - - template_name = 'admin_base/tickets/category_list.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - queryset = self.get_provider_category_queryset() - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(name__icontains=search) - | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(queryset, 15) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'categories': page_obj, - 'search': search, - 'page_title': '工单分类', - 'active_nav': 'ticket_categories', - }) - return context - - -class TicketCategoryCreateView(ProviderTicketMixin, TemplateView): - """ - 工单分类创建视图 - - 自动设置 created_by 为当前用户。 - """ - - template_name = 'admin_base/tickets/category_form.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'form': kwargs.get( - 'form', - TicketCategoryForm(), - ), - 'page_title': '创建工单分类', - 'active_nav': 'ticket_categories', - 'is_create': True, - }) - return context - - def post(self, request, *args, **kwargs): - form = TicketCategoryForm(request.POST) - if form.is_valid(): - category = form.save(commit=False) - category.created_by = request.user - category.save() - - messages.success( - request, - f'工单分类 {category.name} 创建成功', - ) - return redirect('provider_tickets:category_list') - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class TicketCategoryUpdateView(ProviderTicketMixin, TemplateView): - """ - 工单分类编辑视图 - - 提供商数据隔离:只能编辑自己创建的分类。 - """ - - template_name = 'admin_base/tickets/category_form.html' - - def get_category(self): - """获取当前编辑的分类,确保数据隔离""" - return get_object_or_404( - self.get_provider_category_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - category = self.get_category() - form = kwargs.get( - 'form', - TicketCategoryForm(instance=category), - ) - context.update({ - 'form': form, - 'category': category, - 'page_title': f'编辑分类 - {category.name}', - 'active_nav': 'ticket_categories', - 'is_create': False, - }) - return context - - def post(self, request, *args, **kwargs): - category = self.get_category() - form = TicketCategoryForm(request.POST, instance=category) - if form.is_valid(): - category = form.save() - messages.success( - request, - f'工单分类 {category.name} 更新成功', - ) - return redirect('provider_tickets:category_list') - - return self.render_to_response( - self.get_context_data(form=form) - ) - - -class TicketCategoryDeleteView(ProviderTicketMixin, TemplateView): - """ - 工单分类删除视图 - - 提供商数据隔离:只能删除自己创建的分类。 - """ - - template_name = 'admin_base/tickets/category_confirm_delete.html' - - def get_category(self): - return get_object_or_404( - self.get_provider_category_queryset(), - pk=self.kwargs['pk'], - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - category = self.get_category() - - # 获取关联工单数 - ticket_count = Ticket.objects.filter( - category=category - ).count() - - context.update({ - 'category': category, - 'ticket_count': ticket_count, - 'page_title': f'删除分类 - {category.name}', - 'active_nav': 'ticket_categories', - }) - return context - - def post(self, request, *args, **kwargs): - category = self.get_category() - category_name = category.name - category.delete() - - messages.success( - request, - f'工单分类 {category_name} 已删除', - ) - return redirect('provider_tickets:category_list') - - -# =========================================================================== -# 工单管理 -# =========================================================================== - - -class TicketListView(ProviderTicketMixin, TemplateView): - """ - 工单列表视图 - - - 提供商数据隔离:只看到关联自己产品/主机的工单 - - 支持状态筛选、搜索、批量操作 - """ - - template_name = 'admin_base/tickets/ticket_list.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - queryset = self.get_provider_ticket_queryset() - - # 状态筛选 - status_filter = self.request.GET.get('status', '').strip() - if status_filter: - queryset = queryset.filter(status=status_filter) - - # 优先级筛选 - priority_filter = self.request.GET.get('priority', '').strip() - if priority_filter: - queryset = queryset.filter(priority=priority_filter) - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(ticket_no__icontains=search) - | Q(title__icontains=search) - | Q(description__icontains=search) - ) - - # 排序 - queryset = queryset.order_by('-created_at') - - # 分页 - paginator = Paginator(queryset, 20) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - # 统计各状态数量 - base_qs = self.get_provider_ticket_queryset() - status_counts = { - 'pending': base_qs.filter(status='pending').count(), - 'processing': base_qs.filter(status='processing').count(), - 'waiting_feedback': base_qs.filter( - status='waiting_feedback' - ).count(), - 'resolved': base_qs.filter(status='resolved').count(), - 'closed': base_qs.filter(status='closed').count(), - } - - context.update({ - 'page_obj': page_obj, - 'tickets': page_obj, - 'search': search, - 'status_filter': status_filter, - 'priority_filter': priority_filter, - 'status_counts': status_counts, - 'status_choices': Ticket.STATUS_CHOICES, - 'priority_choices': Ticket.PRIORITY_CHOICES, - 'page_title': '工单管理', - 'active_nav': 'tickets', - }) - return context - - -class TicketDetailView(ProviderTicketMixin, DetailView): - """ - 工单详情视图 - - 显示工单信息、评论列表、附件列表, - 支持添加评论和上传附件。 - """ - - template_name = 'admin_base/tickets/ticket_detail.html' - context_object_name = 'ticket' - pk_url_kwarg = 'pk' - - def get_queryset(self): - return self.get_provider_ticket_queryset() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - ticket = self.object - - # 评论列表(提供商不可见内部备注) - comments = ticket.comments.filter( - is_internal=False - ).select_related('author').order_by('created_at') - - # 附件列表 - attachments = ticket.attachments.select_related( - 'uploaded_by' - ).order_by('-created_at') - - # 活动记录 - activities = ticket.activities.select_related( - 'actor' - ).order_by('-created_at')[:10] - - context.update({ - 'comments': comments, - 'attachments': attachments, - 'activities': activities, - 'comment_form': TicketCommentForm(), - 'attachment_form': TicketAttachmentForm(), - 'page_title': f'工单 {ticket.ticket_no}', - 'active_nav': 'tickets', - }) - return context - - -# =========================================================================== -# 工单批量操作 -# =========================================================================== - - -def _get_selected_ids(request): - """从 POST 请求中获取选中的工单 ID 列表""" - selected = request.POST.getlist('selected_ids') - return [int(pk) for pk in selected if pk.isdigit()] - - -class TicketBatchProcessingView(ProviderTicketMixin, View): - """ - 批量标记工单为处理中 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何工单。') - return redirect('provider_tickets:ticket_list') - - qs = self.get_provider_ticket_queryset().filter( - pk__in=selected_ids, - status='pending', - ) - - updated_count = 0 - for ticket in qs: - ticket.status = 'processing' - ticket.assignee = request.user - ticket._current_user = request.user - ticket.save(update_fields=['status', 'assignee', 'updated_at']) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功将 {updated_count} 个工单标记为处理中。', - ) - else: - messages.warning( - request, - '没有可标记为处理中的工单。', - ) - - return redirect('provider_tickets:ticket_list') - - -class TicketBatchResolvedView(ProviderTicketMixin, View): - """ - 批量标记工单为已解决 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何工单。') - return redirect('provider_tickets:ticket_list') - - qs = self.get_provider_ticket_queryset().filter( - pk__in=selected_ids, - status__in=['pending', 'processing', 'waiting_feedback'], - ) - - updated_count = 0 - now = timezone.now() - for ticket in qs: - ticket.status = 'resolved' - ticket.resolved_at = now - ticket._current_user = request.user - ticket.save( - update_fields=['status', 'resolved_at', 'updated_at'] - ) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功将 {updated_count} 个工单标记为已解决。', - ) - else: - messages.warning( - request, - '没有可标记为已解决的工单。', - ) - - return redirect('provider_tickets:ticket_list') - - -class TicketBatchClosedView(ProviderTicketMixin, View): - """ - 批量关闭工单 (POST) - """ - - def post(self, request): - selected_ids = _get_selected_ids(request) - if not selected_ids: - messages.warning(request, '未选择任何工单。') - return redirect('provider_tickets:ticket_list') - - qs = self.get_provider_ticket_queryset().filter( - pk__in=selected_ids, - ).exclude(status='closed') - - updated_count = 0 - now = timezone.now() - for ticket in qs: - ticket.status = 'closed' - ticket.closed_at = now - ticket._current_user = request.user - ticket.save( - update_fields=['status', 'closed_at', 'updated_at'] - ) - updated_count += 1 - - if updated_count > 0: - messages.success( - request, - f'成功关闭了 {updated_count} 个工单。', - ) - else: - messages.warning( - request, - '没有可关闭的工单。', - ) - - return redirect('provider_tickets:ticket_list') - - -# =========================================================================== -# 工单评论 -# =========================================================================== - - -class TicketCommentCreateView(ProviderTicketMixin, View): - """ - 添加工单评论 (POST) - - 提供商添加的评论自动标记作者为当前用户。 - """ - - def post(self, request, pk): - ticket = get_object_or_404( - self.get_provider_ticket_queryset(), - pk=pk, - ) - - form = TicketCommentForm(request.POST) - if form.is_valid(): - comment = form.save(commit=False) - comment.ticket = ticket - comment.author = request.user - comment.save() - - messages.success(request, '评论已添加。') - else: - for field, errors in form.errors.items(): - for error in errors: - messages.error(request, error) - - return redirect('provider_tickets:ticket_detail', pk=ticket.pk) - - -# =========================================================================== -# 工单活动记录(独立只读页面) -# =========================================================================== - - -class TicketActivityListView(ProviderTicketMixin, TemplateView): - """ - 工单活动记录列表视图(独立只读页面) - - - 提供商数据隔离:只看到关联自己工单的活动 - - 支持按操作类型筛选、搜索 - - 只读,无增删改操作 - """ - - template_name = 'admin_base/tickets/activity_list.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - user = self.request.user - - queryset = self.get_provider_activity_queryset() - - # 操作类型筛选 - action_filter = self.request.GET.get('action', '').strip() - if action_filter: - queryset = queryset.filter(action=action_filter) - - # 搜索 - search = self.request.GET.get('search', '').strip() - if search: - queryset = queryset.filter( - Q(ticket__ticket_no__icontains=search) - | Q(actor__username__icontains=search) - | Q(description__icontains=search) - ) - - # 分页 - paginator = Paginator(queryset, 20) - page_number = self.request.GET.get('page', 1) - page_obj = paginator.get_page(page_number) - - context.update({ - 'page_obj': page_obj, - 'activities': page_obj, - 'search': search, - 'action_filter': action_filter, - 'action_choices': TicketActivity.ACTION_CHOICES, - 'page_title': '活动日志', - 'active_nav': 'activity_log', - }) - return context - - -# =========================================================================== -# 工单附件 -# =========================================================================== - - -class TicketAttachmentUploadView(ProviderTicketMixin, View): - """ - 上传工单附件 (POST) - """ - - def post(self, request, ticket_pk): - ticket = get_object_or_404( - self.get_provider_ticket_queryset(), - pk=ticket_pk, - ) - - form = TicketAttachmentForm(request.POST, request.FILES) - if form.is_valid(): - attachment = form.save(commit=False) - attachment.ticket = ticket - attachment.uploaded_by = request.user - if not attachment.filename: - attachment.filename = os.path.basename( - attachment.file.name - ) - attachment.save() - - messages.success(request, '附件上传成功。') - else: - for field, errors in form.errors.items(): - for error in errors: - messages.error(request, error) - - return redirect('provider_tickets:ticket_detail', pk=ticket.pk) - - -class TicketAttachmentDownloadView(ProviderTicketMixin, View): - """ - 下载工单附件 - """ - - def get(self, request, pk): - attachment = get_object_or_404(TicketAttachment, pk=pk) - - # 验证提供商是否有权访问此附件所属工单 - ticket = get_object_or_404( - self.get_provider_ticket_queryset(), - pk=attachment.ticket_id, - ) - - if not attachment.file: - raise Http404('附件文件不存在') - - try: - file_handle = attachment.file.open('rb') - except FileNotFoundError: - raise Http404('附件文件不存在') - - response = FileResponse( - file_handle, - as_attachment=True, - filename=attachment.filename, - ) - return response diff --git a/apps/tunnel/__init__.py b/apps/tunnel/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tunnel/admin.py b/apps/tunnel/admin.py deleted file mode 100644 index b3cef5e..0000000 --- a/apps/tunnel/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 隧道系统无需后台管理 diff --git a/apps/tunnel/apps.py b/apps/tunnel/apps.py deleted file mode 100644 index 65aed11..0000000 --- a/apps/tunnel/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class TunnelConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.tunnel' - verbose_name = '隧道管理' diff --git a/apps/tunnel/migrations/__init__.py b/apps/tunnel/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tunnel/models.py b/apps/tunnel/models.py deleted file mode 100644 index 71a8362..0000000 --- a/apps/tunnel/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/apps/tunnel/tests_dir/__init__.py b/apps/tunnel/tests_dir/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/tunnel/urls.py b/apps/tunnel/urls.py deleted file mode 100644 index c98e925..0000000 --- a/apps/tunnel/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path -from . import views - -app_name = 'tunnel' - -urlpatterns = [ - path('download/', views.download_tunnel_client, name='download'), - path('config/', views.get_tunnel_config, name='config'), - path('install/', views.install_tunnel_service, name='install'), -] diff --git a/apps/tunnel/views.py b/apps/tunnel/views.py deleted file mode 100644 index a96f346..0000000 --- a/apps/tunnel/views.py +++ /dev/null @@ -1,260 +0,0 @@ -import os -import logging -import secrets -import requests -import time -from django.http import JsonResponse, FileResponse, Http404 -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from django.conf import settings -from django.utils import timezone -from django.contrib.auth.decorators import login_required, permission_required -from django.core.cache import cache -from utils.helpers import get_client_ip - -logger = logging.getLogger(__name__) - -TUNNEL_RELEASES_URL = os.environ.get( - 'TUNNEL_RELEASES_URL', - 'https://api.github.com/repos/2c2a/tunnel/releases/latest' -) -TUNNEL_DOWNLOAD_DIR = os.path.join(settings.MEDIA_ROOT, 'tunnel_clients') - - -def _tunnel_rate_limit(key_prefix, rate='10/m'): - limit, period = rate.lower().split('/') - limit = int(limit) - period_map = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} - period_seconds = period_map.get(period, 60) - - def decorator(view_func): - def wrapper(request, *args, **kwargs): - ip = get_client_ip(request) - window = int(time.time() // period_seconds) - cache_key = f'rl:{key_prefix}:{ip}:{window}' - current = cache.get(cache_key, 0) - if current >= limit: - return JsonResponse( - {'success': False, 'error': 'Too many requests'}, - status=429, - ) - cache.set(cache_key, current + 1, timeout=period_seconds + 1) - return view_func(request, *args, **kwargs) - wrapper.__name__ = view_func.__name__ - return wrapper - return decorator - - -@csrf_exempt -@require_http_methods(["GET"]) -@_tunnel_rate_limit('tunnel_download', '5/m') -def download_tunnel_client(request): - """ - 下载tunnel客户端 - 支持从GitHub Release下载或从本地存储下载 - """ - try: - arch = request.GET.get('arch', 'amd64') - if arch not in ['amd64', 'arm64']: - return JsonResponse({ - 'success': False, - 'error': 'Invalid architecture. Use amd64 or arm64' - }, status=400) - - filename = f'2c2a-tunnel-windows-{arch}.exe' - local_path = os.path.join(TUNNEL_DOWNLOAD_DIR, filename) - - if os.path.exists(local_path): - return FileResponse( - open(local_path, 'rb'), - as_attachment=True, - filename=filename - ) - - try: - response = requests.get(TUNNEL_RELEASES_URL, timeout=10) - response.raise_for_status() - release_data = response.json() - - download_url = None - for asset in release_data.get('assets', []): - if asset['name'] == filename: - download_url = asset['browser_download_url'] - break - - if not download_url: - return JsonResponse({ - 'success': False, - 'error': f'Tunnel client not found for architecture: {arch}' - }, status=404) - - download_response = requests.get(download_url, stream=True, timeout=60) - download_response.raise_for_status() - - os.makedirs(TUNNEL_DOWNLOAD_DIR, exist_ok=True) - - with open(local_path, 'wb') as f: - for chunk in download_response.iter_content(chunk_size=8192): - f.write(chunk) - - return FileResponse( - open(local_path, 'rb'), - as_attachment=True, - filename=filename - ) - - except requests.RequestException as e: - logger.error(f"Failed to download tunnel client: {str(e)}") - return JsonResponse({ - 'success': False, - 'error': 'Failed to download tunnel client from GitHub' - }, status=503) - - except Exception as e: - logger.error(f"Error in download_tunnel_client: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Internal server error' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_tunnel_rate_limit('tunnel_config', '10/m') -def get_tunnel_config(request): - """ - 获取tunnel配置 - 需要验证session_token,返回tunnel_token和gateway地址 - """ - try: - import json - from apps.bootstrap.models import ActiveSession - from apps.hosts.models import Host - - data = json.loads(request.body.decode('utf-8')) - session_token = data.get('session_token') - - if not session_token: - return JsonResponse({ - 'success': False, - 'error': 'session_token is required' - }, status=400) - - try: - active_session = ActiveSession.objects.get( - session_token=session_token, - expires_at__gt=timezone.now() - ) - except ActiveSession.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired session token' - }, status=401) - - client_ip = get_client_ip(request) - if active_session.bound_ip != client_ip: - return JsonResponse({ - 'success': False, - 'error': 'IP address mismatch' - }, status=403) - - host = active_session.host - - if not host.tunnel_token: - host.tunnel_token = secrets.token_urlsafe(32) - host.connection_type = 'tunnel' - host.tunnel_status = 'offline' - host.save(update_fields=[ - 'tunnel_token', 'connection_type', 'tunnel_status' - ]) - - gateway_url = os.environ.get( - 'TUNNEL_GATEWAY_URL', - 'wss://gateway.2c2a.com:9000' - ) - - return JsonResponse({ - 'success': True, - 'data': { - 'tunnel_token': host.tunnel_token, - 'gateway_url': gateway_url, - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error in get_tunnel_config: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to get tunnel config' - }, status=500) - - -@csrf_exempt -@require_http_methods(["POST"]) -@_tunnel_rate_limit('tunnel_install', '5/m') -def install_tunnel_service(request): - """ - 一键安装tunnel服务 - 接收session_token,自动下载、配置并安装tunnel服务 - """ - try: - import json - import subprocess - import tempfile - from apps.bootstrap.models import ActiveSession - - data = json.loads(request.body.decode('utf-8')) - session_token = data.get('session_token') - arch = data.get('arch', 'amd64') - - if not session_token: - return JsonResponse({ - 'success': False, - 'error': 'session_token is required' - }, status=400) - - try: - active_session = ActiveSession.objects.get( - session_token=session_token, - expires_at__gt=timezone.now() - ) - except ActiveSession.DoesNotExist: - return JsonResponse({ - 'success': False, - 'error': 'Invalid or expired session token' - }, status=401) - - config_response = get_tunnel_config(request) - if config_response.status_code != 200: - return config_response - - config_data = json.loads(config_response.content) - tunnel_token = config_data['data']['tunnel_token'] - gateway_url = config_data['data']['gateway_url'] - - return JsonResponse({ - 'success': True, - 'data': { - 'message': 'Tunnel service installation initiated', - 'tunnel_token': tunnel_token, - 'gateway_url': gateway_url, - 'install_command': f'2c2a-tunnel.exe install -token {tunnel_token} -server {gateway_url}' - } - }) - - except json.JSONDecodeError: - return JsonResponse({ - 'success': False, - 'error': 'Invalid JSON in request body' - }, status=400) - except Exception as e: - logger.error(f"Error in install_tunnel_service: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': 'Failed to install tunnel service' - }, status=500) diff --git a/celerybeat-schedule b/celerybeat-schedule deleted file mode 100644 index 4abb835c492c1fab3775ba3af0d94e0e3d7bd17f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI1PiqrF6u@V*X_L0Z4PvOIMV25m6|+Qa59+0V%%L<*Xp>yLESuelJ7l}N-JK0> zpio6bVZlSkuORpp{0_x~CqIIoJb3WvO#aviV!Vm?Cd}@8^X9$0_nW!QY-7JlC{{aO zaA;8VF3O@r0&S=&LMXwz%=@@WTu6=;{ucVQmOxtnD5HEs38jmauJV4G1K5E85C8%| z00;m9AOHk_01yBIKmZ8*;{@7LYB86So~%%#?cj&Rq@pLb4=S6@irU=Vs#a7nq88^> z-n#fi-Ky5N)K+b0zg1Cd^`=^DRjaoL)}G-+cxWFR`1tBdVs`OCPC~@B@NszL@Uy*c zMARGTx`?jd5l?ZhPYa$I=>%9tnx(mZ;aoZph*V6FWFK z(c0LcEY37c8(WcsD?Hu_J7+~(iq{R_54E=E(U1m)uTdlHhT15!J_@b>2(6Dpr!9kG zN)9oLFU51VJ)inKb7KyvSMGh~mUcuON<=ED$Cw(F2K;a1bl-C^aXTItA8~O(d~dnuM~F?-owrrhV0 zwO3F1Q^=RZBG)h}>G2Azl8FOKI>h8k$n0d%zBz@-gtPv$bNaIS`>&Xt8zyb%i}n)# z@r-_v)AjKxdVy~n?jW6#MS@{2<+$|Q^%*)1($I%alR3Qovm7F=P?rm9y57Tq*gm?> zciP-=qoH_aU0c)2OuJf>3)doQ>ev-`2i95gg|@gSXUnwDr>Dl#2WzHrtuz{${+j1g zlF9{Az9<*UC*|XG510W2fB+Bx0zd!=00AHX1b_e#00KY&2>ew73vyl>-B&dfxgbs4 S7}RFvtVH{JVtC@Rq5d86I7T!8 diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100755 index 9d284a2..0000000 --- a/config/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -2c2a配置模块 -""" -from .celery import app as celery_app # noqa: E402,F401 - -__all__ = ('celery_app',) diff --git a/config/celery.py b/config/celery.py deleted file mode 100755 index 825880a..0000000 --- a/config/celery.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -from celery import Celery -from celery.schedules import crontab - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') - -app = Celery('2c2a') -app.config_from_object('django.conf:settings', namespace='CELERY') - -from django.conf import settings # noqa: E402 - -_redis_url = getattr(settings, 'REDIS_URL', '') -_base_dir = str(settings.BASE_DIR) - -if not app.conf.broker_url: - _broker = getattr(settings, 'CELERY_BROKER_URL', None) - if _broker: - app.conf.broker_url = _broker - elif _redis_url: - app.conf.broker_url = _redis_url.replace('/0', '/1') - else: - app.conf.broker_url = ( - f'sqla+sqlite:///{_base_dir}/celery_broker.sqlite3' - ) - -if not app.conf.result_backend: - _result = getattr(settings, 'CELERY_RESULT_BACKEND', None) - if _result: - app.conf.result_backend = _result - elif _redis_url: - app.conf.result_backend = _redis_url.replace('/0', '/2') - else: - app.conf.result_backend = ( - f'db+sqlite:///{_base_dir}/celery_results.sqlite3' - ) - -app.autodiscover_tasks() - -app.conf.update( - task_serializer='json', - accept_content=['json'], - result_serializer='json', - timezone='UTC', - enable_utc=True, - broker_connection_retry_on_startup=True, -) - -app.conf.task_routes = { - 'certificates.tasks.*': {'queue': 'certificates'}, - 'hosts.tasks.*': {'queue': 'hosts'}, - 'operations.tasks.*': {'queue': 'operations'}, - 'bootstrap.tasks.*': {'queue': 'bootstrap'}, - 'plugins.beta_push.tasks.*': {'queue': 'beta_push'}, - 'accounts.tasks.*': {'queue': 'accounts'}, -} - -app.conf.task_default_retry_delay = 30 -app.conf.task_max_retries = 3 - -app.conf.CELERY_BEAT_SCHEDULE = { - 'cleanup-expired-provision-tokens': { - 'task': 'apps.bootstrap.tasks.cleanup_expired_provision_tokens', - 'schedule': crontab(hour='0', minute='0'), - }, - 'cleanup-unactivated-certificates': { - 'task': 'apps.bootstrap.tasks.cleanup_unactivated_certificates', - 'schedule': crontab(hour='0', minute='0'), - }, - 'cleanup-orphan-cert-dirs': { - 'task': 'apps.bootstrap.tasks.cleanup_orphan_cert_dirs', - 'schedule': crontab(hour='0', minute='0'), - }, -} diff --git a/config/demo_middleware.py b/config/demo_middleware.py deleted file mode 100755 index 4b5dec4..0000000 --- a/config/demo_middleware.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -DEMO模式中间件 -用于处理演示模式下的特殊逻辑 -""" -import os -import secrets as _secrets -from django.http import JsonResponse -from django.shortcuts import redirect -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType -from apps.accounts.models import User -from apps.operations.models import AccountOpeningRequest, CloudComputerUser, Product -from apps.hosts.models import Host - - -class DemoModeMiddleware: - """ - DEMO模式中间件,处理演示环境的特殊逻辑 - """ - def __init__(self, get_response): - self.get_response = get_response - self.demo_mode = os.environ.get('2C2A_DEMO', '').lower() == '1' - if self.demo_mode: - self.setup_demo_users() - - def __call__(self, request): - if not self.demo_mode: - return self.get_response(request) - - # 在DEMO模式下设置特殊标志 - request.is_demo_mode = True - - response = self.get_response(request) - - return response - - def process_view(self, request, view_func, view_args, view_kwargs): - if not self.demo_mode: - return None - - # 为DEMO模式处理特定的视图逻辑 - # 检查是否是发送邮件的视图 - if hasattr(view_func, '__name__') and 'send_' in view_func.__name__ and 'email' in view_func.__name__: - # 模拟发送邮件成功,但实际上不发送 - if request.method == 'POST': - # 这里我们模拟一个成功响应 - return JsonResponse({'status': 'ok'}) - - # 检查是否是密码修改相关的视图 - if (hasattr(view_func, '__name__') and - ('password' in view_func.__name__.lower() or 'change' in view_func.__name__.lower()) and - any(pwd_keyword in request.path.lower() for pwd_keyword in ['password', 'pwd']) and - 'get-password' not in request.path.lower()): # 排除获取密码的阅后即焚功能 - # 在DEMO模式下,不允许修改密码 - if request.method == 'POST': - from django.contrib import messages - messages.error(request, 'DEMO模式下不允许修改密码') - # 重定向到profile页面或返回错误 - from django.shortcuts import redirect - referer = request.META.get('HTTP_REFERER', '/') - return redirect(referer) - - # 检查Django Admin密码更改URL - if ('/admin/password_change/' in request.path or - ('/admin/auth/user/' in request.path and 'password' in request.path)): - if request.method == 'POST': - from django.contrib import messages - messages.error(request, 'DEMO模式下不允许修改密码') - from django.shortcuts import redirect - referer = request.META.get('HTTP_REFERER', '/admin/') - return redirect(referer) - - # 检查所有可能的密码更改路径 - if ('password' in request.path.lower() and - ('change' in request.path.lower() or 'update' in request.path.lower()) and - 'get-password' not in request.path.lower()): # 排除获取密码的阅后即焚功能 - if request.method == 'POST': - from django.contrib import messages - messages.error(request, 'DEMO模式下不允许修改密码') - from django.shortcuts import redirect - referer = request.META.get('HTTP_REFERER', '/') - return redirect(referer) - - return None - - def setup_demo_users(self): - """ - 创建DEMO模式下的用户 - """ - User = get_user_model() - - # 创建User用户 - user, created = User.objects.get_or_create( - username='User', - defaults={ - 'email': 'user@example.com', - 'first_name': 'Demo', - 'last_name': 'User', - 'is_active': True, - } - ) - if created: - user.set_password(os.environ.get('2C2A_DEMO_USER_PASSWORD', _secrets.token_urlsafe(16))) - user.save() - - # 创建Admin用户 - admin, created = User.objects.get_or_create( - username='Admin', - defaults={ - 'email': 'admin@example.com', - 'first_name': 'Demo', - 'last_name': 'Admin', - 'is_staff': True, - 'is_active': True, - } - ) - if created: - admin.set_password(os.environ.get('2C2A_DEMO_ADMIN_PASSWORD', _secrets.token_urlsafe(16))) - # 分配特定权限 - self.assign_demo_permissions(admin) - admin.save() - - def assign_demo_permissions(self, user): - """ - 为DEMO模式下的Admin用户分配特定权限 - """ - permissions = [ - # View登录日志 - ('accounts', 'loginlog', 'view'), - # View开户申请 - ('operations', 'accountopeningrequest', 'view'), - # Change开户申请 - ('operations', 'accountopeningrequest', 'change'), - # View云电脑用户 - ('operations', 'cloudcomputeruser', 'view'), - # Change云电脑用户 - ('operations', 'cloudcomputeruser', 'change'), - # View产品 - ('operations', 'product', 'view'), - ] - - for app_label, model, perm_action in permissions: - try: - content_type = ContentType.objects.get(app_label=app_label, model=model) - permission_codename = f'{perm_action}_{model}' - permission = Permission.objects.get(content_type=content_type, codename=permission_codename) - user.user_permissions.add(permission) - except ContentType.DoesNotExist: - print(f"ContentType not found: {app_label}.{model}") - except Permission.DoesNotExist: - print(f"Permission not found: {app_label}.{model}.{perm_action}") - - -def is_demo_mode(): - """ - 检查是否处于DEMO模式 - """ - return os.environ.get('2C2A_DEMO', '').lower() == '1' \ No newline at end of file diff --git a/config/demo_startup.py b/config/demo_startup.py deleted file mode 100755 index b2a6cba..0000000 --- a/config/demo_startup.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -DEMO模式启动脚本 -""" -import os -from django.conf import settings -from django.core.management.color import color_style - - -def show_demo_startup_message(): - """ - 显示DEMO模式启动提示信息 - """ - if os.environ.get('2C2A_DEMO', '').lower() != '1': - return - - style = color_style() - - demo_message = """ -******************************************************************************** -* 2C2A DEMO MODE ACTIVATED * -******************************************************************************** -* * -* 当前系统运行在DEMO模式下,具有以下特性: * -* * -* 🔐 数据库: 使用 DEMO.sqlite3 (数据不会持久保存) * -* 👤 预设用户: * -* - 用户名: User, 密码: demo_user_password * -* - 用户名: Admin, 密码: demo_admin_password * -* - 用户名: SuperAdmin, 密码: DemoSuperAdmin123! (如果有创建) * -* 🛠️ 所有主机始终显示为在线状态 * -* 📧 邮件发送功能被模拟(不会实际发送邮件) * -* 🚀 WinRM指令不会实际执行(仅模拟) * -* 🔐 忽略密码复杂度要求 * -* * -* 💡 提示: 在DEMO模式下,您可以自由测试所有功能而不影响实际系统 * -******************************************************************************** -""" - - print(style.HTTP_INFO(demo_message)) \ No newline at end of file diff --git a/config/local_lock_middleware.py b/config/local_lock_middleware.py deleted file mode 100644 index 4a8e91e..0000000 --- a/config/local_lock_middleware.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -本地访问限制中间件 -当 SystemConfig.local_access_locked 启用时, -静默关闭来自 localhost/127.0.0.1 的连接 -""" -import logging -from django.http import HttpResponse - -logger = logging.getLogger('2c2a') - -LOCAL_IPS = frozenset({ - '127.0.0.1', - '::1', - '0.0.0.0', - '0000:0000:0000:0000:0000:0000:0000:0001', -}) - -LOCAL_HOSTNAMES = frozenset({ - 'localhost', -}) - - -class LocalLockMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - remote_addr = request.META.get('REMOTE_ADDR', '') - server_name = request.META.get('SERVER_NAME', '') - - is_local = ( - remote_addr in LOCAL_IPS - or remote_addr.lower() in LOCAL_HOSTNAMES - or server_name.lower() in LOCAL_HOSTNAMES - ) - - if is_local: - try: - from apps.dashboard.models import SystemConfig - config = SystemConfig.get_config() - if config.local_access_locked: - excluded_paths = ['/static/', '/media/'] - if not any( - request.path.startswith(p) - for p in excluded_paths - ): - logger.warning( - '本地访问已禁止,关闭来自 %s 的连接: %s', - remote_addr, request.path, - ) - return HttpResponse(status=403) - except Exception: - logger.exception( - 'LocalLockMiddleware 检查异常,默认拒绝' - ) - return HttpResponse(status=403) - - return self.get_response(request) diff --git a/config/maintenance_middleware.py b/config/maintenance_middleware.py deleted file mode 100755 index 9bad483..0000000 --- a/config/maintenance_middleware.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -维护模式中间件 -当REPAIRING环境变量为1时,将所有请求重定向到维护页面 -""" -import os -from django.shortcuts import render -from django.urls import reverse -from django.http import HttpResponseRedirect - - -class MaintenanceModeMiddleware: - """ - 维护模式中间件 - 当REPAIRING环境变量设置为1时,将所有请求重定向到维护页面 - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # 检查是否处于维护模式 - repairing = os.environ.get('REPAIRING', '0') - - # 排除维护页面本身和静态资源,避免无限重定向 - excluded_paths = [ - '/maintenance/', - '/static/', - '/media/', - ] - - # 如果处于维护模式且不在排除路径中,则重定向到维护页面 - if (repairing.lower() == '1' or repairing == 'true' or repairing == 'on' or repairing == 'yes' or repairing == 'enabled') and \ - not any(request.path.startswith(path) for path in excluded_paths): - - # 检查请求是否是AJAX请求,如果是则返回JSON错误而不是重定向 - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - from django.http import JsonResponse - return JsonResponse({ - 'error': '系统正在维护中,请稍后再试', - 'maintenance': True - }, status=503) - - # 对于非AJAX请求,渲染维护页面 - return render(request, 'maintenance.html') - - response = self.get_response(request) - return response \ No newline at end of file diff --git a/config/management/commands/init_demo.py b/config/management/commands/init_demo.py deleted file mode 100755 index fc89e3f..0000000 --- a/config/management/commands/init_demo.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -初始化DEMO环境的管理命令 -""" -from django.core.management.base import BaseCommand -from django.core.management import call_command -from django.db import DEFAULT_DB_ALIAS -import os - - -class Command(BaseCommand): - help = '初始化DEMO环境,包括数据库和用户' - - def add_arguments(self, parser): - parser.add_argument( - '--force', - action='store_true', - help='强制重新初始化DEMO环境(删除现有DEMO数据库)', - ) - - def handle(self, *args, **options): - # 检查是否在DEMO模式下运行 - if os.environ.get('2C2A_DEMO', '').lower() != '1': - self.stdout.write( - self.style.ERROR('请设置 2C2A_DEMO=1 环境变量以运行DEMO模式') - ) - return - - import shutil - from pathlib import Path - from django.conf import settings - - demo_db_path = settings.BASE_DIR / 'DEMO.sqlite3' - - if demo_db_path.exists() and not options['force']: - self.stdout.write( - self.style.WARNING(f'DEMO数据库已存在: {demo_db_path}') - + '\n使用 --force 参数强制重新初始化' - ) - return - - # 删除现有的DEMO数据库 - if demo_db_path.exists(): - demo_db_path.unlink() - self.stdout.write( - self.style.SUCCESS('已删除现有DEMO数据库') - ) - - # 运行迁移 - self.stdout.write('正在运行数据库迁移...') - call_command('migrate', verbosity=1, interactive=False, database=DEFAULT_DB_ALIAS) - - # 创建DEMO用户 - self.stdout.write('正在创建DEMO用户...') - call_command('setup_demo_users', verbosity=1) - - # 创建一些示例数据 - self.create_demo_data() - - self.stdout.write( - self.style.SUCCESS('DEMO环境初始化完成!') - ) - self.stdout.write('用户名: User, 密码: demo_user_password') - self.stdout.write('管理员: Admin, 密码: demo_admin_password') - - def create_demo_data(self): - """创建DEMO环境的示例数据""" - from apps.accounts.models import LoginLog - from apps.operations.models import Product, AccountOpeningRequest - from django.contrib.auth import get_user_model - from django.utils import timezone - import random - - User = get_user_model() - - # 获取DEMO用户 - try: - user = User.objects.get(username='User') - admin = User.objects.get(username='Admin') - except User.DoesNotExist: - return # 如果用户不存在,则跳过示例数据创建 - - # 创建一些示例登录日志 - for i in range(5): - LoginLog.objects.get_or_create( - user=user, - ip_address=f'192.168.1.{random.randint(1, 100)}', - user_agent='Mozilla/5.0 (DEMO Mode)', - login_type='web', - status='success', - created_at=timezone.now() - ) - - # 创建示例产品 - from apps.hosts.models import Host - # 创建一个示例主机 - demo_host, created = Host.objects.get_or_create( - name='DEMO主机', - hostname='demo.example.com', - username='demo', - host_type='server', - defaults={ - 'port': 5985, - 'description': 'DEMO模式下的示例主机', - 'status': 'online', # 在DEMO模式下总是在线 - } - ) - demo_host.password = 'demo_password' - demo_host.save() - - # 创建示例产品 - demo_product, created = Product.objects.get_or_create( - name='DEMO产品', - display_name='DEMO云电脑', - description='DEMO模式下的示例产品', - display_description='DEMO云电脑产品', - host=demo_host, - defaults={ - 'rdp_port': 3389, - 'display_hostname': 'demo.example.com', - 'is_available': True, - } - ) - - # 创建示例开户申请 - for i in range(3): - AccountOpeningRequest.objects.get_or_create( - applicant=admin, - contact_email=f'user{i}@demo.com', - username=f'demo_user_{i}', - user_fullname=f'DEMO User {i}', - user_email=f'user{i}@demo.com', - target_product=demo_product, - defaults={ - 'status': random.choice(['pending', 'approved', 'completed']), - 'user_description': f'DEMO用户 {i} 的描述', - } - ) \ No newline at end of file diff --git a/config/security_middleware.py b/config/security_middleware.py deleted file mode 100644 index 05c7e33..0000000 --- a/config/security_middleware.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.conf import settings - -class SecurityHeadersMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - - if not settings.DEBUG: - csp_parts = [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' " - "https://static.2c2a.cc.cd", - "style-src 'self' 'unsafe-inline' " - "https://static.2c2a.cc.cd", - "img-src 'self' data: blob: " - "https://static.2c2a.cc.cd", - "font-src 'self' " - "https://static.2c2a.cc.cd", - "connect-src 'self' wss://rdp.2c2a.com ws://rdp.2c2a.com", - "frame-ancestors 'none'", - "base-uri 'self'", - "form-action 'self'", - ] - response['Content-Security-Policy'] = '; '.join(csp_parts) - - response['Permissions-Policy'] = ( - 'geolocation=(), microphone=(), camera=(), ' - 'payment=(), usb=(), magnetometer=(), gyroscope=(), ' - 'accelerometer=()' - ) - - return response diff --git a/config/settings.py b/config/settings.py deleted file mode 100644 index 438480b..0000000 --- a/config/settings.py +++ /dev/null @@ -1,589 +0,0 @@ -""" -Django settings for 2c2a project. - -配置加载优先级(从高到低): -1. 环境变量(os.environ)- 方便 DEMO 配置和容器部署 -2. .env 文件 - 本地开发配置 -3. 默认值 - 确保基本可用 - -DEMO 模式(2C2A_DEMO=1)会强制锁定特定配置,不受 .env 影响。 -""" - -import os -import importlib -from pathlib import Path -from django.core.exceptions import ImproperlyConfigured - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# ========== .env 文件加载 ========== -# 优先级:环境变量 > .env 文件 > 默认值 -# 使用 python-dotenv 加载 .env,但不覆盖已存在的环境变量 -from dotenv import load_dotenv - -ENV_FILE = BASE_DIR / '.env' -if ENV_FILE.exists(): - load_dotenv(dotenv_path=ENV_FILE, override=False) - - -def _env(key, default=None): - """ - 读取配置的统一入口。 - 优先级:环境变量 > .env 文件 > 默认值 - """ - return os.environ.get(key, default) - - -# ========== 核心配置(必须在初始化时定义) ========== - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases -DB_ENGINE = _env('DB_ENGINE', 'sqlite').lower() - -if DB_ENGINE == 'mysql': - import pymysql - pymysql.install_as_MySQLdb() - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': _env('DB_NAME', '2c2a'), - 'USER': _env('DB_USER', 'root'), - 'PASSWORD': _env('DB_PASSWORD', ''), - 'HOST': _env('DB_HOST', '127.0.0.1'), - 'PORT': _env('DB_PORT', '3306'), - 'CONN_MAX_AGE': int(_env('DB_CONN_MAX_AGE', '60')), - 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': ( - "SET sql_mode='STRICT_TRANS_TABLES'" - ), - }, - } - } -elif DB_ENGINE == 'postgresql': - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': _env('DB_NAME', '2c2a'), - 'USER': _env('DB_USER', 'postgres'), - 'PASSWORD': _env('DB_PASSWORD', ''), - 'HOST': _env('DB_HOST', '127.0.0.1'), - 'PORT': _env('DB_PORT', '5432'), - 'CONN_MAX_AGE': int(_env('DB_CONN_MAX_AGE', '60')), - } - } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } - } - -# SECURITY WARNING: keep the secret key used in production secret! -_INSECURE_SECRET_KEY = 'django-insecure-change-this-in-production' -SECRET_KEY = _env('DJANGO_SECRET_KEY', _INSECURE_SECRET_KEY) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = _env('DEBUG', 'False').lower() == 'true' - -if SECRET_KEY == _INSECURE_SECRET_KEY and not DEBUG: - raise ImproperlyConfigured( - 'DJANGO_SECRET_KEY 环境变量必须设置,不允许在生产环境使用默认不安全密钥' - ) - -# 允许的主机列表 -# 在DEBUG模式下,允许所有主机 -if DEBUG: - ALLOWED_HOSTS = _env('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') - CSRF_TRUSTED_ORIGINS = [ - 'http://localhost', - 'http://127.0.0.1', - 'https://localhost', - 'https://127.0.0.1', - 'https://demo.supercmd.dpdns.org', - 'https://2c2a.supercmd.dpdns.org', - ] -else: - ALLOWED_HOSTS = _env('ALLOWED_HOSTS', '').split(',') - CSRF_TRUSTED_ORIGINS = _env('CSRF_TRUSTED_ORIGINS', 'https://localhost,https://127.0.0.1').split(',') - _ALLOWED_HOSTS_ENV = _env('ALLOWED_HOSTS', '') - if not _ALLOWED_HOSTS_ENV or _ALLOWED_HOSTS_ENV == 'localhost,127.0.0.1': - raise ImproperlyConfigured( - 'ALLOWED_HOSTS 环境变量必须在生产环境中显式配置为实际域名' - ) - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - # 第三方应用 - 'rest_framework', - 'corsheaders', - - # 模板组件框架(必须在本地应用之前,确保 cotton 模板优先发现) - 'django_cotton', - - # 本地应用 - 'django_tianai_captcha', - - 'apps.accounts', - 'apps.hosts', - 'apps.operations', - 'apps.dashboard', - 'apps.certificates', - 'apps.bootstrap', # 主机引导系统 - 'apps.audit', - 'apps.tasks', - 'apps.themes', # 主题系统 - 'apps.tunnel', # 隧道管理 - 'apps.tickets', # 工单系统 - 'apps.provider', # 提供商后台(新版 Tailwind/MD3) - 'apps.provider_backend', # 提供商后台(旧版,保留中间件和API) - 'plugins', -] - -# ========== 插件 Django App 动态注册 ========== -# 从 plugins.toml 读取需要注册为 Django App 的插件模块 -# 这样插件不存在时系统仍能正常启动(松耦合) -def _discover_plugin_apps(): - plugin_apps = [] - seen = set() - toml_path = BASE_DIR / 'plugins' / 'plugins.toml' - if not toml_path.exists(): - return plugin_apps - try: - import toml - toml_data = toml.loads(toml_path.read_text(encoding='utf-8')) - for section in ('builtin', 'third_party'): - for _key, info in toml_data.get(section, {}).items(): - if not info.get('enabled', True): - continue - if not info.get('django_app', True): - continue - module = info.get('module', '') - if not module: - continue - parts = module.split('.') - if len(parts) >= 2 and parts[0] == 'plugins': - app_module = '.'.join(parts[:2]) - else: - app_module = module - if app_module in seen: - continue - seen.add(app_module) - pkg_dir = ( - BASE_DIR / 'plugins' / app_module.split('.')[-1] - ) - if pkg_dir.is_dir() and (pkg_dir / '__init__.py').exists(): - plugin_apps.append(app_module) - except Exception: - pass - return plugin_apps - -INSTALLED_APPS += _discover_plugin_apps() - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'config.maintenance_middleware.MaintenanceModeMiddleware', - 'config.local_lock_middleware.LocalLockMiddleware', - 'config.security_middleware.SecurityHeadersMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'apps.dashboard.middleware.SiteGroupMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'apps.bootstrap.middleware.SessionValidationMiddleware', - 'config.demo_middleware.DemoModeMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'config.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - BASE_DIR / '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', - 'apps.dashboard.context_processors.system_config', - ], - }, - }, -] - -WSGI_APPLICATION = 'config.wsgi.application' - -# Custom user model -AUTH_USER_MODEL = 'accounts.User' - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'zh-hans' - -TIME_ZONE = 'Asia/Shanghai' - -USE_I18N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = BASE_DIR / 'staticfiles' -STATICFILES_DIRS = [BASE_DIR / 'static'] - -# Media files -MEDIA_URL = 'media/' -MEDIA_ROOT = BASE_DIR / 'media' - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# REST Framework settings -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.SessionAuthentication', - ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', - ], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 20, - 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.UserRateThrottle', - ], - 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/hour', - 'user': '1000/hour', - }, -} - -# CORS settings -CORS_ALLOW_ALL_ORIGINS = False -CORS_ALLOWED_ORIGINS = [ - origin.strip() - for origin in _env( - 'CORS_ALLOWED_ORIGINS', - 'http://localhost:8000,https://localhost,http://127.0.0.1:8000' - ).split(',') - if origin.strip() -] - - -# Winrm settings -WINRM_TIMEOUT = int(_env('WINRM_TIMEOUT', '30')) # Winrm连接超时时间(秒) -WINRM_MAX_RETRIES = int(_env('WINRM_RETRY_COUNT', '3')) # Winrm连接最大重试次数 - -# Logging settings -# 默认只输出到 stdout,方便 nohup/systemd 等收集日志 -# 如需文件日志,设置环境变量 LOG_FILE=/path/to/2c2a.log -LOG_LEVEL = _env('LOG_LEVEL', 'INFO') -LOG_FILE = _env('LOG_FILE', '') - -_logging_handlers = { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, -} -_logging_root_handlers = ['console'] -_logging_logger_handlers = ['console'] - -if LOG_FILE: - _logging_handlers['file'] = { - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': LOG_FILE, - 'maxBytes': 1024 * 1024 * 10, # 10MB - 'backupCount': 5, - 'formatter': 'verbose', - } - _logging_root_handlers.append('file') - _logging_logger_handlers.append('file') - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', - }, - }, - 'handlers': _logging_handlers, - 'root': { - 'handlers': _logging_root_handlers, - 'level': LOG_LEVEL, - }, - 'loggers': { - 'django': { - 'handlers': _logging_logger_handlers, - 'level': LOG_LEVEL, - 'propagate': False, - }, - '2c2a': { - 'handlers': _logging_logger_handlers, - 'level': 'DEBUG' if DEBUG else 'INFO', - 'propagate': False, - }, - }, -} - -# 安全配置 -SECURE_BROWSER_XSS_FILTER = True -SECURE_CONTENT_TYPE_NOSNIFF = True -X_FRAME_OPTIONS = 'DENY' -SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' - -SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin' - -if not DEBUG: - SECURE_CROSS_ORIGIN_EMBEDDER_POLICY = 'require-corp' - SECURE_CROSS_ORIGIN_RESOURCE_POLICY = 'same-origin' - -USE_X_FORWARDED_FOR = _env( - 'USE_X_FORWARDED_FOR', 'False' -).lower() == 'true' - -# 可信反向代理 IP 集合(nginx 等) -# 当 REMOTE_ADDR 在此集合中时,从 X-Forwarded-For / X-Real-IP 获取真实客户端 IP -# 防止代理场景下所有用户共享同一 IP 导致限流误触发 -TRUSTED_PROXY_IPS = set( - _env('TRUSTED_PROXY_IPS', '127.0.0.1,::1').split(',') -) - -SESSION_COOKIE_SECURE = _env( - 'SESSION_COOKIE_SECURE', 'True' if not DEBUG else 'False' -).lower() == 'true' -CSRF_COOKIE_SECURE = _env( - 'CSRF_COOKIE_SECURE', 'True' if not DEBUG else 'False' -).lower() == 'true' -SESSION_COOKIE_HTTPONLY = True -SESSION_COOKIE_SAMESITE = 'Lax' -CSRF_COOKIE_SAMESITE = 'Lax' -CSRF_COOKIE_HTTPONLY = True -SESSION_COOKIE_AGE = int(_env('SESSION_COOKIE_AGE', '3600')) -SESSION_EXPIRE_AT_BROWSER_CLOSE = True - -# HTTPS相关安全配置 (仅在生产环境中启用) -if not DEBUG: - SECURE_SSL_REDIRECT = _env('SECURE_SSL_REDIRECT', 'True').lower() == 'true' - SECURE_HSTS_SECONDS = 31536000 # 一年 - SECURE_HSTS_INCLUDE_SUBDOMAINS = True - SECURE_HSTS_PRELOAD = True - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - -# ========== Redis 可选配置 ========== -# Redis 是锦上添花的增强组件,不配置时程序使用本地替代方案。 -# 配置 REDIS_URL 且 Redis 服务可达时,自动用于缓存、会话、Celery。 -# import redis 采用延迟导入:REDIS_URL 未配置时不 import,redis 包未安装也不报错。 -REDIS_URL = _env('REDIS_URL', '') - -def _check_redis_available(): - """检测 Redis 是否配置且可达(延迟导入 redis 包)""" - if not REDIS_URL: - return False - try: - import redis as _redis - client = _redis.Redis.from_url(REDIS_URL, socket_connect_timeout=3) - client.ping() - return True - except Exception: - return False - -REDIS_ENABLED = _check_redis_available() - -# ========== 缓存配置 ========== -if REDIS_ENABLED: - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': REDIS_URL, - 'KEY_PREFIX': '2c2a', - 'TIMEOUT': 300, - }, - } -else: - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': '2c2a-inmemory', - 'KEY_PREFIX': '2c2a', - 'TIMEOUT': 300, - 'OPTIONS': { - 'MAX_ENTRIES': 1000, - }, - }, - } - -# ========== 会话引擎 ========== -if REDIS_ENABLED: - SESSION_ENGINE = 'django.contrib.sessions.backends.cache' - SESSION_CACHE_ALIAS = 'default' -else: - SESSION_ENGINE = 'django.contrib.sessions.backends.db' - -# ========== Celery 配置 ========== -if REDIS_ENABLED: - CELERY_BROKER_URL = _env( - 'CELERY_BROKER_URL', - REDIS_URL.replace('/0', '/1') if REDIS_URL else 'redis://localhost:6379/1', - ) - CELERY_RESULT_BACKEND = _env( - 'CELERY_RESULT_BACKEND', - REDIS_URL.replace('/0', '/2') if REDIS_URL else 'redis://localhost:6379/2', - ) - CELERY_BROKER_TRANSPORT_OPTIONS = { - 'polling_interval': 1, - 'max_connections': 20, - } -else: - CELERY_BROKER_URL = f'sqla+sqlite:///{BASE_DIR / "celery_broker.sqlite3"}' - CELERY_RESULT_BACKEND = f'db+sqlite:///{BASE_DIR / "celery_results.sqlite3"}' - CELERY_BROKER_TRANSPORT_OPTIONS = { - 'polling_interval': 1, - } - -# ========== 限流配置 ========== -LOGIN_RATE_LIMIT = int(_env('LOGIN_RATE_LIMIT', '5')) -API_RATE_LIMIT = int(_env('API_RATE_LIMIT', '100')) - -# Gateway 控制面配置 -GATEWAY_ENABLED = _env( - 'GATEWAY_ENABLED', 'False' -).lower() in ('true', '1', 'yes') -GATEWAY_CONTROL_SOCKET = _env( - 'GATEWAY_CONTROL_SOCKET', '/run/2c2a/control.sock' -) - -GATEWAY_PAA_TOKEN_SIGNING_KEY = _env( - 'GATEWAY_PAA_TOKEN_SIGNING_KEY', 'change-me-32-chars-minimum!!' -) - -_INSECURE_GATEWAY_KEY = 'change-me-32-chars-minimum!!' -if not DEBUG and GATEWAY_ENABLED and GATEWAY_PAA_TOKEN_SIGNING_KEY == _INSECURE_GATEWAY_KEY: - raise ImproperlyConfigured( - 'GATEWAY_PAA_TOKEN_SIGNING_KEY 环境变量必须设置,不允许在生产环境使用默认不安全密钥' - ) -GATEWAY_PAA_TOKEN_EXPIRY_SECONDS = int(_env( - 'GATEWAY_PAA_TOKEN_EXPIRY_SECONDS', '600' -)) -GATEWAY_ADDRESS = _env('GATEWAY_ADDRESS', 'rdp.2c2a.com') -GATEWAY_PORT = int(_env('GATEWAY_PORT', '443')) - -# RDP 域名配置 -RDP_DOMAIN = _env('RDP_DOMAIN', '2c2a.com') - -# ========== DEMO模式配置(强制锁定,不受 .env 影响) ========== -DEMO_MODE = os.environ.get('2C2A_DEMO', '').lower() == '1' - -if DEMO_MODE: - # DEMO模式强制使用 DEMO.sqlite3,不受 DB_ENGINE 或 .env 影响 - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'DEMO.sqlite3', - } - } - - # DEMO模式保留最小长度验证,仅放宽复杂度要求 - AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - 'OPTIONS': {'min_length': 4}, - }, - ] - - ALLOWED_HOSTS = _env( - 'ALLOWED_HOSTS', - 'localhost,127.0.0.1,demo.supercmd.dpdns.org,2c2a.supercmd.dpdns.org' - ).split(',') - - # DEBUG模式开启 - DEBUG = True - - # 生成随机SECRET_KEY(每次启动不同) - import secrets as _secrets - SECRET_KEY = _secrets.token_urlsafe(50) - import logging as _logging - _logging.getLogger('2c2a').warning('DEMO模式: 使用随机生成的SECRET_KEY,重启后所有session将失效') - -# DEMO模式启动消息 -if DEMO_MODE: - from config.demo_startup import show_demo_startup_message - show_demo_startup_message() - -# Create logs directory if it doesn't exist -os.makedirs(BASE_DIR / 'logs', exist_ok=True) - -# Bootstrap认证配置 -BOOTSTRAP_SHARED_SALT = _env('BOOTSTRAP_SHARED_SALT', '') - -if not DEBUG and not BOOTSTRAP_SHARED_SALT: - import logging as _bootstrap_logging - _bootstrap_logging.getLogger('2c2a').warning( - 'BOOTSTRAP_SHARED_SALT 未设置,建议在生产环境中配置此值以增强引导认证安全性' - ) - -CAPTCHA = { - "PREFIX": "captcha", - "EXPIRE": { - "default": 120, - "WORD_IMAGE_CLICK": 180, - }, - "INIT_DEFAULT_RESOURCE": True, - "CACHE_BACKEND": "redis" if REDIS_ENABLED else "local", - "REDIS_URL": REDIS_URL if REDIS_ENABLED else "", - "DEFAULT_TYPE": "SLIDER", - "TOLERANT": 0.02, - "TRACK_VALIDATION_ENABLED": True, - "SECONDARY": { - "ENABLED": True, - "EXPIRE": 120, - "KEY_PREFIX": "captcha:secondary", - }, - "RATE_LIMIT": { - "ENABLED": True, - "RATE": 10, - "PERIOD": 60, - }, -} diff --git a/config/tests.py b/config/tests.py deleted file mode 100644 index 263e017..0000000 --- a/config/tests.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from django.test import RequestFactory -from config.views import static_fallback_view - - -@pytest.mark.django_db -class TestStaticFallbackView: - def setup_method(self): - self.factory = RequestFactory() - - def test_path_traversal_blocked(self): - request = self.factory.get("/static/../../etc/passwd") - response = static_fallback_view(request, "../../etc/passwd") - assert response.status_code == 400 - - def test_absolute_url_scheme_blocked(self): - request = self.factory.get("/static/https://evil.com") - response = static_fallback_view(request, "https://evil.com") - assert response.status_code == 400 - - def test_absolute_url_netloc_blocked(self): - request = self.factory.get("/static//evil.com/path") - response = static_fallback_view(request, "//evil.com/path") - assert response.status_code == 400 - - def test_backslash_url_blocked(self): - request = self.factory.get("/static/\\\\evil.com/path") - response = static_fallback_view(request, "\\\\evil.com/path") - assert response.status_code == 400 - - def test_normal_path_served_or_redirected(self): - request = self.factory.get("/static/css/base.css") - response = static_fallback_view(request, "css/base.css") - assert response.status_code in (200, 302) - - def test_normal_js_path_served_or_redirected(self): - request = self.factory.get("/static/js/base.js") - response = static_fallback_view(request, "js/base.js") - assert response.status_code in (200, 302) - - def test_nonexistent_path_redirects_to_cdn(self): - request = self.factory.get("/static/nonexistent/file.xyz") - response = static_fallback_view(request, "nonexistent/file.xyz") - assert response.status_code == 302 - assert "static.2c2a.cc.cd" in response.url diff --git a/config/urls.py b/config/urls.py deleted file mode 100755 index 1466471..0000000 --- a/config/urls.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -2c2a URL Configuration -""" -from django.contrib.staticfiles.views import serve -from django.urls import path, include, re_path -from django.conf import settings -from django.conf.urls.static import static -from django.views.generic import TemplateView -from config import views - -urlpatterns = [ - path('admin/', include('apps.accounts.urls_admin')), - path('provider/', include('apps.accounts.urls_provider')), - path('api/', include('rest_framework.urls')), - path('accounts/', include('apps.accounts.urls')), - path('operations/', include('apps.operations.urls')), - path('certificates/', include('apps.certificates.urls')), - path('bootstrap/', include('apps.bootstrap.urls')), - path('audit/', include('apps.audit.urls')), - path('tasks/', include('apps.tasks.urls')), - path('tunnel/', include('apps.tunnel.urls')), - path('tickets/', include('apps.tickets.urls')), - path('captcha/', include('django_tianai_captcha.urls')), - path('docs/', views.docs_index, name='docs_index'), - path('', include('apps.dashboard.urls')), - path('404/', TemplateView.as_view(template_name='errors/404.html'), name='404'), - path('favicon.ico', views.favicon_view), - path('favicon.svg', views.favicon_svg_view), -] - -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -else: - # 生产环境:static 文件降级逻辑 - # 本地找不到时自动重定向到 static.2c2a.cc.cd - urlpatterns += [ - re_path(r'^static/(?P.*)$', views.static_fallback_view), - ] - -handler404 = 'config.views.custom_404' -handler500 = 'config.views.custom_500' diff --git a/config/views.py b/config/views.py deleted file mode 100755 index 6b5bdc4..0000000 --- a/config/views.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -自定义错误处理视图 -""" -import re -from urllib.parse import urlparse - -from django.shortcuts import render, redirect -from django.views.static import serve -from django.conf import settings -from django.http import HttpResponse, HttpResponseBadRequest -import os - -from apps.dashboard.models import SystemConfig - - -def custom_404(request, exception): - """ - 自定义404错误页面 - - Args: - request: HTTP请求对象 - exception: 异常对象 - - Returns: - HttpResponse: 404错误页面 - """ - return render(request, 'errors/404.html', status=404) - - -def custom_500(request): - """ - 自定义500错误页面 - - Args: - request: HTTP请求对象 - - Returns: - HttpResponse: 500错误页面 - """ - return render(request, 'errors/500.html', status=500) - - -def _get_default_favicon_path(filename): - return os.path.join( - settings.STATIC_ROOT or settings.STATICFILES_DIRS[0], - 'img', filename - ) - - -def _serve_hostname_favicon(request, default_filename): - hostname = request.get_host().split(':')[0] - site_icon = None - - site_group = getattr(request, 'site_group', None) - if site_group and site_group.site_icon: - site_icon = site_group.site_icon - else: - try: - config = SystemConfig.get_config() - site_icon = config.get_site_icon_for_hostname(hostname) - except Exception: - site_icon = '/static/img/favicon.svg' - - if site_icon and site_icon != '/static/img/favicon.svg': - if site_icon.startswith(settings.MEDIA_URL): - rel_path = site_icon[len(settings.MEDIA_URL):] - file_path = os.path.join(settings.MEDIA_ROOT, rel_path) - elif site_icon.startswith('/'): - file_path = os.path.join(settings.BASE_DIR, site_icon.lstrip('/')) - else: - file_path = site_icon - - if os.path.exists(file_path) and os.path.isfile(file_path): - content_type = 'image/svg+xml' - if file_path.endswith('.ico'): - content_type = 'image/x-icon' - elif file_path.endswith('.png'): - content_type = 'image/png' - with open(file_path, 'rb') as f: - return HttpResponse(f.read(), content_type=content_type) - - favicon_path = _get_default_favicon_path(default_filename) - if not os.path.exists(favicon_path): - favicon_path = os.path.join( - settings.STATICFILES_DIRS[0], 'img', default_filename - ) - return serve( - request, os.path.basename(favicon_path), - document_root=os.path.dirname(favicon_path) - ) - - -def favicon_view(request): - return _serve_hostname_favicon(request, 'favicon.ico') - - -def favicon_svg_view(request): - return _serve_hostname_favicon(request, 'favicon.svg') - - -# Static 文件降级服务域名 -STATIC_FALLBACK_HOST = 'https://static.2c2a.cc.cd' - - -def static_fallback_view(request, path): - """ - 生产环境 static 文件降级视图 - - 逻辑: - 1. 先尝试从本地 STATIC_ROOT 或 STATICFILES_DIRS 中查找并 serve 文件 - 2. 如果本地文件不存在,则 302 重定向到外部 static 服务 - - Args: - request: HTTP请求对象 - path: static 文件路径 - - Returns: - HttpResponse: 本地文件或 302 重定向响应 - """ - document_root = None - - if settings.STATIC_ROOT and os.path.exists(settings.STATIC_ROOT): - document_root = settings.STATIC_ROOT - elif settings.STATICFILES_DIRS: - for static_dir in settings.STATICFILES_DIRS: - if os.path.exists(static_dir): - document_root = static_dir - break - - if document_root: - real_root = os.path.realpath(document_root) - file_path = os.path.realpath(os.path.join(document_root, path)) - if not file_path.startswith(real_root + os.sep) and file_path != real_root: - return HttpResponseBadRequest('Invalid path') - if os.path.exists(file_path) and os.path.isfile(file_path): - return serve(request, os.path.relpath(file_path, real_root), document_root=real_root) - - sanitized = path.replace('\\', '/') - parsed = urlparse(sanitized) - if parsed.scheme or parsed.netloc: - return HttpResponseBadRequest('Invalid path') - - redirect_url = f"{STATIC_FALLBACK_HOST}/static/{sanitized}" - return redirect(redirect_url, permanent=False) - - -USER_DOCS_FILE = settings.BASE_DIR / 'USER_DOCS.md' - - -def docs_index(request): - md_text = '' - if USER_DOCS_FILE.exists(): - with open(USER_DOCS_FILE, 'r', encoding='utf-8') as f: - md_text = f.read() - return render(request, 'docs/index.html', { - 'doc_title': '用户手册', - 'md_text': md_text, - }) \ No newline at end of file diff --git a/config/wsgi.py b/config/wsgi.py deleted file mode 100755 index 1ba51ab..0000000 --- a/config/wsgi.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -WSGI config for 2c2a project. -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') - -application = get_wsgi_application() diff --git a/conftest.py b/conftest.py deleted file mode 100644 index deda15c..0000000 --- a/conftest.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -from django.contrib.auth.models import User, Group - - -@pytest.fixture -def admin_user(db): - user = User.objects.create_superuser( - username="admin_test", - email="admin@test.com", - password="testpass123", - ) - return user - - -@pytest.fixture -def normal_user(db): - user = User.objects.create_user( - username="user_test", - email="user@test.com", - password="testpass123", - ) - return user - - -@pytest.fixture -def provider_user(db): - provider_group, _ = Group.objects.get_or_create(name="provider") - user = User.objects.create_user( - username="provider_test", - email="provider@test.com", - password="testpass123", - ) - user.groups.add(provider_group) - return user - - -@pytest.fixture -def client_logged_in(client, normal_user): - client.login(username="user_test", password="testpass123") - return client - - -@pytest.fixture -def admin_client_logged_in(client, admin_user): - client.login(username="admin_test", password="testpass123") - return client diff --git a/manage.py b/manage.py deleted file mode 100755 index 8e7ac79..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', 'config.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/plugins/README.md b/plugins/README.md deleted file mode 100755 index adb1181..0000000 --- a/plugins/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# 插件系统 - -插件系统为2c2a提供可扩展的功能模块支持,采用松耦合设计,支持动态加载和管理插件。 - -## 架构设计 - -插件系统采用分层架构,将核心接口与具体实现分离: - -- **core/**: 核心组件目录,包含接口定义和插件管理器 -- **各插件目录**: 每个插件都有独立的目录,包含其所有相关文件 - -## 核心概念 - -### PluginInterface - -所有插件必须继承 `PluginInterface` 抽象基类,并实现以下方法: - -- `initialize()`: 初始化插件 -- `shutdown()`: 关闭插件 - -### 插件管理器 - -`PluginManager` 负责插件的加载、初始化、运行和卸载。 - -## 开发新插件 - -### 1. 创建插件目录 - -为新插件创建独立的目录: - -```bash -mkdir plugins/new_plugin_name -``` - -### 2. 实现插件类 - -创建插件实现文件,继承 `PluginInterface`: - -```python -from plugins.core.base import PluginInterface - -class NewPlugin(PluginInterface): - def __init__(self): - super().__init__( - plugin_id="new_plugin", - name="新插件", - version="1.0.0", - description="新插件描述" - ) - - def initialize(self) -> bool: - # 初始化插件 - return True - - def shutdown(self) -> bool: - # 关闭插件 - return True -``` - -### 3. 注册插件 - -在 [available_plugins.py](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/available_plugins.py) 文件中注册插件: - -```python -BUILTIN_PLUGINS = { - 'new_plugin': { - 'name': '新插件', - 'module': 'plugins.new_plugin.new_plugin', - 'class': 'NewPlugin', - 'description': '新插件描述', - 'version': '1.0.0', - 'enabled': True - } -} -``` - -## 现有插件 - -### QQ验证插件 - -位于 `plugins/qq_verification/` 目录,提供QQ群验证功能: - -- 检测QQ号是否在指定群中 -- 支持"只有加入了某个群才允许使用机器"模式 -- 支持"老六模式"(对已有云电脑用户进行验证) - -## 目录结构 - -参见 [STRUCTURE.md](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/STRUCTURE.md) 文件了解详细的目录结构说明。 - -## 运行时管理 - -插件系统通过 `PluginManager` 实现插件的动态管理,支持: - -- 插件热加载 -- 插件状态监控 -- 事件钩子机制 \ No newline at end of file diff --git a/plugins/STRUCTURE.md b/plugins/STRUCTURE.md deleted file mode 100755 index 4435b07..0000000 --- a/plugins/STRUCTURE.md +++ /dev/null @@ -1,57 +0,0 @@ -# 插件系统目录结构 - -## 概述 -插件系统采用分层架构设计,将核心接口与具体实现分离,确保系统的可扩展性和可维护性。 - -## 目录结构 - -``` -plugins/ # 插件系统根目录 -├── core/ # 核心组件目录 -│ ├── __init__.py # 核心模块初始化 -│ ├── base.py # 插件接口定义 -│ └── plugin_manager.py # 插件管理器 -├── qq_verification/ # QQ验证插件 -│ ├── __init__.py # 插件初始化 -│ ├── qq_checker.py # QQ验证核心功能 -│ └── qq_verification_plugin.py # QQ验证插件实现 -├── sample_plugins/ # 示例插件目录 -│ ├── __init__.py # 示例插件初始化 -│ └── ... # 各种示例插件 -├── __init__.py # 插件系统初始化 -├── apps.py # Django应用配置 -├── models.py # 插件相关的Django模型 -├── admin.py # Django管理界面配置 -├── signals.py # Django信号处理器 -├── available_plugins.py # 可用插件配置 -├── README.md # 插件系统说明 -└── migrations/ # 数据库迁移文件 -``` - -## 核心组件 (core/) - -- **base.py**: 定义了[PluginInterface](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/core/base.py#L8-L48)抽象基类和其他核心接口 -- **plugin_manager.py**: 实现插件的加载、管理、运行和卸载功能 -- **__init__.py**: 核心模块初始化 - -## 具体插件目录 - -每个插件都有自己的目录,包含该插件的所有相关文件: - -- **qq_verification/**: QQ验证插件 - - [qq_checker.py](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/qq_verification/qq_checker.py): 实现QQ群验证的核心功能 - - [qq_verification_plugin.py](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/qq_verification/qq_verification_plugin.py): 实现[PluginInterface](file:///Users/Supercmd/Desktop/Python/2c2a/plugins/core/base.py#L8-L48)的具体插件类 - - **__init__.py**: 插件初始化 - -## 配置文件 - -- **available_plugins.py**: 定义系统中所有可用的插件及其配置信息 -- **models.py**: 插件相关的数据库模型 -- **signals.py**: Django信号处理器,用于集成插件功能 - -## 设计优势 - -1. **清晰分离**: 核心接口与具体实现完全分离 -2. **易于扩展**: 添加新插件只需创建新目录和实现接口 -3. **易于维护**: 每个插件的功能封装在自己的目录中 -4. **降低耦合**: 插件之间相互独立,减少依赖关系 \ No newline at end of file diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100755 index f1dfe29..0000000 --- a/plugins/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 插件系统初始化 -default_app_config = 'plugins.apps.PluginsConfig' \ No newline at end of file diff --git a/plugins/admin.py b/plugins/admin.py deleted file mode 100755 index f6d83b8..0000000 --- a/plugins/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 已迁移至自建超管后台 (plugins.views_admin) 和提供商后台 (plugins.views_provider) diff --git a/plugins/apps.py b/plugins/apps.py deleted file mode 100755 index 3442f0e..0000000 --- a/plugins/apps.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -插件应用配置 -""" - -from django.apps import AppConfig - - -class PluginsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'plugins' - verbose_name = '插件管理' - - def ready(self): - # 导入信号处理器 - import plugins.signals - - # 初始化插件管理器并加载所有插件 - from .core.plugin_manager import get_plugin_manager - plugin_manager = get_plugin_manager() - plugin_manager.load_all_builtin_plugins() \ No newline at end of file diff --git a/plugins/available_plugins.py b/plugins/available_plugins.py deleted file mode 100755 index 6333349..0000000 --- a/plugins/available_plugins.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -可用插件配置 -定义系统中所有可用的插件 -""" -import toml -import os -from django.conf import settings - - -# 加载 TOML 配置文件 -toml_file_path = os.path.join(settings.BASE_DIR, 'plugins', 'plugins.toml') -if os.path.exists(toml_file_path): - with open(toml_file_path, 'r', encoding='utf-8') as f: - toml_data = toml.load(f) -else: - # 如果 TOML 文件不存在,使用默认配置 - toml_data = { - 'builtin': {}, - 'third_party': {} - } - - -# 系统内置插件 -BUILTIN_PLUGINS = toml_data.get('builtin', {}) - -# 第三方插件(如果有的话) -THIRD_PARTY_PLUGINS = toml_data.get('third_party', {}) - -# 合并所有插件 -ALL_AVAILABLE_PLUGINS = {**BUILTIN_PLUGINS, **THIRD_PARTY_PLUGINS} \ No newline at end of file diff --git a/plugins/beta_push/.gitignore b/plugins/beta_push/.gitignore deleted file mode 100644 index 9e4992f..0000000 --- a/plugins/beta_push/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -__pycache__/ -*.pyc -*.pyo -.mypy_cache/ -.pytest_cache/ -*.egg-info/ -dist/ -build/ -.env diff --git a/plugins/beta_push/__init__.py b/plugins/beta_push/__init__.py deleted file mode 100644 index 8fe5100..0000000 --- a/plugins/beta_push/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -import logging - -from plugins.core.base import PluginInterface, URLProvider, UIExtension, UIExtensionProvider - -logger = logging.getLogger(__name__) - - -class BetaPushPlugin(PluginInterface, URLProvider, UIExtensionProvider): - - def __init__(self): - super().__init__( - plugin_id='beta_push', - name='Beta数据推送', - version='1.0.0', - description='将生产环境数据异步推送到Beta版本数据库,支持增量同步', - ) - - def initialize(self) -> bool: - return True - - def shutdown(self) -> bool: - return True - - def get_url_patterns(self): - return [ - { - 'prefix': 'beta-push/', - 'module': 'plugins.beta_push.urls', - 'namespace': 'beta_push', - 'section': URLProvider.PROVIDER, - }, - ] - - def get_ui_extensions(self): - return [ - UIExtension( - extension_type=UIExtension.NAV_ITEM, - slot='admin_sidebar_plugins', - html=( - '' - 'sync_alt' - 'Beta推送' - '' - ), - order=10, - ), - ] - - -def is_beta_db_configured(): - return bool(os.environ.get('BETA_DB_NAME', '')) diff --git a/plugins/beta_push/apps.py b/plugins/beta_push/apps.py deleted file mode 100644 index 6c3389c..0000000 --- a/plugins/beta_push/apps.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import logging - -from django.apps import AppConfig -from django.conf import settings - -logger = logging.getLogger(__name__) - - -class BetaPushConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'plugins.beta_push' - verbose_name = 'Beta数据推送' - - def ready(self): - self._configure_beta_database() - - def _configure_beta_database(self): - beta_db_name = os.environ.get('BETA_DB_NAME', '') - if not beta_db_name: - return - - if 'beta' in settings.DATABASES: - return - - default_db = settings.DATABASES.get('default', {}) - engine = default_db.get('ENGINE', '') - - if engine != 'django.db.backends.postgresql': - logger.warning( - 'Beta推送插件仅支持PostgreSQL架构,' - '当前默认数据库引擎不是PostgreSQL' - ) - return - - beta_db = { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': beta_db_name, - 'USER': os.environ.get('BETA_DB_USER', default_db.get('USER', '')), - 'PASSWORD': os.environ.get('BETA_DB_PASSWORD', default_db.get('PASSWORD', '')), - 'HOST': os.environ.get('BETA_DB_HOST', default_db.get('HOST', '127.0.0.1')), - 'PORT': os.environ.get('BETA_DB_PORT', default_db.get('PORT', '5432')), - 'CONN_MAX_AGE': int(os.environ.get('BETA_DB_CONN_MAX_AGE', '60')), - } - - settings.DATABASES['beta'] = beta_db - logger.info(f'Beta数据库已配置: {beta_db_name}') diff --git a/plugins/beta_push/migrations/0001_initial.py b/plugins/beta_push/migrations/0001_initial.py deleted file mode 100644 index 0fd1d77..0000000 --- a/plugins/beta_push/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-15 15:48 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='SyncLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('pending', '等待中'), ('running', '执行中'), ('success', '成功'), ('failed', '失败')], default='pending', max_length=20, verbose_name='状态')), - ('task_id', models.CharField(blank=True, max_length=255, verbose_name='Celery任务ID')), - ('records_pushed', models.IntegerField(default=0, verbose_name='推送记录数')), - ('records_skipped', models.IntegerField(default=0, verbose_name='跳过记录数')), - ('records_failed', models.IntegerField(default=0, verbose_name='失败记录数')), - ('error_message', models.TextField(blank=True, verbose_name='错误信息')), - ('started_at', models.DateTimeField(blank=True, null=True, verbose_name='开始时间')), - ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='beta_push_logs', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': 'Beta推送日志', - 'verbose_name_plural': 'Beta推送日志', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['user'], name='beta_push_s_user_id_29a2ce_idx'), models.Index(fields=['status'], name='beta_push_s_status_7d5027_idx')], - }, - ), - ] diff --git a/plugins/beta_push/migrations/__init__.py b/plugins/beta_push/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/beta_push/models.py b/plugins/beta_push/models.py deleted file mode 100644 index fb99ea0..0000000 --- a/plugins/beta_push/models.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.db import models -from django.conf import settings - - -class SyncLog(models.Model): - STATUS_CHOICES = [ - ('pending', '等待中'), - ('running', '执行中'), - ('success', '成功'), - ('failed', '失败'), - ] - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='beta_push_logs', - verbose_name='用户', - ) - status = models.CharField( - max_length=20, - choices=STATUS_CHOICES, - default='pending', - verbose_name='状态', - ) - task_id = models.CharField( - max_length=255, - blank=True, - verbose_name='Celery任务ID', - ) - records_pushed = models.IntegerField( - default=0, - verbose_name='推送记录数', - ) - records_skipped = models.IntegerField( - default=0, - verbose_name='跳过记录数', - ) - records_failed = models.IntegerField( - default=0, - verbose_name='失败记录数', - ) - error_message = models.TextField( - blank=True, - verbose_name='错误信息', - ) - started_at = models.DateTimeField( - null=True, - blank=True, - verbose_name='开始时间', - ) - completed_at = models.DateTimeField( - null=True, - blank=True, - verbose_name='完成时间', - ) - created_at = models.DateTimeField( - auto_now_add=True, - verbose_name='创建时间', - ) - - class Meta: - verbose_name = 'Beta推送日志' - verbose_name_plural = 'Beta推送日志' - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['user']), - models.Index(fields=['status']), - ] - - def __str__(self): - return f'Beta推送({self.user.username}) - {self.get_status_display()}' diff --git a/plugins/beta_push/services.py b/plugins/beta_push/services.py deleted file mode 100644 index 28db5ee..0000000 --- a/plugins/beta_push/services.py +++ /dev/null @@ -1,554 +0,0 @@ -import logging -import os -from collections import OrderedDict - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.db import connections, models, IntegrityError -from django.utils import timezone - -import redis as redis_lib - -User = get_user_model() -logger = logging.getLogger(__name__) - -BETA_DB = "beta" - -REDIS_KEY_PREFIX = "beta_push:progress" - -ENCRYPTED_FIELDS = { - "hosts.Host._password", - "operations.CloudComputerUser._initial_password", -} - - -def _get_redis(): - url = getattr(settings, "REDIS_URL", "") - if not url: - return None - try: - client = redis_lib.Redis.from_url(url, socket_connect_timeout=3) - client.ping() - return client - except Exception: - return None - - -def set_progress(task_id, current, total, message=""): - r = _get_redis() - if not r: - return - import json - - r.setex( - f"{REDIS_KEY_PREFIX}:{task_id}", - 3600, - json.dumps( - { - "current": current, - "total": total, - "message": message, - } - ), - ) - - -def get_progress(task_id): - r = _get_redis() - if not r: - return None - import json - - data = r.get(f"{REDIS_KEY_PREFIX}:{task_id}") - if data: - return json.loads(data) - return None - - -def _get_fernet(secret_key): - try: - from cryptography.fernet import Fernet - import base64 - import hashlib - - key = hashlib.sha256(secret_key.encode()).digest() - return Fernet(base64.urlsafe_b64encode(key)) - except ImportError: - return None - - -def _re_encrypt_value(encrypted_value, model_label, field_name): - beta_secret_key = os.environ.get("BETA_SECRET_KEY", "") - if not beta_secret_key or not encrypted_value: - return encrypted_value - - field_key = f"{model_label}.{field_name}" - if field_key not in ENCRYPTED_FIELDS: - return encrypted_value - - prod_fernet = _get_fernet(settings.SECRET_KEY) - beta_fernet = _get_fernet(beta_secret_key) - if not prod_fernet or not beta_fernet: - return encrypted_value - - try: - plaintext = prod_fernet.decrypt(encrypted_value.encode()).decode() - return beta_fernet.encrypt(plaintext.encode()).decode() - except Exception as e: - logger.warning(f"重加密失败 [{field_key}]: {e}") - return encrypted_value - - -class BetaPushService: - _beta_schema_cache = {} - - def __init__(self, user_id, task_id="", site_group_id=None): - self.user_id = user_id - self.user = User.objects.get(pk=user_id) - self.task_id = task_id - self.site_group_id = site_group_id - self.site_group = self._resolve_site_group() - self.stats = { - "pushed": 0, - "skipped": 0, - "failed": 0, - "errors": [], - } - self._synced_pks = {} - self._missing_tables = set() - self.last_sync_at = self._get_last_sync_at() - - def _resolve_site_group(self): - if not self.site_group_id: - return None - try: - from apps.dashboard.models import SiteGroup - - return SiteGroup.objects.get(pk=self.site_group_id) - except Exception: - return None - - def _get_last_sync_at(self): - from .models import SyncLog - - try: - log = SyncLog.objects.filter( - user_id=self.user_id, - status="success", - ).latest("completed_at") - return log.completed_at - except SyncLog.DoesNotExist: - return None - - def push_all(self): - self.__class__._beta_schema_cache.clear() - - steps = [ - ("用户信息", self._push_user), - ("用户资料", self._push_user_profile), - ("用户组", self._push_user_groups), - ("主机", self._push_hosts), - ("主机组", self._push_host_groups), - ("产品组", self._push_product_groups), - ("产品", self._push_products), - ("云电脑用户", self._push_cloud_computer_users), - ("开户申请", self._push_account_opening_requests), - ("邀请令牌", self._push_invitation_tokens), - ("授权记录", self._push_access_grants), - ("域名路由", self._push_rdp_domain_routes), - ] - total_steps = len(steps) - for idx, (label, step_func) in enumerate(steps, 1): - if self.task_id: - set_progress(self.task_id, idx, total_steps, label) - try: - step_func() - except Exception as e: - logger.error(f"Beta推送步骤失败 [{label}]: {e}", exc_info=True) - self.stats["errors"].append(f"{label}: {str(e)}") - - if self.task_id: - set_progress(self.task_id, total_steps, total_steps, "完成") - - return self.stats - - def _is_changed(self, instance): - if self.last_sync_at is None: - return True - updated_at = getattr(instance, "updated_at", None) - if updated_at and updated_at > self.last_sync_at: - return True - created_at = getattr(instance, "created_at", None) - if created_at and created_at > self.last_sync_at: - return True - return False - - def _get_beta_table_info(self, model): - table_name = model._meta.db_table - if table_name in self._beta_schema_cache: - return self._beta_schema_cache[table_name] - - info = {"exists": False, "columns": set(), "not_null_no_default": set()} - - try: - with connections[BETA_DB].cursor() as cursor: - cursor.execute( - "SELECT column_name, is_nullable, column_default " - "FROM information_schema.columns " - "WHERE table_schema = 'public' AND table_name = %s", - [table_name], - ) - rows = cursor.fetchall() - - if rows: - info["exists"] = True - for col_name, is_nullable, col_default in rows: - info["columns"].add(col_name) - if is_nullable == "NO" and col_default is None: - info["not_null_no_default"].add(col_name) - except Exception as e: - logger.error(f"查询Beta数据库表结构失败 [{table_name}]: {e}") - - self._beta_schema_cache[table_name] = info - return info - - def _sync_instance(self, instance): - model = instance.__class__ - model_label = f"{model._meta.app_label}.{model.__name__}" - pk = instance.pk - - if model_label in self._synced_pks and pk in self._synced_pks[model_label]: - self.stats["skipped"] += 1 - return True - - table_info = self._get_beta_table_info(model) - if not table_info["exists"]: - if model._meta.db_table not in self._missing_tables: - self.stats["errors"].append( - f"{model.__name__}: Beta数据库中表 {model._meta.db_table} 不存在,已跳过" - ) - self._missing_tables.add(model._meta.db_table) - self._synced_pks.setdefault(model_label, set()).add(pk) - self.stats["skipped"] += 1 - return True - - if not self._is_changed(instance): - if model.objects.using(BETA_DB).filter(pk=pk).exists(): - self._synced_pks.setdefault(model_label, set()).add(pk) - self.stats["skipped"] += 1 - return True - - beta_columns = table_info["columns"] - field_values = {} - m2m_values = OrderedDict() - - for field in model._meta.get_fields(): - if field.many_to_many: - if field.auto_created: - continue - m2m_values[field.name] = list( - getattr(instance, field.name).values_list("pk", flat=True) - ) - continue - - if field.auto_created and not field.concrete: - continue - - if not hasattr(instance, field.attname): - continue - - if not hasattr(field, "column") or field.column not in beta_columns: - continue - - value = getattr(instance, field.attname) - - if isinstance(field, models.ForeignKey): - if value is not None: - try: - related_obj = getattr(instance, field.name) - if related_obj is not None: - self._ensure_stub_exists(related_obj) - except Exception: - pass - - value = _re_encrypt_value(value, model_label, field.name) - - field_values[field.name] = value - - try: - obj, created = model.objects.using(BETA_DB).update_or_create( - pk=pk, - defaults=field_values, - ) - except IntegrityError as e: - logger.warning(f"IntegrityError [{model.__name__}:{pk}]: {e}") - if model.objects.using(BETA_DB).filter(pk=pk).exists(): - try: - model.objects.using(BETA_DB).filter(pk=pk).update(**field_values) - obj = model.objects.using(BETA_DB).get(pk=pk) - except Exception as e2: - self.stats["failed"] += 1 - self.stats["errors"].append( - f"{model.__name__}:{pk} - 更新失败: {str(e2)}" - ) - return False - else: - prod_cols = { - f.column - for f in model._meta.concrete_fields - if hasattr(f, "column") - } - missing = table_info["not_null_no_default"] - prod_cols - hint = ( - f"Beta数据库存在额外NOT NULL无默认值列: {missing}" - if missing - else str(e) - ) - self.stats["failed"] += 1 - self.stats["errors"].append(f"{model.__name__}:{pk} - 创建失败: {hint}") - return False - - for field_name, related_pks in m2m_values.items(): - try: - m2m_field = model._meta.get_field(field_name) - m2m_through_info = self._get_beta_table_info( - m2m_field.remote_field.through - ) - if not m2m_through_info["exists"]: - logger.warning( - f"M2M中间表不存在于Beta数据库 [{model.__name__}.{field_name}]" - ) - continue - - m2m_model = m2m_field.related_model - existing_pks = set( - m2m_model.objects.using(BETA_DB) - .filter(pk__in=related_pks) - .values_list("pk", flat=True) - ) - m2m_manager = getattr(obj, field_name) - m2m_manager.set(existing_pks) - except Exception as e: - logger.warning(f"M2M同步失败 [{model.__name__}.{field_name}]: {e}") - - self._synced_pks.setdefault(model_label, set()).add(pk) - self.stats["pushed"] += 1 - return True - - def _ensure_stub_exists(self, related_instance): - model = related_instance.__class__ - model_label = f"{model._meta.app_label}.{model.__name__}" - pk = related_instance.pk - - if model._meta.db_table in self._missing_tables: - return - - if model_label in self._synced_pks and pk in self._synced_pks[model_label]: - return - - if model.objects.using(BETA_DB).filter(pk=pk).exists(): - self._synced_pks.setdefault(model_label, set()).add(pk) - return - - self._sync_instance(related_instance) - - def _push_user(self): - self._sync_instance(self.user) - - def _push_user_profile(self): - try: - profile = self.user.profile - self._sync_instance(profile) - except Exception: - pass - - def _push_user_groups(self): - for group in self.user.groups.all(): - try: - if ( - not group.__class__.objects.using(BETA_DB) - .filter(pk=group.pk) - .exists() - ): - self._sync_instance(group) - try: - gp = group.profile - self._sync_instance(gp) - except Exception: - pass - except Exception: - pass - - def _get_provider_hosts(self): - from apps.hosts.models import Host - from django.db.models import Q - - if self.user.is_superuser: - return Host.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return Host.objects.filter(site_group=self.site_group) - return Host.objects.filter(providers=self.user) - - def _push_hosts(self): - from apps.hosts.models import Host - - hosts = self._get_provider_hosts() - for host in hosts: - self._sync_instance(host) - - def _get_provider_host_groups(self): - from apps.hosts.models import HostGroup - from django.db.models import Q - - if self.user.is_superuser: - return HostGroup.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return HostGroup.objects.filter(site_group=self.site_group) - return HostGroup.objects.filter(providers=self.user) - - def _push_host_groups(self): - from apps.hosts.models import HostGroup - - host_groups = self._get_provider_host_groups() - for hg in host_groups: - self._sync_instance(hg) - - def _get_provider_product_groups(self): - from apps.operations.models import ProductGroup - from django.db.models import Q - - if self.user.is_superuser: - return ProductGroup.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return ProductGroup.objects.filter(site_group=self.site_group) - return ProductGroup.objects.filter(created_by=self.user) - - def _push_product_groups(self): - from apps.operations.models import ProductGroup - - product_groups = self._get_provider_product_groups() - for pg in product_groups: - self._sync_instance(pg) - - def _get_provider_products(self): - from apps.operations.models import Product - from django.db.models import Q - - if self.user.is_superuser: - return Product.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return Product.objects.filter(site_group=self.site_group) - return Product.objects.filter(created_by=self.user) - - def _push_products(self): - from apps.operations.models import Product - - products = self._get_provider_products() - for product in products: - self._sync_instance(product) - - def _get_provider_cloud_users(self): - from apps.operations.models import CloudComputerUser, Product - from django.db.models import Q - - if self.user.is_superuser: - return CloudComputerUser.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return CloudComputerUser.objects.filter( - product__site_group=self.site_group - ) - provider_product_ids = Product.objects.filter(created_by=self.user).values_list( - "pk", flat=True - ) - return CloudComputerUser.objects.filter(product_id__in=provider_product_ids) - - def _push_cloud_computer_users(self): - from apps.operations.models import CloudComputerUser - - cloud_users = self._get_provider_cloud_users() - for cu in cloud_users: - self._sync_instance(cu) - - def _get_provider_requests(self): - from apps.operations.models import AccountOpeningRequest, Product - from django.db.models import Q - - if self.user.is_superuser: - return AccountOpeningRequest.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return AccountOpeningRequest.objects.filter( - target_product__site_group=self.site_group - ) - provider_product_ids = Product.objects.filter(created_by=self.user).values_list( - "pk", flat=True - ) - return AccountOpeningRequest.objects.filter( - target_product_id__in=provider_product_ids - ) - - def _push_account_opening_requests(self): - from apps.operations.models import AccountOpeningRequest - - requests = self._get_provider_requests() - for req in requests: - self._sync_instance(req) - - def _get_provider_invitation_tokens(self): - from apps.operations.models import ProductInvitationToken, Product - - if self.user.is_superuser: - return ProductInvitationToken.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return ProductInvitationToken.objects.filter( - product__in=Product.objects.filter(site_group=self.site_group) - ) - return ProductInvitationToken.objects.filter(created_by=self.user) - - def _push_invitation_tokens(self): - from apps.operations.models import ProductInvitationToken - - tokens = self._get_provider_invitation_tokens() - for token in tokens: - self._sync_instance(token) - - def _get_provider_access_grants(self): - from apps.operations.models import ProductAccessGrant, Product - from django.db.models import Q - - if self.user.is_superuser: - return ProductAccessGrant.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return ProductAccessGrant.objects.filter( - product__site_group=self.site_group - ) - provider_product_ids = Product.objects.filter(created_by=self.user).values_list( - "pk", flat=True - ) - return ProductAccessGrant.objects.filter(product_id__in=provider_product_ids) - - def _push_access_grants(self): - from apps.operations.models import ProductAccessGrant - - grants = self._get_provider_access_grants() - for grant in grants: - self._sync_instance(grant) - - def _get_provider_rdp_routes(self): - from apps.operations.models import RdpDomainRoute, Product - from django.db.models import Q - - if self.user.is_superuser: - return RdpDomainRoute.objects.all() - if self.site_group and self.user.is_site_group_admin(self.site_group): - return RdpDomainRoute.objects.filter(product__site_group=self.site_group) - provider_product_ids = Product.objects.filter(created_by=self.user).values_list( - "pk", flat=True - ) - return RdpDomainRoute.objects.filter(product_id__in=provider_product_ids) - - def _push_rdp_domain_routes(self): - from apps.operations.models import RdpDomainRoute - - routes = self._get_provider_rdp_routes() - for route in routes: - self._sync_instance(route) diff --git a/plugins/beta_push/tasks.py b/plugins/beta_push/tasks.py deleted file mode 100644 index 94464a2..0000000 --- a/plugins/beta_push/tasks.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging - -from celery import shared_task -from django.utils import timezone - -logger = logging.getLogger(__name__) - - -@shared_task(bind=True, max_retries=1, default_retry_delay=10) -def push_to_beta(self, user_id, sync_log_id, site_group_id=None): - from .models import SyncLog - from .services import BetaPushService - - try: - sync_log = SyncLog.objects.get(pk=sync_log_id) - except SyncLog.DoesNotExist: - logger.error(f"SyncLog {sync_log_id} 不存在") - return - - sync_log.status = "running" - sync_log.started_at = timezone.now() - sync_log.task_id = self.request.id - sync_log.save(update_fields=["status", "started_at", "task_id"]) - - try: - service = BetaPushService( - user_id=user_id, - task_id=self.request.id, - site_group_id=site_group_id, - ) - stats = service.push_all() - - sync_log.status = "success" - sync_log.records_pushed = stats["pushed"] - sync_log.records_skipped = stats["skipped"] - sync_log.records_failed = stats["failed"] - if stats["errors"]: - sync_log.error_message = "\n".join(stats["errors"][:20]) - sync_log.completed_at = timezone.now() - sync_log.save() - - logger.info( - f"Beta推送完成: user={user_id}, " - f'pushed={stats["pushed"]}, ' - f'skipped={stats["skipped"]}, ' - f'failed={stats["failed"]}' - ) - - except Exception as e: - logger.error(f"Beta推送失败: user={user_id}, error={e}", exc_info=True) - sync_log.status = "failed" - sync_log.error_message = str(e)[:2000] - sync_log.completed_at = timezone.now() - sync_log.save() - raise self.retry(exc=e) diff --git a/plugins/beta_push/templates/beta_push/dashboard.html b/plugins/beta_push/templates/beta_push/dashboard.html deleted file mode 100644 index c4853a3..0000000 --- a/plugins/beta_push/templates/beta_push/dashboard.html +++ /dev/null @@ -1,293 +0,0 @@ -{% extends "admin_base/base.html" %} -{% load static %} - -{% block title %}2c2a 提供商后台 - Beta数据推送{% endblock %} - -{% block breadcrumb %} -首页 -chevron_right -Beta数据推送 -{% endblock %} - -{% block content %} -
- -
-

Beta数据推送

-

将您的生产环境数据异步推送到Beta版本数据库,仅推送变更数据

-
- - {% if not beta_configured %} -
-
- warning -
-

Beta数据库未配置

-

请在环境变量中配置以下参数后重启服务:

-
-

BETA_DB_NAME=zasca_beta

-

BETA_DB_USER=postgres

-

BETA_DB_PASSWORD=your_password

-

BETA_DB_HOST=127.0.0.1

-

BETA_DB_PORT=5432

-
-
-
-
- {% else %} - -
- -
-
-
- sync -
-
-

上次成功推送

-

- {% if last_success %} - {{ last_success.completed_at|date:"Y-m-d H:i" }} - {% else %} - 尚未推送 - {% endif %} -

-
-
- {% if last_success %} -
- 推送 {{ last_success.records_pushed }} - 跳过 {{ last_success.records_skipped }} - 失败 {{ last_success.records_failed }} -
- {% endif %} -
- -
-
-
- dns -
-
-

推送范围

-

当前用户关联数据

-
-
-

主机、产品、云电脑用户、开户申请、邀请令牌、授权记录、域名路由

-
- -
-
-
- update -
-
-

同步模式

-

增量同步

-
-
-

仅推送自上次同步以来变更的数据,已有数据自动跳过

-
-
- -
-
-
-

执行推送

-

点击按钮将数据推送到Beta数据库

-
- -
- -
-
- 准备中... - 0% -
-
-
-
-
-
- -
-

推送历史

- -
- history -

暂无推送记录

-
- -
- -
-
- - {% endif %} - -
- - -{% endblock %} diff --git a/plugins/beta_push/urls.py b/plugins/beta_push/urls.py deleted file mode 100644 index 9f85e38..0000000 --- a/plugins/beta_push/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'beta_push' - -urlpatterns = [ - path('', views.dashboard, name='dashboard'), - path('push/', views.start_push, name='start_push'), - path('status/', views.push_status, name='push_status'), -] diff --git a/plugins/beta_push/views.py b/plugins/beta_push/views.py deleted file mode 100644 index b07d170..0000000 --- a/plugins/beta_push/views.py +++ /dev/null @@ -1,173 +0,0 @@ -import logging - -from django.contrib.auth import get_user_model -from django.contrib import messages -from django.http import JsonResponse -from django.shortcuts import redirect, render -from django.views.decorators.http import require_POST -from django.utils import timezone - -from apps.accounts.provider_decorators import is_provider, is_site_group_admin - -from .models import SyncLog -from .services import get_progress - -User = get_user_model() -logger = logging.getLogger(__name__) - - -def _check_permission(user, site_group=None): - if user.is_superuser: - return True - if is_site_group_admin(user, site_group): - return True - return is_provider(user) - - -def dashboard(request): - site_group = getattr(request, "site_group", None) - if not _check_permission(request.user, site_group): - from django.http import HttpResponseForbidden - - return HttpResponseForbidden("仅主机提供商及以上权限用户可使用此功能") - - from . import is_beta_db_configured - from django.conf import settings - - beta_configured = is_beta_db_configured() and "beta" in settings.DATABASES - - sync_logs = SyncLog.objects.filter(user=request.user).order_by("-created_at")[:10] - - is_running = sync_logs.filter(status="running").exists() if sync_logs else False - - last_success = None - for log in sync_logs: - if log.status == "success": - last_success = log - break - - running_task_id = "" - if is_running: - running_log = sync_logs.filter(status="running").first() - running_task_id = running_log.task_id if running_log else "" - - context = { - "page_title": "Beta数据推送", - "active_nav": "beta_push", - "beta_configured": beta_configured, - "sync_logs": sync_logs, - "is_running": is_running, - "last_success": last_success, - "running_task_id": running_task_id, - } - - return render(request, "beta_push/dashboard.html", context) - - -@require_POST -def start_push(request): - site_group = getattr(request, "site_group", None) - if not _check_permission(request.user, site_group): - return JsonResponse({"success": False, "error": "权限不足"}, status=403) - - from . import is_beta_db_configured - from django.conf import settings - - if not is_beta_db_configured() or "beta" not in settings.DATABASES: - return JsonResponse({"success": False, "error": "Beta数据库未配置"}, status=400) - - if SyncLog.objects.filter(user=request.user, status="running").exists(): - return JsonResponse( - {"success": False, "error": "已有推送任务正在执行"}, status=409 - ) - - sync_log = SyncLog.objects.create( - user=request.user, - status="pending", - ) - - try: - from .tasks import push_to_beta - - result = push_to_beta.delay( - request.user.pk, - sync_log.pk, - site_group_id=site_group.pk if site_group else None, - ) - sync_log.task_id = result.id - sync_log.save(update_fields=["task_id"]) - - return JsonResponse( - { - "success": True, - "task_id": result.id, - "sync_log_id": sync_log.pk, - } - ) - except Exception as e: - logger.error(f"启动Beta推送任务失败: {e}", exc_info=True) - sync_log.status = "failed" - sync_log.error_message = str(e) - sync_log.save() - return JsonResponse({"success": False, "error": "任务启动失败"}, status=500) - - -def push_status(request): - site_group = getattr(request, "site_group", None) - if not _check_permission(request.user, site_group): - return JsonResponse({"success": False, "error": "权限不足"}, status=403) - - task_id = request.GET.get("task_id", "") - if not task_id: - latest_log = ( - SyncLog.objects.filter(user=request.user).order_by("-created_at").first() - ) - if not latest_log: - return JsonResponse({"success": True, "status": "none"}) - return JsonResponse( - { - "success": True, - "status": latest_log.status, - "records_pushed": latest_log.records_pushed, - "records_skipped": latest_log.records_skipped, - "records_failed": latest_log.records_failed, - "error_message": latest_log.error_message, - "completed_at": ( - latest_log.completed_at.isoformat() - if latest_log.completed_at - else None - ), - } - ) - - progress = get_progress(task_id) - - sync_log = ( - SyncLog.objects.filter( - user=request.user, - task_id=task_id, - ) - .order_by("-created_at") - .first() - ) - - response_data = { - "success": True, - "progress": progress, - } - - if sync_log: - response_data.update( - { - "status": sync_log.status, - "records_pushed": sync_log.records_pushed, - "records_skipped": sync_log.records_skipped, - "records_failed": sync_log.records_failed, - "error_message": sync_log.error_message, - "completed_at": ( - sync_log.completed_at.isoformat() if sync_log.completed_at else None - ), - } - ) - - return JsonResponse(response_data) diff --git a/plugins/core/__init__.py b/plugins/core/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/plugins/core/base.py b/plugins/core/base.py deleted file mode 100755 index 212b393..0000000 --- a/plugins/core/base.py +++ /dev/null @@ -1,231 +0,0 @@ -import abc -import logging -from typing import Any, Dict, List, Optional, Type - -from django.utils.safestring import mark_safe - -logger = logging.getLogger(__name__) - - -class PluginInterface(abc.ABC): - def __init__(self, plugin_id: str, name: str, version: str, description: str = ""): - self.plugin_id = plugin_id - self.name = name - self.version = version - self.description = description - self.enabled = True - - @property - def metadata(self) -> Dict[str, Any]: - return { - 'id': self.plugin_id, - 'name': self.name, - 'version': self.version, - 'description': self.description, - 'enabled': self.enabled - } - - @abc.abstractmethod - def initialize(self) -> bool: - pass - - @abc.abstractmethod - def shutdown(self) -> bool: - pass - - -class ServiceProvider(abc.ABC): - """ - 服务提供者接口 - 插件可实现此接口以向系统注册可发现的服务 - """ - - @abc.abstractmethod - def get_service_name(self) -> str: - pass - - @abc.abstractmethod - def get_service_interface(self) -> Type: - pass - - def get_service(self) -> Any: - return self - - -class ServiceRegistry: - """ - 服务注册表 - 管理插件提供的服务实例,支持按服务名称和接口类型查找 - """ - - def __init__(self): - self._services: Dict[str, Any] = {} - self._interfaces: Dict[Type, List[str]] = {} - - def register(self, provider: ServiceProvider) -> None: - name = provider.get_service_name() - interface = provider.get_service_interface() - service = provider.get_service() - - self._services[name] = service - - if interface not in self._interfaces: - self._interfaces[interface] = [] - if name not in self._interfaces[interface]: - self._interfaces[interface].append(name) - - logger.info(f"Service registered: {name} (interface: {interface.__name__})") - - def unregister(self, service_name: str) -> None: - if service_name in self._services: - del self._services[service_name] - for interface, names in self._interfaces.items(): - if service_name in names: - names.remove(service_name) - logger.info(f"Service unregistered: {service_name}") - - def get(self, service_name: str) -> Optional[Any]: - return self._services.get(service_name) - - def get_by_interface(self, interface: Type) -> List[Any]: - names = self._interfaces.get(interface, []) - return [self._services[n] for n in names if n in self._services] - - def list_services(self) -> Dict[str, Any]: - return dict(self._services) - - -class HookInterface(abc.ABC): - @abc.abstractmethod - def execute(self, *args, **kwargs) -> Any: - pass - - -class EventHook(HookInterface): - def __init__(self, name: str): - self.name = name - self.handlers: List[callable] = [] - - def register(self, handler: callable): - if handler not in self.handlers: - self.handlers.append(handler) - - def unregister(self, handler: callable): - if handler in self.handlers: - self.handlers.remove(handler) - - def execute(self, *args, **kwargs) -> List[Any]: - results = [] - for handler in self.handlers: - try: - result = handler(*args, **kwargs) - results.append(result) - except Exception as e: - logger.error( - f"Error executing handler " - f"{handler.__name__}: {str(e)}" - ) - results.append(None) - return results - - -class UIExtension: - """ - UI 扩展点描述对象 - - 插件通过此对象声明要在某个页面位置注入的 - HTML 片段、模板路径或表单字段。 - """ - - FORM_FIELD = 'form_field' - SECTION = 'section' - NAV_ITEM = 'nav_item' - TEMPLATE = 'template' - HTML = 'html' - - def __init__( - self, - extension_type: str, - slot: str, - label: str = '', - html: str = '', - template_name: str = '', - field_name: str = '', - field_config: Optional[Dict[str, Any]] = None, - order: int = 0, - context_callback=None, - ): - self.extension_type = extension_type - self.slot = slot - self.label = label - self.html = html - self.template_name = template_name - self.field_name = field_name - self.field_config = field_config or {} - self.order = order - self.context_callback = context_callback - - def render(self, request=None) -> str: - if self.html: - return mark_safe(self.html) - - if self.template_name: - render_ctx = {} - if self.context_callback: - extra = self.context_callback() - if extra: - render_ctx.update(extra) - from django.template.loader import ( - render_to_string, - ) - return mark_safe( - render_to_string( - self.template_name, render_ctx, - request=request, - ) - ) - - return '' - - -class UIExtensionProvider(abc.ABC): - """ - UI 扩展提供者接口 - - 插件实现此接口以向前端页面注入扩展内容。 - 扩展点(slot)由核心系统在各页面模板中预定义, - 插件只需声明自己要注入到哪个 slot 即可。 - """ - - @abc.abstractmethod - def get_ui_extensions(self) -> List[UIExtension]: - pass - - -class URLProvider(abc.ABC): - """ - URL 提供者接口 - - 插件实现此接口以向系统注册 URL 路由。 - 系统在启动时收集所有插件的 URL 模式, - 并动态 include 到对应命名空间下。 - """ - - ADMIN = 'admin' - PROVIDER = 'provider' - PUBLIC = 'public' - - @abc.abstractmethod - def get_url_patterns(self) -> List[dict]: - """ - 返回 URL 模式列表。 - - 每个元素为 dict: - { - 'prefix': 'qq/', # URL 前缀 - 'module': 'plugins.qq_verification.urls_admin', - 'namespace': 'admin_plugins', - 'section': URLProvider.ADMIN, - } - """ - pass diff --git a/plugins/core/plugin_manager.py b/plugins/core/plugin_manager.py deleted file mode 100755 index a431e63..0000000 --- a/plugins/core/plugin_manager.py +++ /dev/null @@ -1,220 +0,0 @@ -import os -import sys -import importlib -import inspect -import logging -from typing import Dict, List, Type, Any, Optional, Set -from pathlib import Path -from django.conf import settings -from .base import ( - PluginInterface, - EventHook, - ServiceProvider, - ServiceRegistry, - UIExtension, - UIExtensionProvider, - URLProvider, -) - -logger = logging.getLogger(__name__) - - -class PluginManager: - def __init__(self): - self.plugins: Dict[str, PluginInterface] = {} - self.hooks: Dict[str, EventHook] = {} - self.loaded_modules: Set[str] = set() - self.service_registry = ServiceRegistry() - self._ui_extensions: Dict[ - str, List[UIExtension] - ] = {} - - def discover_builtin_plugins(self) -> Dict[str, dict]: - from ..available_plugins import ALL_AVAILABLE_PLUGINS - return ALL_AVAILABLE_PLUGINS - - def load_builtin_plugin(self, plugin_key: str, plugin_info: dict) -> Optional[PluginInterface]: - if not plugin_info.get('enabled', True): - logger.info(f"插件 {plugin_key} 已禁用,跳过加载") - return None - - try: - module_path = plugin_info['module'] - class_name = plugin_info['class'] - - module = importlib.import_module(module_path) - plugin_class = getattr(module, class_name) - - if (inspect.isclass(plugin_class) and - issubclass(plugin_class, PluginInterface) and - plugin_class != PluginInterface): - - plugin_instance = plugin_class() - self.plugins[plugin_instance.plugin_id] = plugin_instance - - if isinstance(plugin_instance, ServiceProvider): - self.service_registry.register(plugin_instance) - - logger.info(f"成功加载插件: {plugin_instance.name} (ID: {plugin_instance.plugin_id})") - return plugin_instance - else: - logger.error(f"模块 {module_path} 中的 {class_name} 不是一个有效的插件类") - return None - - except ImportError as e: - logger.error(f"无法导入插件模块 {plugin_info['module']}: {str(e)}") - return None - except AttributeError as e: - logger.error(f"模块 {plugin_info['module']} 中找不到类 {plugin_info['class']}: {str(e)}") - return None - except Exception as e: - logger.error(f"加载插件 {plugin_key} 时发生错误: {str(e)}") - return None - - def load_all_builtin_plugins(self): - builtin_plugins = self.discover_builtin_plugins() - - for plugin_key, plugin_info in builtin_plugins.items(): - plugin = self.load_builtin_plugin(plugin_key, plugin_info) - if plugin: - try: - if not plugin.initialize(): - logger.warning(f"插件 {plugin.name} 初始化失败") - except Exception as e: - logger.error(f"插件 {plugin.name} 初始化时发生错误: {str(e)}") - - def unload_plugin(self, plugin_id: str) -> bool: - if plugin_id not in self.plugins: - logger.warning(f"插件 {plugin_id} 不存在") - return False - - plugin = self.plugins[plugin_id] - - try: - if isinstance(plugin, ServiceProvider): - self.service_registry.unregister(plugin.get_service_name()) - - if not plugin.shutdown(): - logger.warning(f"插件 {plugin.name} 关闭时返回失败状态") - - del self.plugins[plugin_id] - - logger.info(f"插件 {plugin.name} (ID: {plugin_id}) 已卸载") - return True - except Exception as e: - logger.error(f"卸载插件 {plugin_id} 时发生错误: {str(e)}") - return False - - def get_plugin(self, plugin_id: str) -> Optional[PluginInterface]: - return self.plugins.get(plugin_id) - - def get_all_plugins(self) -> Dict[str, PluginInterface]: - return self.plugins.copy() - - def get_plugin_metadata(self) -> List[Dict[str, Any]]: - metadata_list = [] - for plugin in self.plugins.values(): - metadata_list.append(plugin.metadata) - return metadata_list - - def get_service(self, service_name: str) -> Optional[Any]: - return self.service_registry.get(service_name) - - def get_services_by_interface(self, interface: Type) -> List[Any]: - return self.service_registry.get_by_interface(interface) - - def list_services(self) -> Dict[str, Any]: - return self.service_registry.list_services() - - def register_hook(self, hook_name: str) -> EventHook: - if hook_name not in self.hooks: - self.hooks[hook_name] = EventHook(hook_name) - return self.hooks[hook_name] - - def get_hook(self, hook_name: str) -> Optional[EventHook]: - return self.hooks.get(hook_name) - - def trigger_hook(self, hook_name: str, *args, **kwargs) -> List[Any]: - hook = self.get_hook(hook_name) - if hook: - return hook.execute(*args, **kwargs) - return [] - - def start_all_plugins(self): - for plugin_id, plugin in self.plugins.items(): - try: - if not plugin.initialize(): - logger.warning( - f"插件 {plugin.name} 启动失败" - ) - except Exception as e: - logger.error( - f"启动插件 {plugin.name} " - f"时发生错误: {str(e)}" - ) - - def _collect_ui_extensions(self): - self._ui_extensions.clear() - for plugin in self.plugins.values(): - if isinstance(plugin, UIExtensionProvider): - try: - exts = plugin.get_ui_extensions() - for ext in exts: - slot = ext.slot - if slot not in self._ui_extensions: - self._ui_extensions[slot] = [] - self._ui_extensions[slot].append(ext) - except Exception as e: - logger.error( - f"收集插件 {plugin.name} " - f"UI扩展失败: {str(e)}" - ) - for slot in self._ui_extensions: - self._ui_extensions[slot].sort( - key=lambda e: e.order - ) - - def get_ui_extensions( - self, slot: str - ) -> List[UIExtension]: - if not self._ui_extensions: - self._collect_ui_extensions() - return self._ui_extensions.get(slot, []) - - def get_all_ui_slots(self) -> List[str]: - if not self._ui_extensions: - self._collect_ui_extensions() - return list(self._ui_extensions.keys()) - - def get_plugin_url_patterns( - self, section: str - ) -> List[dict]: - patterns = [] - for plugin in self.plugins.values(): - if isinstance(plugin, URLProvider): - try: - for p in plugin.get_url_patterns(): - if p.get('section') == section: - patterns.append(p) - except Exception as e: - logger.error( - f"收集插件 {plugin.name} " - f"URL失败: {str(e)}" - ) - return patterns - - def stop_all_plugins(self): - for plugin_id in reversed(list(self.plugins.keys())): - plugin = self.plugins[plugin_id] - try: - if not plugin.shutdown(): - logger.warning(f"插件 {plugin.name} 停止时返回失败状态") - except Exception as e: - logger.error(f"停止插件 {plugin.name} 时发生错误: {str(e)}") - - -plugin_manager = PluginManager() - - -def get_plugin_manager() -> PluginManager: - return plugin_manager diff --git a/plugins/django_integration.py b/plugins/django_integration.py deleted file mode 100755 index 3930b69..0000000 --- a/plugins/django_integration.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -Django项目插件系统集成 -展示如何将插件系统集成到Django项目中 -""" - -from django.conf import settings -from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.contrib.auth.decorators import login_required -import json -import logging - -from . import plugin_manager - -logger = logging.getLogger(__name__) - - -def initialize_plugins(): - """ - 初始化项目插件 - 应该在Django应用启动时调用 - """ - # 添加项目插件目录 - plugin_dirs = [ - "./plugins/custom_plugins", # 自定义插件目录 - "./plugins/third_party", # 第三方插件目录 - ] - - # 从设置中读取额外的插件目录 - if hasattr(settings, 'PLUGIN_DIRS'): - plugin_dirs.extend(settings.PLUGIN_DIRS) - - for directory in plugin_dirs: - plugin_manager.add_plugin_directory(directory) - - # 加载所有插件 - loaded_plugins = plugin_manager.load_all_plugins() - print(f"Django项目已加载插件: {loaded_plugins}") - - return loaded_plugins - - -def get_plugin(plugin_id): - """ - 获取插件实例 - :param plugin_id: 插件ID - :return: 插件实例或None - """ - return plugin_manager.get_plugin(plugin_id) - - -def trigger_hook(hook_name, *args, **kwargs): - """ - 触发钩子 - :param hook_name: 钩子名称 - :param args: 参数 - :param kwargs: 关键字参数 - :return: 钩子执行结果 - """ - return plugin_manager.trigger_hook(hook_name, *args, **kwargs) - - -def register_hook(hook_name, handler): - """ - 注册钩子处理器 - :param hook_name: 钩子名称 - :param handler: 处理器函数 - """ - plugin_manager.register_hook(hook_name, handler) - - -def plugin_api_view(request, plugin_id, action): - """ - 插件API视图 - 提供对插件功能的HTTP访问 - """ - plugin = get_plugin(plugin_id) - - if not plugin: - return JsonResponse({ - 'error': f'Plugin {plugin_id} not found', - 'success': False - }, status=404) - - if not plugin.enabled: - return JsonResponse({ - 'error': f'Plugin {plugin_id} is disabled', - 'success': False - }, status=400) - - if request.method == 'POST': - try: - data = json.loads(request.body) - except json.JSONDecodeError: - data = {} - else: - data = request.GET.dict() - - # 根据动作调用插件方法 - if hasattr(plugin, action): - try: - method = getattr(plugin, action) - if callable(method): - result = method(**data) - return JsonResponse({ - 'result': result, - 'success': True - }) - else: - return JsonResponse({ - 'error': f'{action} is not callable', - 'success': False - }, status=400) - except Exception as e: - logger.error("Plugin action execution failed", exc_info=True) - return JsonResponse({ - 'error': 'Internal server error', - 'success': False - }, status=500) - else: - return JsonResponse({ - 'error': f'Action {action} not found in plugin {plugin_id}', - 'success': False - }, status=400) - - -@require_http_methods(["GET", "POST"]) -@login_required -def plugin_management_api(request): - """ - 插件管理API - 用于管理插件的启用/禁用、加载/卸载等 - """ - if request.method == 'GET': - # 返回所有插件信息 - plugins = [] - for plugin in plugin_manager.get_all_plugins(): - plugins.append(plugin.metadata) - return JsonResponse({'plugins': plugins}) - - elif request.method == 'POST': - try: - data = json.loads(request.body) - action = data.get('action') - plugin_id = data.get('plugin_id') - - if action == 'enable': - success = plugin_manager.enable_plugin(plugin_id) - return JsonResponse({'success': success}) - elif action == 'disable': - success = plugin_manager.disable_plugin(plugin_id) - return JsonResponse({'success': success}) - elif action == 'reload': - # 重新加载插件(在实际实现中可能需要更复杂的逻辑) - plugin_manager.unregister_plugin(plugin_id) - # 重新从目录加载 - for directory in plugin_manager.plugin_dirs: - plugin_manager.load_plugins_from_directory(directory) - return JsonResponse({'success': True}) - else: - return JsonResponse({ - 'error': f'Unknown action: {action}', - 'success': False - }, status=400) - - except Exception as e: - logger.error("Plugin management action failed", exc_info=True) - return JsonResponse({ - 'error': 'Internal server error', - 'success': False - }, status=500) - - -class PluginMiddleware: - """ - 插件中间件 - 在请求处理过程中执行插件钩子 - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # 在请求处理前触发钩子 - trigger_hook('before_request', request=request) - - response = self.get_response(request) - - # 在请求处理后触发钩子 - trigger_hook('after_request', request=request, response=response) - - return response - - def process_view(self, request, view_func, view_args, view_kwargs): - """在视图函数调用前触发钩子""" - result = trigger_hook('before_view', - request=request, - view_func=view_func, - view_args=view_args, - view_kwargs=view_kwargs) - # 如果任何插件返回了HttpResponse,则使用它 - for res in result: - if res and hasattr(res, 'status_code'): - return res - return None \ No newline at end of file diff --git a/plugins/dynamic_urls.py b/plugins/dynamic_urls.py deleted file mode 100644 index c7c8b64..0000000 --- a/plugins/dynamic_urls.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.urls import path, include - -from plugins.core.plugin_manager import get_plugin_manager -from plugins.core.base import URLProvider - - -def _build_url_list(section): - pm = get_plugin_manager() - patterns = pm.get_plugin_url_patterns(section) - result = [] - for p in patterns: - namespace = p.get('namespace', '') - if namespace: - result.append( - path( - p['prefix'], - include(p['module'], namespace), - ) - ) - else: - result.append( - path( - p['prefix'], - include(p['module']), - ) - ) - return result - - -def get_plugin_admin_urls(): - return _build_url_list(URLProvider.ADMIN) - - -def get_plugin_provider_urls(): - return _build_url_list(URLProvider.PROVIDER) diff --git a/plugins/forms_admin.py b/plugins/forms_admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/forms_provider.py b/plugins/forms_provider.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/management/commands/plugin.py b/plugins/management/commands/plugin.py deleted file mode 100755 index a4768ec..0000000 --- a/plugins/management/commands/plugin.py +++ /dev/null @@ -1,1482 +0,0 @@ -""" -插件管理命令 -提供类似 pip 的插件管理功能,支持安装、卸载、搜索、登录等操作 -支持从文件安装:将插件目录解压到 plugins/ 目录后,使用 scan 发现并 install 安装 -""" -import os -import sys -import subprocess -import json -import re -import toml -import inspect -import zipfile -import tempfile -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings -from plugins.core.plugin_manager import get_plugin_manager -from plugins.models import PluginRecord -import importlib -import importlib.util -from plugins.available_plugins import ALL_AVAILABLE_PLUGINS -import shutil -import urllib.request -import urllib.error - -PLUGIN_REGISTRY_URL = "https://raw.githubusercontent.com/2c2a/2c2a-plugin-registry/main/plugins.json" -PLUGIN_REGISTRY_RAW_API = "https://api.github.com/repos/2c2a/2c2a-plugin-registry/contents/plugins.json" -PLUGIN_REGISTRY_GITEE_URL = "https://raw.giteeusercontent.com/Bilibili-Supercmd/plugin-registry/raw/main/plugins.json" - - -class Command(BaseCommand): - help = '插件管理命令,类似 pip 的功能' - - def add_arguments(self, parser): - parser.add_argument('action', type=str, help='操作类型: install, upgrade, uninstall, list, info, search, scan, login, enable, disable') - parser.add_argument('plugin_name', nargs='?', type=str, help='插件名称、本地路径或zip文件路径') - parser.add_argument('--source', type=str, help='插件源地址或本地路径') - parser.add_argument('--force', action='store_true', help='强制执行操作') - parser.add_argument('--no-migrate', action='store_true', help='跳过数据库迁移') - parser.add_argument('--debug', action='store_true', help='输出调试信息') - parser.add_argument('--registry', type=str, default=PLUGIN_REGISTRY_URL, help='插件仓库地址') - parser.add_argument('--force-github', action='store_true', help='强制使用 GitHub 插件仓库') - parser.add_argument('--force-gitee', action='store_true', help='强制使用 Gitee 插件仓库镜像') - parser.add_argument('--install-all', action='store_true', help='与 scan 配合使用,安装所有发现的未注册插件') - - def handle(self, *args, **options): - action = options['action'] - plugin_name = options.get('plugin_name') - no_migrate = options.get('no_migrate', False) - self.debug = options.get('debug', False) - - registry_url = options.get('registry') - if options.get('force_gitee'): - registry_url = PLUGIN_REGISTRY_GITEE_URL - elif options.get('force_github'): - registry_url = PLUGIN_REGISTRY_URL - - if action == 'list': - self.list_plugins() - elif action == 'scan': - self.scan_plugins( - install_all=options.get('install_all', False), - no_migrate=no_migrate, - ) - elif action == 'install': - if not plugin_name: - raise CommandError('安装插件需要指定插件名称、路径或zip文件') - self.install_plugin( - plugin_name, - options.get('source'), - options.get('force'), - registry_url, - no_migrate=no_migrate, - ) - elif action == 'upgrade': - if not plugin_name: - raise CommandError('升级插件需要指定插件名称') - self.upgrade_plugin(plugin_name, registry_url) - elif action == 'uninstall': - if not plugin_name: - raise CommandError('卸载插件需要指定插件名称') - self.uninstall_plugin( - plugin_name, - options.get('force'), - no_migrate=no_migrate, - ) - elif action == 'info': - if not plugin_name: - raise CommandError('查看插件信息需要指定插件名称') - self.plugin_info(plugin_name) - elif action == 'search': - keyword = plugin_name or '' - self.search_plugins(keyword, registry_url) - elif action == 'login': - self.login_github() - elif action == 'enable': - if not plugin_name: - raise CommandError('启用插件需要指定插件名称') - self.enable_plugin(plugin_name) - elif action == 'disable': - if not plugin_name: - raise CommandError('禁用插件需要指定插件名称') - self.disable_plugin(plugin_name) - else: - raise CommandError( - f'未知的操作: {action}. ' - f'支持的操作: install, upgrade, uninstall, ' - f'list, info, search, scan, login, enable, disable' - ) - - def _fetch_registry(self, registry_url=None): - url = registry_url or PLUGIN_REGISTRY_URL - urls_to_try = [] - - if url == PLUGIN_REGISTRY_GITEE_URL: - urls_to_try = [PLUGIN_REGISTRY_GITEE_URL, PLUGIN_REGISTRY_URL] - elif url == PLUGIN_REGISTRY_URL: - urls_to_try = [PLUGIN_REGISTRY_URL, PLUGIN_REGISTRY_GITEE_URL] - else: - urls_to_try = [url, PLUGIN_REGISTRY_GITEE_URL, PLUGIN_REGISTRY_URL] - - last_error = None - for try_url in urls_to_try: - try: - req = urllib.request.Request(try_url, headers={'User-Agent': '2c2a-PluginManager/1.0'}) - with urllib.request.urlopen(req, timeout=15) as resp: - data = json.loads(resp.read().decode('utf-8')) - return data.get('plugins', {}) - except Exception as e: - last_error = e - continue - - try: - result = subprocess.run( - ['gh', 'api', '-H', 'Accept: application/vnd.github.v3.raw', - 'repos/2c2a/2c2a-plugin-registry/contents/plugins.json'], - capture_output=True, text=True, timeout=15 - ) - if result.returncode == 0 and result.stdout.strip(): - data = json.loads(result.stdout) - return data.get('plugins', {}) - except FileNotFoundError: - pass - except subprocess.TimeoutExpired: - pass - except json.JSONDecodeError: - pass - except Exception: - pass - - raise CommandError( - '无法访问插件仓库。\n' - '请检查网络连接或使用 "uv run manage.py plugin login" 登录 GitHub CLI 后重试。' - ) - - def search_plugins(self, keyword='', registry_url=None): - remote_plugins = self._fetch_registry(registry_url) - if not remote_plugins: - self.stdout.write('远程插件仓库中没有插件') - return - - results = {} - for plugin_id, info in remote_plugins.items(): - if not keyword: - results[plugin_id] = info - else: - kw = keyword.lower() - if (kw in plugin_id.lower() or - kw in info.get('name', '').lower() or - kw in info.get('description', '').lower()): - results[plugin_id] = info - - if not results: - self.stdout.write(f'未找到与 "{keyword}" 匹配的插件') - return - - self.stdout.write(self.style.SUCCESS(f'找到 {len(results)} 个插件:')) - self.stdout.write('') - for plugin_id, info in results.items(): - self.stdout.write(f' {self.style.SUCCESS(plugin_id)}') - self.stdout.write(f' 名称: {info.get("name", "N/A")}') - self.stdout.write(f' 简介: {info.get("description", "N/A")}') - self.stdout.write(f' 仓库: {info.get("repository", "N/A")}') - self.stdout.write(f' 版本: {info.get("version", "N/A")}') - self.stdout.write('') - - def scan_plugins(self, install_all=False, no_migrate=False): - plugins_base = os.path.join(settings.BASE_DIR, 'plugins') - if not os.path.isdir(plugins_base): - raise CommandError(f'插件目录不存在: {plugins_base}') - - registered_ids = set(ALL_AVAILABLE_PLUGINS.keys()) - db_ids = set( - PluginRecord.objects.values_list('plugin_id', flat=True) - ) - loaded_plugins = get_plugin_manager().get_all_plugins() - loaded_ids = set(loaded_plugins.keys()) - - skip_dirs = {'core', 'management', 'templatetags', 'migrations', - '__pycache__', '.git'} - - discovered = [] - - for entry in sorted(os.listdir(plugins_base)): - entry_path = os.path.join(plugins_base, entry) - if not os.path.isdir(entry_path): - continue - if entry in skip_dirs: - continue - if entry.startswith('.') or entry.startswith('_'): - continue - init_file = os.path.join(entry_path, '__init__.py') - if not os.path.exists(init_file): - continue - - has_plugin_class = self._detect_plugin_class(entry, entry_path) - if not has_plugin_class: - continue - - is_registered = entry in registered_ids - is_in_db = entry in db_ids - is_loaded = entry in loaded_ids - - if is_registered or is_in_db or is_loaded: - continue - - plugin_class, plugin_module_name = self._load_plugin_class_from_package( - entry, entry_path - ) - plugin_name = entry - plugin_version = '0.0.0' - plugin_desc = '' - if plugin_class: - try: - inst = plugin_class() - plugin_name = inst.name - plugin_version = inst.version - plugin_desc = inst.description - except Exception as exc: - self.stderr.write( - f"Warning: failed to read metadata from plugin '{entry}': {exc}" - ) - - discovered.append({ - 'dir_name': entry, - 'path': entry_path, - 'name': plugin_name, - 'version': plugin_version, - 'description': plugin_desc, - 'plugin_class': plugin_class, - 'plugin_module_name': plugin_module_name, - }) - - if not discovered: - self.stdout.write(self.style.SUCCESS('没有发现未注册的插件')) - return - - self.stdout.write(self.style.SUCCESS( - f'发现 {len(discovered)} 个未注册的插件:' - )) - self.stdout.write('') - for info in discovered: - self.stdout.write(f' {self.style.SUCCESS(info["dir_name"])}') - self.stdout.write(f' 名称: {info["name"]}') - self.stdout.write(f' 版本: {info["version"]}') - self.stdout.write(f' 描述: {info["description"]}') - self.stdout.write(f' 路径: {info["path"]}') - self.stdout.write('') - - if install_all: - self.stdout.write('正在安装所有发现的插件...') - for info in discovered: - try: - app_label = self._register_discovered_plugin( - info, no_migrate=no_migrate - ) - self.stdout.write(self.style.SUCCESS( - f' ✓ {info["name"]} 安装完成' - )) - except Exception as e: - self.stdout.write(self.style.ERROR( - f' ✗ {info["dir_name"]} 安装失败: {str(e)}' - )) - else: - self.stdout.write( - '使用 "uv run python manage.py plugin install <目录名>" ' - '安装单个插件\n' - '使用 "uv run python manage.py plugin scan --install-all" ' - '安装所有发现的插件' - ) - - def _detect_plugin_class(self, dir_name, dir_path): - init_file = os.path.join(dir_path, '__init__.py') - if os.path.exists(init_file): - try: - with open(init_file, 'r', encoding='utf-8') as f: - content = f.read() - if re.search( - r'class\s+\w+\s*\([^)]*PluginInterface', - content, re.DOTALL - ): - return True - if 'PLUGIN_INFO' in content: - return True - except Exception as e: - self.stdout.write( - self.style.WARNING( - f'读取插件 {dir_name} 的 __init__.py 失败: {e}' - ) - ) - - for item in os.listdir(dir_path): - if not item.endswith('.py') or item == '__init__.py': - continue - fp = os.path.join(dir_path, item) - try: - with open(fp, 'r', encoding='utf-8') as f: - content = f.read() - if re.search( - r'class\s+\w+\s*\([^)]*PluginInterface', - content, re.DOTALL - ): - return True - except Exception: - continue - - return False - - def _register_discovered_plugin(self, info, no_migrate=False): - plugin_class = info['plugin_class'] - plugin_module_name = info['plugin_module_name'] - dir_name = info['dir_name'] - - if not plugin_class: - plugin_class, plugin_module_name = self._load_plugin_class_from_package( - dir_name, info['path'] - ) - - if not plugin_class: - raise CommandError( - f'在 {info["path"]} 中未找到有效的插件类' - ) - - plugin_instance = plugin_class() - plugin_manager = get_plugin_manager() - plugin_manager.plugins[plugin_instance.plugin_id] = plugin_instance - - try: - plugin_instance.initialize() - except Exception as e: - self.stdout.write( - self.style.WARNING( - f'插件 {plugin_instance.plugin_id} 初始化失败,已继续安装流程: {e}' - ) - ) - - PluginRecord.objects.update_or_create( - plugin_id=plugin_instance.plugin_id, - defaults={ - 'name': plugin_instance.name, - 'version': plugin_instance.version, - 'description': plugin_instance.description, - 'is_active': True, - } - ) - - self.add_plugin_to_toml_config(plugin_instance.plugin_id, { - 'name': plugin_instance.name, - 'module': plugin_module_name, - 'class': plugin_class.__name__, - 'description': plugin_instance.description, - 'version': plugin_instance.version, - 'enabled': True - }) - - app_label = self._get_app_label_from_module(plugin_module_name) - if app_label and not no_migrate: - self._run_migrate(app_label) - - return app_label - - def login_github(self): - self.stdout.write('正在检查 GitHub CLI 认证状态...') - try: - result = subprocess.run( - ['gh', 'auth', 'status'], - capture_output=True, text=True, timeout=10 - ) - if result.returncode == 0: - self.stdout.write(self.style.SUCCESS('GitHub CLI 已认证')) - self.stdout.write(result.stdout.strip()) - return - except FileNotFoundError: - raise CommandError( - '未找到 gh CLI,请先安装 GitHub CLI: https://cli.github.com/' - ) - except subprocess.TimeoutExpired: - raise CommandError('检查认证状态超时') - - self.stdout.write('GitHub CLI 未认证,正在启动登录流程...') - try: - result = subprocess.run( - ['gh', 'auth', 'login'], - stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr - ) - if result.returncode == 0: - self.stdout.write(self.style.SUCCESS('GitHub 登录成功!')) - else: - raise CommandError('GitHub 登录失败') - except FileNotFoundError: - raise CommandError( - '未找到 gh CLI,请先安装 GitHub CLI: https://cli.github.com/' - ) - - def install_plugin(self, plugin_name, source=None, force=False, registry_url=None, no_migrate=False): - self.stdout.write(f'正在安装插件: {plugin_name}') - - app_label = None - - if plugin_name.endswith('.zip'): - if not os.path.exists(plugin_name): - raise CommandError(f'zip 文件不存在: {plugin_name}') - app_label = self.install_from_zip(plugin_name, force=force, no_migrate=no_migrate) - elif os.path.exists(plugin_name) and os.path.isdir(plugin_name): - app_label = self.install_from_path(plugin_name) - else: - plugin_path = os.path.join( - settings.BASE_DIR, 'plugins', plugin_name - ) - if os.path.exists(plugin_path) and os.path.isdir(plugin_path): - app_label = self.install_from_path(plugin_path) - elif plugin_name in ALL_AVAILABLE_PLUGINS: - plugin_info = ALL_AVAILABLE_PLUGINS[plugin_name] - app_label = self.install_builtin_plugin( - plugin_id=plugin_name, plugin_info=plugin_info - ) - else: - found = False - for pid, pinfo in ALL_AVAILABLE_PLUGINS.items(): - if pinfo['name'].lower() == plugin_name.lower(): - app_label = self.install_builtin_plugin( - plugin_id=pid, plugin_info=pinfo - ) - found = True - break - if not found: - if source and source.endswith('.zip') and os.path.exists(source): - app_label = self.install_from_zip(source, force=force, no_migrate=no_migrate) - elif source and os.path.exists(source) and os.path.isdir(source): - app_label = self.install_from_path(source) - else: - app_label = self.install_from_registry( - plugin_name, registry_url, force - ) - - if app_label and not no_migrate: - self._run_migrate(app_label) - - def install_from_zip(self, zip_path, force=False, no_migrate=False): - if not zipfile.is_zipfile(zip_path): - raise CommandError(f'不是有效的 zip 文件: {zip_path}') - - plugins_base = os.path.join(settings.BASE_DIR, 'plugins') - zip_basename = os.path.basename(zip_path) - plugin_dir_name = os.path.splitext(zip_basename)[0] - - with zipfile.ZipFile(zip_path, 'r') as zf: - top_dirs = set() - for name in zf.namelist(): - parts = name.split('/') - if len(parts) > 1 and parts[0]: - top_dirs.add(parts[0]) - - if len(top_dirs) == 1: - single_dir = top_dirs.pop() - if single_dir == plugin_dir_name or single_dir.replace('-', '_') == plugin_dir_name: - plugin_dir_name = single_dir - - if len(top_dirs) == 1: - single_dir = list(top_dirs)[0] if not plugin_dir_name else plugin_dir_name - has_init = any( - n == f'{single_dir}/__init__.py' or n.startswith(f'{single_dir}/') - for n in zf.namelist() - if n.endswith('__init__.py') - ) - if not has_init: - nested = [n for n in zf.namelist() - if n.startswith(f'{single_dir}/') and n.endswith('/')] - if nested: - for sub in nested: - sub_name = sub.rstrip('/').split('/')[-1] - sub_init = f'{single_dir}/{sub_name}/__init__.py' - if sub_init in zf.namelist(): - plugin_dir_name = sub_name - break - - target_dir = os.path.join(plugins_base, plugin_dir_name) - if os.path.exists(target_dir): - if force: - shutil.rmtree(target_dir) - else: - raise CommandError( - f'插件目录已存在: {target_dir}\n' - f'使用 --force 强制重新安装' - ) - - self.stdout.write(f'正在从 zip 文件解压插件: {zip_path}') - - with tempfile.TemporaryDirectory() as tmp_dir: - with zipfile.ZipFile(zip_path, 'r') as zf: - zf.extractall(tmp_dir) - - extracted_items = os.listdir(tmp_dir) - if len(extracted_items) == 1 and os.path.isdir( - os.path.join(tmp_dir, extracted_items[0]) - ): - src_dir = os.path.join(tmp_dir, extracted_items[0]) - inner_items = os.listdir(src_dir) - has_init = '__init__.py' in inner_items - if has_init: - plugin_dir_name = extracted_items[0] - target_dir = os.path.join(plugins_base, plugin_dir_name) - if os.path.exists(target_dir): - if force: - shutil.rmtree(target_dir) - else: - raise CommandError( - f'插件目录已存在: {target_dir}\n' - f'使用 --force 强制重新安装' - ) - shutil.copytree(src_dir, target_dir) - else: - sub_dirs = [ - d for d in inner_items - if os.path.isdir(os.path.join(src_dir, d)) - and os.path.exists(os.path.join(src_dir, d, '__init__.py')) - ] - if len(sub_dirs) == 1: - plugin_dir_name = sub_dirs[0] - target_dir = os.path.join(plugins_base, plugin_dir_name) - if os.path.exists(target_dir): - if force: - shutil.rmtree(target_dir) - else: - raise CommandError( - f'插件目录已存在: {target_dir}\n' - f'使用 --force 强制重新安装' - ) - shutil.copytree( - os.path.join(src_dir, sub_dirs[0]), target_dir - ) - else: - shutil.copytree(src_dir, target_dir) - else: - shutil.copytree(tmp_dir, target_dir) - - self.stdout.write(self.style.SUCCESS( - f'插件已解压到: {target_dir}' - )) - - app_label = self.install_from_path(target_dir) - return app_label - - def install_from_registry(self, plugin_name, registry_url=None, force=False): - remote_plugins = self._fetch_registry(registry_url) - - plugin_info = None - for pid, info in remote_plugins.items(): - if pid == plugin_name or info.get('name', '').lower() == plugin_name.lower(): - plugin_info = info - plugin_name = pid - break - - if not plugin_info: - available = list(remote_plugins.keys()) - raise CommandError( - f'在远程仓库中找不到插件: {plugin_name}\n' - f'可用的远程插件: {available}' - ) - - repository_url = plugin_info.get('repository') - if not repository_url: - raise CommandError(f'插件 {plugin_name} 没有提供仓库地址') - - target_dir = os.path.join(settings.BASE_DIR, 'plugins', plugin_name) - if os.path.exists(target_dir): - if force: - shutil.rmtree(target_dir) - else: - raise CommandError( - f'插件目录已存在: {target_dir}\n' - f'使用 --force 强制重新安装' - ) - - self.stdout.write(f'正在从 {repository_url} 克隆插件...') - - clone_url = repository_url - if repository_url.startswith('https://github.com/'): - clone_url = repository_url.replace('https://github.com/', 'git@github.com:') - - try: - result = subprocess.run( - ['git', 'clone', '--depth', '1', clone_url, target_dir], - capture_output=True, text=True, timeout=120 - ) - if result.returncode != 0: - self.stdout.write('SSH 克隆失败,尝试 HTTPS...') - result = subprocess.run( - ['git', 'clone', '--depth', '1', repository_url, target_dir], - capture_output=True, text=True, timeout=120 - ) - if result.returncode != 0: - raise CommandError(f'克隆插件仓库失败: {result.stderr.strip()}') - except subprocess.TimeoutExpired: - raise CommandError('克隆插件仓库超时') - except FileNotFoundError: - raise CommandError('未找到 git,请先安装 git') - - self.stdout.write(self.style.SUCCESS(f'插件仓库已克隆到: {target_dir}')) - - git_dir = os.path.join(target_dir, '.git') - if os.path.exists(git_dir): - shutil.rmtree(git_dir) - self.stdout.write('已移除 .git 目录') - - plugin_record, created = PluginRecord.objects.update_or_create( - plugin_id=plugin_name, - defaults={ - 'name': plugin_info.get('name', plugin_name), - 'version': plugin_info.get('version', '0.0.0'), - 'description': plugin_info.get('description', ''), - 'is_active': True, - } - ) - if created: - self.stdout.write('已创建插件数据库记录') - else: - self.stdout.write('已更新插件数据库记录') - - self._try_register_cloned_plugin(plugin_name, target_dir, plugin_info) - - app_label = plugin_name - self.stdout.write(self.style.SUCCESS( - f'插件 {plugin_info.get("name", plugin_name)} 安装完成!' - )) - return app_label - - def _try_register_cloned_plugin(self, plugin_id, plugin_path, registry_info): - plugin_class = None - plugin_module_name = None - - plugin_class, plugin_module_name = self._load_plugin_class_from_package( - plugin_id, plugin_path - ) - - if plugin_class: - try: - plugin_instance = plugin_class() - plugin_manager = get_plugin_manager() - plugin_manager.plugins[plugin_instance.plugin_id] = plugin_instance - - try: - plugin_instance.initialize() - except Exception: - pass - - plugin_record, _ = PluginRecord.objects.update_or_create( - plugin_id=plugin_instance.plugin_id, - defaults={ - 'name': plugin_instance.name, - 'version': plugin_instance.version, - 'description': plugin_instance.description, - 'is_active': True, - } - ) - - self.add_plugin_to_toml_config(plugin_instance.plugin_id, { - 'name': plugin_instance.name, - 'module': plugin_module_name, - 'class': plugin_class.__name__, - 'description': plugin_instance.description, - 'version': plugin_instance.version, - 'enabled': True - }) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'插件类注册失败(插件文件已下载): {str(e)}' - )) - else: - self.stdout.write(self.style.WARNING( - '未找到 PluginInterface 子类,插件已下载但未自动注册到 TOML 配置' - )) - self.stdout.write('你可能需要手动在 plugins.toml 中添加插件配置') - - def _load_plugin_class_from_package(self, plugin_id, plugin_path): - plugin_class = None - plugin_module_name = None - mod_name = f'plugins.{plugin_id}' - - init_file = os.path.join(plugin_path, '__init__.py') - if os.path.exists(init_file): - try: - init_module = importlib.import_module(mod_name) - if hasattr(init_module, 'PLUGIN_INFO'): - pinfo = getattr(init_module, 'PLUGIN_INFO') - if 'main_class' in pinfo and hasattr(init_module, pinfo['main_class']): - plugin_class = getattr(init_module, pinfo['main_class']) - plugin_module_name = mod_name - if self.debug: - self.stdout.write(f'[DEBUG] 从 PLUGIN_INFO 找到插件类: {plugin_class.__name__}') - - if not plugin_class: - for attr_name in dir(init_module): - attr = getattr(init_module, attr_name) - if (inspect.isclass(attr) and - hasattr(attr, '__mro__') and - attr.__name__ != 'PluginInterface' and - any(hasattr(b, '__name__') and b.__name__ == 'PluginInterface' - for b in attr.__mro__)): - plugin_class = attr - plugin_module_name = mod_name - break - except ImportError as e: - if self.debug: - self.stdout.write(f'[DEBUG] import_module({mod_name}) 失败: {e}') - - try: - spec = importlib.util.spec_from_file_location( - mod_name, init_file - ) - if spec is not None and spec.loader is not None: - init_module = importlib.util.module_from_spec(spec) - sys.modules[mod_name] = init_module - spec.loader.exec_module(init_module) - - if hasattr(init_module, 'PLUGIN_INFO'): - pinfo = getattr(init_module, 'PLUGIN_INFO') - if 'main_class' in pinfo and hasattr(init_module, pinfo['main_class']): - plugin_class = getattr(init_module, pinfo['main_class']) - plugin_module_name = mod_name - - if not plugin_class: - for attr_name in dir(init_module): - attr = getattr(init_module, attr_name) - if (inspect.isclass(attr) and - hasattr(attr, '__mro__') and - attr.__name__ != 'PluginInterface' and - any(hasattr(b, '__name__') and b.__name__ == 'PluginInterface' - for b in attr.__mro__)): - plugin_class = attr - plugin_module_name = mod_name - break - except Exception as e2: - if self.debug: - self.stdout.write(f'[DEBUG] spec_from_file_location 也失败: {e2}') - - if not plugin_class: - plugin_class, plugin_module_name = self._scan_py_files_for_plugin( - plugin_id, plugin_path - ) - - if self.debug: - if plugin_class: - self.stdout.write(f'[DEBUG] 找到插件类: {plugin_class.__name__} from {plugin_module_name}') - else: - self.stdout.write(f'[DEBUG] 未找到插件类 in {plugin_path}') - - return plugin_class, plugin_module_name - - def _scan_py_files_for_plugin(self, plugin_id, plugin_path): - py_files = [ - f for f in os.listdir(plugin_path) - if f.endswith('.py') and f != '__init__.py' - ] - for pf in py_files: - fp = os.path.join(plugin_path, pf) - try: - with open(fp, 'r', encoding='utf-8') as f: - content = f.read() - if not re.search( - r'class\s+\w+\s*\([^)]*PluginInterface', - content, re.DOTALL - ): - continue - - mod_name = f'plugins.{plugin_id}.{pf[:-3]}' - try: - module = importlib.import_module(mod_name) - except ImportError: - spec = importlib.util.spec_from_file_location( - f"external_plugin_{plugin_id}", fp - ) - if spec is None or spec.loader is None: - continue - module = importlib.util.module_from_spec(spec) - sys.modules[mod_name] = module - spec.loader.exec_module(module) - - for attr_name in dir(module): - attr = getattr(module, attr_name) - if not inspect.isclass(attr) or not hasattr(attr, '__mro__'): - continue - if attr.__name__ == 'PluginInterface': - continue - if any( - hasattr(base, '__name__') and base.__name__ == 'PluginInterface' - for base in attr.__mro__ - ): - return attr, mod_name - except Exception: - continue - - return None, None - - def install_from_path(self, plugin_path): - if not os.path.exists(plugin_path): - raise CommandError(f'插件路径不存在: {plugin_path}') - - plugin_dir_name = os.path.basename(os.path.abspath(plugin_path)) - plugins_base = os.path.join(settings.BASE_DIR, 'plugins') - is_under_plugins = ( - os.path.dirname(os.path.abspath(plugin_path)) == - os.path.abspath(plugins_base) - ) - - plugin_manager = get_plugin_manager() - loaded_plugins = plugin_manager.get_all_plugins() - - plugin_class = None - plugin_module_name = None - - if is_under_plugins: - plugin_class, plugin_module_name = ( - self._load_plugin_class_from_package( - plugin_dir_name, plugin_path - ) - ) - else: - plugin_class, plugin_module_name = ( - self._load_plugin_class_from_external(plugin_path) - ) - - if not plugin_class: - raise CommandError( - f'在 {plugin_path} 中未找到有效的插件类\n' - f'请确保插件目录中包含继承自 PluginInterface 的类' - ) - - try: - plugin_instance = plugin_class() - - if plugin_instance.plugin_id in loaded_plugins: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_instance.name} (ID: {plugin_instance.plugin_id}) 已加载,跳过重复安装' - )) - return self._get_app_label_from_module(plugin_module_name) - - plugin_manager.plugins[plugin_instance.plugin_id] = ( - plugin_instance - ) - - from plugins.core.base import ServiceProvider - if isinstance(plugin_instance, ServiceProvider): - plugin_manager.service_registry.register(plugin_instance) - - try: - if plugin_instance.initialize(): - self.stdout.write(self.style.SUCCESS( - f'成功从路径安装插件: {plugin_instance.name}' - )) - else: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_instance.name} 安装成功但初始化失败' - )) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_instance.name} 初始化出错: {str(e)}' - )) - - plugin_record, created = PluginRecord.objects.update_or_create( - plugin_id=plugin_instance.plugin_id, - defaults={ - 'name': plugin_instance.name, - 'version': plugin_instance.version, - 'description': plugin_instance.description, - 'is_active': True, - } - ) - - if created: - self.stdout.write('已创建插件数据库记录') - else: - self.stdout.write('已更新插件数据库记录') - - self.add_plugin_to_toml_config(plugin_instance.plugin_id, { - 'name': plugin_instance.name, - 'module': plugin_module_name, - 'class': plugin_class.__name__, - 'description': plugin_instance.description, - 'version': plugin_instance.version, - 'enabled': True - }) - - self.stdout.write(self.style.SUCCESS( - f'插件 {plugin_instance.name} (v{plugin_instance.version}) 安装完成!' - )) - self.stdout.write( - '提示: 需要重启服务以使 Django App 注册生效' - ) - - return self._get_app_label_from_module(plugin_module_name) - except CommandError: - raise - except Exception as e: - raise CommandError(f'从路径安装插件失败: {str(e)}') - - def _load_plugin_class_from_external(self, plugin_path): - plugin_class = None - plugin_module_name = None - plugin_dir_name = os.path.basename(plugin_path) - - init_file = os.path.join(plugin_path, '__init__.py') - if os.path.exists(init_file): - try: - spec = importlib.util.spec_from_file_location( - f"plugin_init_{plugin_dir_name}", init_file - ) - if spec is not None and spec.loader is not None: - init_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(init_module) - if hasattr(init_module, 'PLUGIN_INFO'): - pinfo = getattr(init_module, 'PLUGIN_INFO') - mc = pinfo.get('main_class') - if mc and hasattr(init_module, mc): - plugin_class = getattr(init_module, mc) - plugin_module_name = ( - f'plugins.{plugin_dir_name}' - ) - except Exception: - pass - - if not plugin_class: - py_files = [ - f for f in os.listdir(plugin_path) - if f.endswith('.py') and f != '__init__.py' - ] - for pf in py_files: - fp = os.path.join(plugin_path, pf) - try: - with open(fp, 'r', encoding='utf-8') as f: - content = f.read() - if not re.search( - r'class\s+\w+\s*\([^)]*PluginInterface', - content, re.DOTALL - ): - continue - - mod_name = f'plugins.{plugin_dir_name}.{pf[:-3]}' - spec = importlib.util.spec_from_file_location( - mod_name, fp - ) - if spec is None or spec.loader is None: - continue - module = importlib.util.module_from_spec(spec) - sys.modules[mod_name] = module - spec.loader.exec_module(module) - - for attr_name in dir(module): - attr = getattr(module, attr_name) - if not inspect.isclass(attr): - continue - if attr.__name__ == 'PluginInterface': - continue - if not hasattr(attr, '__mro__'): - continue - if any( - hasattr(b, '__name__') and - b.__name__ == 'PluginInterface' - for b in attr.__mro__ - ): - return attr, mod_name - except Exception: - continue - - return plugin_class, plugin_module_name - - def install_builtin_plugin(self, plugin_id, plugin_info): - plugin_manager = get_plugin_manager() - - if plugin_id in plugin_manager.get_all_plugins(): - self.stdout.write( - self.style.WARNING(f'插件 {plugin_info["name"]} 已经加载.') - ) - return self._get_app_label_from_module( - plugin_info.get('module', '') - ) - - plugin = plugin_manager.load_builtin_plugin(plugin_id, plugin_info) - if not plugin: - raise CommandError(f'加载插件 {plugin_info["name"]} 失败') - - try: - if plugin.initialize(): - self.stdout.write(self.style.SUCCESS( - f'成功安装并初始化插件: {plugin.name}' - )) - else: - self.stdout.write(self.style.WARNING( - f'插件 {plugin.name} 安装成功但初始化失败' - )) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'插件 {plugin.name} 安装成功但初始化出错: {str(e)}' - )) - - plugin_record, created = PluginRecord.objects.update_or_create( - plugin_id=plugin.plugin_id, - defaults={ - 'name': plugin.name, - 'version': plugin.version, - 'description': plugin.description, - 'is_active': True, - } - ) - - if created: - self.stdout.write(f'已创建插件数据库记录') - - if plugin_id not in ALL_AVAILABLE_PLUGINS: - self.add_plugin_to_toml_config(plugin_id, plugin_info) - - return self._get_app_label_from_module( - plugin_info.get('module', '') - ) - - def uninstall_plugin(self, plugin_name, force=False, no_migrate=False): - self.stdout.write(f'正在卸载插件: {plugin_name}') - - plugin_manager = get_plugin_manager() - - plugin = None - loaded_plugins = plugin_manager.get_all_plugins() - app_label = None - for pid, p in loaded_plugins.items(): - if p.plugin_id == plugin_name or p.name.lower() == plugin_name.lower(): - plugin = p - app_label = pid - break - - if not plugin: - db_record = PluginRecord.objects.filter(plugin_id=plugin_name).first() - if db_record: - if force or input(f'插件 {plugin_name} 未加载但数据库中有记录。是否删除数据库记录?(y/N): ').lower() == 'y': - if not no_migrate: - self._run_migrate_reverse(plugin_name) - PluginRecord.objects.filter(plugin_id=plugin_name).delete() - self.remove_plugin_from_toml_config(plugin_name) - plugin_dir = os.path.join(settings.BASE_DIR, 'plugins', plugin_name) - if os.path.exists(plugin_dir): - shutil.rmtree(plugin_dir) - self.stdout.write(f'已删除插件目录: {plugin_dir}') - self.stdout.write(self.style.SUCCESS(f'已从数据库中删除插件记录: {plugin_name}')) - return - else: - self.stdout.write(f'操作已取消') - return - else: - raise CommandError(f'找不到已加载的插件: {plugin_name}') - - try: - if hasattr(plugin, 'shutdown'): - plugin.shutdown() - - success = plugin_manager.unload_plugin(plugin.plugin_id) - - if success: - if not no_migrate and app_label: - self._run_migrate_reverse(app_label) - PluginRecord.objects.filter(plugin_id=plugin.plugin_id).update(is_active=False) - self.remove_plugin_from_toml_config(plugin.plugin_id) - plugin_dir = os.path.join(settings.BASE_DIR, 'plugins', plugin.plugin_id) - if os.path.exists(plugin_dir): - shutil.rmtree(plugin_dir) - self.stdout.write(f'已删除插件目录: {plugin_dir}') - self.stdout.write(self.style.SUCCESS(f'成功卸载插件: {plugin.name}')) - else: - raise CommandError(f'卸载插件 {plugin.name} 失败') - except Exception as e: - if force: - if not no_migrate and app_label: - self._run_migrate_reverse(app_label) - PluginRecord.objects.filter(plugin_id=plugin.plugin_id).delete() - self.remove_plugin_from_toml_config(plugin.plugin_id) - plugin_dir = os.path.join(settings.BASE_DIR, 'plugins', plugin.plugin_id) - if os.path.exists(plugin_dir): - shutil.rmtree(plugin_dir) - self.stdout.write(self.style.WARNING(f'强制卸载插件: {plugin.name}')) - else: - raise CommandError(f'卸载插件失败: {str(e)}') - - def list_plugins(self): - self.stdout.write(self.style.SUCCESS('已安装的插件:')) - - plugin_manager = get_plugin_manager() - loaded_plugins = plugin_manager.get_all_plugins() - - if not loaded_plugins: - self.stdout.write(' 没有加载任何插件') - else: - for plugin_id, plugin in loaded_plugins.items(): - status = '✓' if hasattr(plugin, 'enabled') and plugin.enabled else '✓' - self.stdout.write(f' [{status}] {plugin.name} (v{plugin.version}) - {plugin.description}') - - db_records = PluginRecord.objects.all() - if db_records.exists(): - self.stdout.write('\n数据库中的插件记录:') - for record in db_records: - status = '✓' if record.is_active else '✗' - is_available = record.plugin_id in ALL_AVAILABLE_PLUGINS - availability_indicator = '●' if is_available else '○' - self.stdout.write(f' [{status}{availability_indicator}] {record.name} (v{record.version}) - {record.plugin_id}') - - self.stdout.write('\n可用插件:') - installed_plugin_ids = {p.plugin_id for p in loaded_plugins.values()} - for plugin_id, plugin_info in ALL_AVAILABLE_PLUGINS.items(): - status = '✓' if plugin_id in installed_plugin_ids else '○' - self.stdout.write(f' [{status}] {plugin_info["name"]} - {plugin_info["description"]}') - - self.stdout.write('\n远程仓库插件 (使用 plugins search 查看更多):') - try: - remote_plugins = self._fetch_registry() - for plugin_id, info in remote_plugins.items(): - installed = '✓' if plugin_id in installed_plugin_ids else '○' - self.stdout.write(f' [{installed}] {info.get("name", plugin_id)} - {info.get("description", "N/A")}') - except Exception: - self.stdout.write(self.style.WARNING(' 无法连接远程仓库')) - - def plugin_info(self, plugin_name): - plugin_manager = get_plugin_manager() - - loaded_plugins = plugin_manager.get_all_plugins() - plugin = None - for pid, p in loaded_plugins.items(): - if p.plugin_id == plugin_name or p.name.lower() == plugin_name.lower(): - plugin = p - break - - if not plugin: - for plugin_id, plugin_info in ALL_AVAILABLE_PLUGINS.items(): - if plugin_id == plugin_name or plugin_info['name'].lower() == plugin_name.lower(): - self.stdout.write(f'插件信息: {plugin_info["name"]}') - self.stdout.write(f' ID: {plugin_id}') - self.stdout.write(f' 版本: {plugin_info["version"]}') - self.stdout.write(f' 描述: {plugin_info["description"]}') - self.stdout.write(f' 模块: {plugin_info["module"]}') - self.stdout.write(f' 类: {plugin_info["class"]}') - self.stdout.write(f' 状态: {"已启用" if plugin_info["enabled"] else "已禁用"}') - return - - if plugin: - self.stdout.write(f'插件信息: {plugin.name}') - self.stdout.write(f' ID: {plugin.plugin_id}') - self.stdout.write(f' 版本: {plugin.version}') - self.stdout.write(f' 描述: {plugin.description}') - if hasattr(plugin, 'enabled'): - self.stdout.write(f' 状态: {"已启用" if plugin.enabled else "已禁用"}') - else: - self.stdout.write(f' 状态: 已加载') - else: - try: - remote_plugins = self._fetch_registry() - for plugin_id, info in remote_plugins.items(): - if plugin_id == plugin_name or info.get('name', '').lower() == plugin_name.lower(): - self.stdout.write(f'远程插件信息: {info.get("name", plugin_id)}') - self.stdout.write(f' ID: {plugin_id}') - self.stdout.write(f' 版本: {info.get("version", "N/A")}') - self.stdout.write(f' 描述: {info.get("description", "N/A")}') - self.stdout.write(f' 仓库: {info.get("repository", "N/A")}') - return - except Exception: - pass - - db_record = PluginRecord.objects.filter(plugin_id=plugin_name).first() - if db_record: - self.stdout.write(f'插件信息: {db_record.name}') - self.stdout.write(f' ID: {db_record.plugin_id}') - self.stdout.write(f' 版本: {db_record.version}') - self.stdout.write(f' 描述: {db_record.description}') - self.stdout.write(f' 状态: {"已激活" if db_record.is_active else "未激活"}') - self.stdout.write(f' 注意: 此插件在数据库中有记录,但当前版本中不可用(可能是私有插件)') - else: - raise CommandError(f'找不到插件: {plugin_name}') - - def add_plugin_to_toml_config(self, plugin_id, plugin_info): - config_file_path = os.path.join(settings.BASE_DIR, 'plugins', 'plugins.toml') - - if os.path.exists(config_file_path): - with open(config_file_path, 'r', encoding='utf-8') as f: - toml_data = toml.load(f) - else: - toml_data = { - 'builtin': {}, - 'third_party': {} - } - - if plugin_id in toml_data.get('builtin', {}) or plugin_id in toml_data.get('third_party', {}): - self.stdout.write(f'插件 {plugin_id} 已存在于配置中') - return - - toml_data.setdefault('third_party', {})[plugin_id] = { - 'name': plugin_info['name'], - 'module': plugin_info['module'], - 'class': plugin_info['class'], - 'description': plugin_info['description'], - 'version': plugin_info['version'], - 'enabled': plugin_info['enabled'] - } - - with open(config_file_path, 'w', encoding='utf-8') as f: - toml.dump(toml_data, f) - - self.stdout.write(f'已将插件 {plugin_id} 添加到 TOML 配置文件') - - def remove_plugin_from_toml_config(self, plugin_id): - config_file_path = os.path.join(settings.BASE_DIR, 'plugins', 'plugins.toml') - - if not os.path.exists(config_file_path): - self.stdout.write(f'TOML 配置文件不存在: {config_file_path}') - return - - with open(config_file_path, 'r', encoding='utf-8') as f: - toml_data = toml.load(f) - - plugin_removed = False - if 'builtin' in toml_data and plugin_id in toml_data['builtin']: - del toml_data['builtin'][plugin_id] - plugin_removed = True - self.stdout.write(f'已从 builtin 部分移除插件 {plugin_id}') - - if 'third_party' in toml_data and plugin_id in toml_data['third_party']: - del toml_data['third_party'][plugin_id] - plugin_removed = True - self.stdout.write(f'已从 third_party 部分移除插件 {plugin_id}') - - if plugin_removed: - with open(config_file_path, 'w', encoding='utf-8') as f: - toml.dump(toml_data, f) - - self.stdout.write(f'已从 TOML 配置文件中移除插件 {plugin_id}') - else: - self.stdout.write(f'插件 {plugin_id} 在 TOML 配置文件中未找到') - - def _resolve_plugin_id(self, plugin_name): - plugin_manager = get_plugin_manager() - loaded_plugins = plugin_manager.get_all_plugins() - - for pid, p in loaded_plugins.items(): - if p.plugin_id == plugin_name or p.name.lower() == plugin_name.lower(): - return p.plugin_id - - for plugin_id, plugin_info in ALL_AVAILABLE_PLUGINS.items(): - if plugin_id == plugin_name or plugin_info.get('name', '').lower() == plugin_name.lower(): - return plugin_id - - db_record = PluginRecord.objects.filter(plugin_id=plugin_name).first() - if not db_record: - db_record = PluginRecord.objects.filter( - name__iexact=plugin_name - ).first() - if db_record: - return db_record.plugin_id - - return None - - def enable_plugin(self, plugin_name): - plugin_id = self._resolve_plugin_id(plugin_name) - if not plugin_id: - raise CommandError(f'找不到插件: {plugin_name}') - - plugin_manager = get_plugin_manager() - loaded_plugins = plugin_manager.get_all_plugins() - - if plugin_id in loaded_plugins: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_id} 已加载且处于启用状态' - )) - self.update_plugin_enabled_in_toml(plugin_id, True) - PluginRecord.objects.filter(plugin_id=plugin_id).update( - is_active=True - ) - return - - plugin_info = ALL_AVAILABLE_PLUGINS.get(plugin_id) - if plugin_info: - plugin = plugin_manager.load_builtin_plugin(plugin_id, plugin_info) - if plugin: - try: - plugin.initialize() - self.stdout.write(self.style.SUCCESS( - f'成功启用并初始化插件: {plugin.name}' - )) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'插件 {plugin.name} 启用成功但初始化失败: {str(e)}' - )) - else: - raise CommandError(f'加载插件 {plugin_id} 失败') - else: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_id} 不在可用插件列表中,仅更新配置和数据库状态' - )) - - self.update_plugin_enabled_in_toml(plugin_id, True) - PluginRecord.objects.update_or_create( - plugin_id=plugin_id, - defaults={'is_active': True} - ) - self.stdout.write(self.style.SUCCESS(f'插件 {plugin_id} 已启用')) - - def disable_plugin(self, plugin_name): - plugin_id = self._resolve_plugin_id(plugin_name) - if not plugin_id: - raise CommandError(f'找不到插件: {plugin_name}') - - plugin_manager = get_plugin_manager() - loaded_plugins = plugin_manager.get_all_plugins() - - if plugin_id in loaded_plugins: - plugin = loaded_plugins[plugin_id] - try: - if hasattr(plugin, 'shutdown'): - plugin.shutdown() - plugin_manager.unload_plugin(plugin_id) - self.stdout.write(f'已卸载插件: {plugin.name}') - except Exception as e: - raise CommandError(f'卸载插件 {plugin.name} 失败: {str(e)}') - - self.update_plugin_enabled_in_toml(plugin_id, False) - PluginRecord.objects.filter(plugin_id=plugin_id).update( - is_active=False - ) - self.stdout.write(self.style.SUCCESS(f'插件 {plugin_id} 已禁用')) - - def update_plugin_enabled_in_toml(self, plugin_id, enabled): - config_file_path = os.path.join( - settings.BASE_DIR, 'plugins', 'plugins.toml' - ) - - if not os.path.exists(config_file_path): - self.stdout.write(self.style.WARNING( - f'TOML 配置文件不存在: {config_file_path}' - )) - return False - - with open(config_file_path, 'r', encoding='utf-8') as f: - toml_data = toml.load(f) - - updated = False - for section in ('builtin', 'third_party'): - if section in toml_data and plugin_id in toml_data[section]: - toml_data[section][plugin_id]['enabled'] = enabled - updated = True - break - - if not updated: - self.stdout.write(self.style.WARNING( - f'插件 {plugin_id} 在 TOML 配置文件中未找到,将添加配置' - )) - toml_data.setdefault('third_party', {})[plugin_id] = { - 'enabled': enabled - } - updated = True - - if updated: - with open(config_file_path, 'w', encoding='utf-8') as f: - toml.dump(toml_data, f) - state = '启用' if enabled else '禁用' - self.stdout.write( - f'已更新 TOML 配置: 插件 {plugin_id} -> {state}' - ) - - return updated - - def _get_app_label_from_module(self, module_name): - if not module_name: - return None - parts = module_name.rsplit('.', 1) - if len(parts) == 2 and parts[0].startswith('plugins.'): - return parts[0].split('.')[1] - return None - - def _run_migrate(self, app_label): - import subprocess - import sys - self.stdout.write(f'正在执行数据库迁移: {app_label}') - if self.debug: - from django.apps import apps - installed = [a.label for a in apps.get_app_configs()] - self.stdout.write(f'[DEBUG] 已注册 App labels: {installed}') - self.stdout.write(f'[DEBUG] {app_label} is_installed: {apps.is_installed(app_label)}') - try: - result = subprocess.run( - [sys.executable, 'manage.py', 'migrate', app_label, '--verbosity=2' if self.debug else '--verbosity=0'], - capture_output=True, text=True, - cwd=str(settings.BASE_DIR), - ) - if self.debug and result.stdout: - self.stdout.write(f'[DEBUG] migrate stdout:\n{result.stdout}') - if self.debug and result.stderr: - self.stdout.write(f'[DEBUG] migrate stderr:\n{result.stderr}') - if result.returncode == 0: - self.stdout.write(self.style.SUCCESS( - f'数据库迁移完成: {app_label}' - )) - else: - self.stdout.write(self.style.WARNING( - f'数据库迁移失败: {result.stderr.strip()}' - )) - self.stdout.write( - '你可以手动执行: ' - f'python manage.py migrate {app_label}' - ) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'数据库迁移失败: {str(e)}' - )) - - def _run_migrate_reverse(self, app_label): - import subprocess - import sys - self.stdout.write(f'正在回滚数据库迁移: {app_label}') - try: - result = subprocess.run( - [ - sys.executable, 'manage.py', - 'migrate', app_label, 'zero', - ], - capture_output=True, text=True, - cwd=str(settings.BASE_DIR), - ) - if result.returncode == 0: - self.stdout.write(self.style.SUCCESS( - f'数据库迁移已回滚: {app_label}' - )) - else: - self.stdout.write(self.style.WARNING( - f'数据库迁移回滚失败: {result.stderr.strip()}' - )) - except Exception as e: - self.stdout.write(self.style.WARNING( - f'数据库迁移回滚失败: {str(e)}' - )) - - def upgrade_plugin(self, plugin_name, registry_url=None): - self.stdout.write(f'正在升级插件: {plugin_name}') - - plugin_info = ALL_AVAILABLE_PLUGINS.get(plugin_name) - if not plugin_info: - for pid, pinfo in ALL_AVAILABLE_PLUGINS.items(): - if pinfo['name'].lower() == plugin_name.lower(): - plugin_info = pinfo - plugin_name = pid - break - - if not plugin_info: - self.stdout.write( - '插件未在本地配置中找到,尝试从远程仓库更新...' - ) - self.install_from_registry( - plugin_name, registry_url, force=True - ) - return - - app_label = self._get_app_label_from_module( - plugin_info.get('module', '') - ) - if app_label: - self._run_migrate(app_label) - - self.stdout.write(self.style.SUCCESS( - f'插件 {plugin_name} 升级完成' - )) diff --git a/plugins/migrations/0001_initial.py b/plugins/migrations/0001_initial.py deleted file mode 100755 index 6b9cb21..0000000 --- a/plugins/migrations/0001_initial.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 4.2+ on migration creation - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='PluginRecord', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('plugin_id', models.CharField(help_text='插件的唯一标识符', max_length=255, unique=True, verbose_name='插件ID')), - ('name', models.CharField(help_text='插件的显示名称', max_length=255, verbose_name='插件名称')), - ('version', models.CharField(default='1.0.0', help_text='插件的版本号', max_length=50, verbose_name='版本号')), - ('description', models.TextField(blank=True, help_text='插件的功能描述', verbose_name='描述')), - ('is_active', models.BooleanField(default=True, help_text='插件是否处于启用状态', verbose_name='是否启用')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '插件', - 'verbose_name_plural': '插件', - 'db_table': 'plugin_records', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='PluginConfiguration', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='配置项的键名', max_length=255, verbose_name='配置键')), - ('value', models.TextField(help_text='配置项的值', verbose_name='配置值')), - ('description', models.TextField(blank=True, help_text='配置项的描述信息', verbose_name='描述')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('plugin', models.ForeignKey(on_delete=models.CASCADE, related_name='configurations', to='plugins.pluginrecord', verbose_name='关联插件')), - ], - options={ - 'verbose_name': '插件配置', - 'verbose_name_plural': '插件配置', - 'db_table': 'plugin_configurations', - }, - ), - migrations.AddConstraint( - model_name='pluginconfiguration', - constraint=models.UniqueConstraint(fields=('plugin', 'key'), name='unique_plugin_key'), - ), - ] \ No newline at end of file diff --git a/plugins/migrations/0002_qqverifyconfig_and_more.py b/plugins/migrations/0002_qqverifyconfig_and_more.py deleted file mode 100755 index 20e0aa5..0000000 --- a/plugins/migrations/0002_qqverifyconfig_and_more.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-29 09:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('plugins', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='QQVerifyConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('host', models.CharField(help_text='QQ机器人服务器的主机地址', max_length=255, verbose_name='机器人服务器地址')), - ('port', models.CharField(help_text='QQ机器人服务器的端口', max_length=10, verbose_name='机器人服务器端口')), - ('token', models.CharField(help_text='访问QQ机器人API的令牌', max_length=255, verbose_name='访问令牌')), - ('group_id', models.CharField(help_text='需要验证的QQ群号', max_length=20, verbose_name='目标群号')), - ('enable_status', models.CharField(choices=[('disabled', '禁用'), ('enabled_require_group', '启用-要求入群'), ('enabled_old_six_mode', '启用-老六模式'), ('enabled_both', '启用-两种模式')], default='disabled', help_text='QQ验证功能的启用状态和模式', max_length=25, verbose_name='启用状态')), - ], - ), - migrations.RemoveConstraint( - model_name='pluginconfiguration', - name='unique_plugin_key', - ), - migrations.AlterUniqueTogether( - name='pluginconfiguration', - unique_together={('plugin', 'key')}, - ), - ] diff --git a/plugins/migrations/0003_auto_20260129_1808.py b/plugins/migrations/0003_auto_20260129_1808.py deleted file mode 100755 index bc2540b..0000000 --- a/plugins/migrations/0003_auto_20260129_1808.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.2.27 on 2026-01-29 10:08 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('operations', '0002_publichostinfo'), - ('plugins', '0002_qqverifyconfig_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='QQVerificationConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('host', models.CharField(help_text='QQ机器人服务器的主机地址', max_length=255, verbose_name='机器人服务器地址')), - ('port', models.CharField(help_text='QQ机器人服务器的端口号', max_length=20, verbose_name='机器人服务器端口')), - ('token', models.CharField(help_text='用于认证的访问令牌', max_length=255, verbose_name='访问令牌')), - ('group_id', models.CharField(help_text='用于验证QQ号是否在群内的群号', max_length=20, verbose_name='验证群号')), - ('enable_status', models.CharField(choices=[('disabled', '禁用'), ('enabled_require_group', '启用-要求入群'), ('enabled_old_six_mode', '启用-老六模式'), ('enabled_both', '启用-两种模式')], default='disabled', help_text='QQ验证功能的启用状态', max_length=25, verbose_name='启用状态')), - ('non_qq_email_handling', models.CharField(choices=[('default_allow', '默认符合'), ('default_deny', '默认不符合'), ('manual_review', '等待人工处理')], default='default_deny', help_text='当用户使用非QQ邮箱时的处理策略', max_length=20, verbose_name='非QQ邮箱处理策略')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ('product', models.OneToOneField(help_text='此QQ验证配置关联的产品', on_delete=django.db.models.deletion.CASCADE, related_name='qq_verification_config', to='operations.product', verbose_name='关联产品')), - ], - options={ - 'verbose_name': 'QQ验证配置', - 'verbose_name_plural': 'QQ验证配置', - 'unique_together': {('product',)}, - }, - ), - ] \ No newline at end of file diff --git a/plugins/migrations/0005_auto_20260130_1041.py b/plugins/migrations/0005_auto_20260130_1041.py deleted file mode 100755 index 0808ca0..0000000 --- a/plugins/migrations/0005_auto_20260130_1041.py +++ /dev/null @@ -1,14 +0,0 @@ -# 修复依赖问题的占位迁移文件 -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('plugins', '0003_auto_20260129_1808'), - ] - - operations = [ - # 占位操作,不实际改变数据库 - migrations.RunSQL(migrations.RunSQL.noop, reverse_sql=migrations.RunSQL.noop), - ] \ No newline at end of file diff --git a/plugins/migrations/0006_delete_qqverifyconfig_alter_pluginrecord_options_and_more.py b/plugins/migrations/0006_delete_qqverifyconfig_alter_pluginrecord_options_and_more.py deleted file mode 100644 index e51dd0c..0000000 --- a/plugins/migrations/0006_delete_qqverifyconfig_alter_pluginrecord_options_and_more.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.2.27 on 2026-02-02 15:15 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('plugins', '0005_auto_20260130_1041'), - ] - - operations = [ - migrations.DeleteModel( - name='QQVerifyConfig', - ), - migrations.AlterModelOptions( - name='pluginrecord', - options={'ordering': ['-created_at'], 'verbose_name': '插件记录', 'verbose_name_plural': '插件记录'}, - ), - migrations.AlterField( - model_name='pluginconfiguration', - name='description', - field=models.TextField(blank=True, help_text='配置项的描述', verbose_name='描述'), - ), - migrations.AlterField( - model_name='pluginconfiguration', - name='key', - field=models.CharField(help_text='配置参数的键名', max_length=200, verbose_name='配置键'), - ), - migrations.AlterField( - model_name='pluginconfiguration', - name='plugin', - field=models.ForeignKey(help_text='配置所属的插件', on_delete=django.db.models.deletion.CASCADE, to='plugins.pluginrecord', verbose_name='插件'), - ), - migrations.AlterField( - model_name='pluginconfiguration', - name='value', - field=models.TextField(help_text='配置参数的值', verbose_name='配置值'), - ), - migrations.AlterField( - model_name='pluginrecord', - name='description', - field=models.TextField(blank=True, help_text='插件的详细描述', verbose_name='描述'), - ), - migrations.AlterField( - model_name='pluginrecord', - name='is_active', - field=models.BooleanField(default=True, help_text='插件是否处于激活状态', verbose_name='是否激活'), - ), - migrations.AlterField( - model_name='pluginrecord', - name='name', - field=models.CharField(help_text='插件的显示名称', max_length=200, verbose_name='插件名称'), - ), - migrations.AlterField( - model_name='pluginrecord', - name='plugin_id', - field=models.CharField(help_text='插件的唯一标识符', max_length=100, unique=True, verbose_name='插件ID'), - ), - migrations.AlterField( - model_name='pluginrecord', - name='version', - field=models.CharField(help_text='插件的版本号', max_length=50, verbose_name='版本号'), - ), - migrations.AddConstraint( - model_name='pluginconfiguration', - constraint=models.UniqueConstraint(fields=('plugin', 'key'), name='unique_plugin_key'), - ), - migrations.AlterModelTable( - name='pluginconfiguration', - table=None, - ), - migrations.AlterModelTable( - name='pluginrecord', - table=None, - ), - ] diff --git a/plugins/migrations/0007_add_use_default_bot_and_group_ids.py b/plugins/migrations/0007_add_use_default_bot_and_group_ids.py deleted file mode 100644 index b6a73bb..0000000 --- a/plugins/migrations/0007_add_use_default_bot_and_group_ids.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 4.2.27 on 2026-05-02 09:15 - -from django.db import migrations, models - - -def migrate_group_id_to_group_ids(apps, schema_editor): - QQVerificationConfig = apps.get_model( - 'plugins', 'QQVerificationConfig' - ) - for config in QQVerificationConfig.objects.all(): - old_group_id = getattr(config, 'group_id', None) - if old_group_id: - config.group_ids = old_group_id - config.save(update_fields=['group_ids']) - - -class Migration(migrations.Migration): - - dependencies = [ - ('plugins', '0006_delete_qqverifyconfig_alter_pluginrecord_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='qqverificationconfig', - name='group_ids', - field=models.TextField(blank=True, help_text='用于验证QQ号是否在群内的群号,每行一个QQ群号', null=True, verbose_name='验证群号'), - ), - migrations.AddField( - model_name='qqverificationconfig', - name='use_default_bot', - field=models.BooleanField(default=True, help_text='启用后使用系统配置中的默认机器人服务器地址、端口和令牌', verbose_name='使用系统默认机器人服务器'), - ), - migrations.AlterField( - model_name='qqverificationconfig', - name='host', - field=models.CharField(blank=True, help_text='QQ机器人服务器的主机地址', max_length=255, null=True, verbose_name='机器人服务器地址'), - ), - migrations.AlterField( - model_name='qqverificationconfig', - name='port', - field=models.CharField(blank=True, help_text='QQ机器人服务器的端口号', max_length=20, null=True, verbose_name='机器人服务器端口'), - ), - migrations.AlterField( - model_name='qqverificationconfig', - name='token', - field=models.CharField(blank=True, help_text='用于认证的访问令牌', max_length=255, null=True, verbose_name='访问令牌'), - ), - migrations.RunPython( - migrate_group_id_to_group_ids, - migrations.RunPython.noop, - ), - migrations.RemoveField( - model_name='qqverificationconfig', - name='group_id', - ), - ] diff --git a/plugins/migrations/0008_move_qqverificationconfig_to_own_app.py b/plugins/migrations/0008_move_qqverificationconfig_to_own_app.py deleted file mode 100644 index f8885ce..0000000 --- a/plugins/migrations/0008_move_qqverificationconfig_to_own_app.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('plugins', '0007_add_use_default_bot_and_group_ids'), - ] - - operations = [ - migrations.SeparateDatabaseAndState( - state_operations=[ - migrations.DeleteModel( - name='QQVerificationConfig', - ), - ], - database_operations=[], - ), - ] diff --git a/plugins/migrations/__init__.py b/plugins/migrations/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/plugins/models.py b/plugins/models.py deleted file mode 100755 index fd143df..0000000 --- a/plugins/models.py +++ /dev/null @@ -1,89 +0,0 @@ -from django.db import models -import logging - -logger = logging.getLogger(__name__) - - -class PluginRecord(models.Model): - plugin_id = models.CharField( - max_length=100, - unique=True, - verbose_name='插件ID', - help_text='插件的唯一标识符', - ) - name = models.CharField( - max_length=200, - verbose_name='插件名称', - help_text='插件的显示名称', - ) - version = models.CharField( - max_length=50, - verbose_name='版本号', - help_text='插件的版本号', - ) - description = models.TextField( - blank=True, verbose_name='描述', - help_text='插件的详细描述', - ) - is_active = models.BooleanField( - default=True, - verbose_name='是否激活', - help_text='插件是否处于激活状态', - ) - created_at = models.DateTimeField( - auto_now_add=True, verbose_name='创建时间' - ) - updated_at = models.DateTimeField( - auto_now=True, verbose_name='更新时间' - ) - - class Meta: - verbose_name = '插件记录' - verbose_name_plural = '插件记录' - ordering = ['-created_at'] - - def __str__(self): - return f"{self.name} v{self.version}" - - -class PluginConfiguration(models.Model): - plugin = models.ForeignKey( - PluginRecord, - on_delete=models.CASCADE, - verbose_name='插件', - help_text='配置所属的插件', - ) - key = models.CharField( - max_length=200, - verbose_name='配置键', - help_text='配置参数的键名', - ) - value = models.TextField( - verbose_name='配置值', - help_text='配置参数的值', - ) - description = models.TextField( - blank=True, - verbose_name='描述', - help_text='配置项的描述', - ) - created_at = models.DateTimeField( - auto_now_add=True, verbose_name='创建时间' - ) - updated_at = models.DateTimeField( - auto_now=True, verbose_name='更新时间' - ) - - class Meta: - verbose_name = '插件配置' - verbose_name_plural = '插件配置' - unique_together = [['plugin', 'key']] - constraints = [ - models.UniqueConstraint( - fields=['plugin', 'key'], - name='unique_plugin_key', - ) - ] - - def __str__(self): - return f"{self.plugin.name}: {self.key}" diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py deleted file mode 100755 index f53318f..0000000 --- a/plugins/plugin_manager.py +++ /dev/null @@ -1,393 +0,0 @@ -""" -插件管理系统管理器 -负责插件的加载、卸载、管理和执行 -""" - -import os -import sys -import importlib -import importlib.util -import logging -from typing import Any, Dict, List, Optional, Type - -from plugins.core.base import PluginInterface, EventHook - -logger = logging.getLogger(__name__) - - -class PluginManager: - """ - 插件管理器 - 负责管理所有插件的生命周期 - """ - - def __init__(self): - self.plugins: Dict[str, PluginInterface] = {} - self.hooks: Dict[str, EventHook] = {} - self.plugin_dirs: List[str] = [] - - def _get_plugin_model(self): - """延迟导入PluginRecord模型""" - try: - from django.conf import settings - # 检查Django是否已配置 - if settings.configured: - from .models import PluginRecord - return PluginRecord - except (ImportError, AttributeError): - pass - return None - - def add_plugin_directory(self, directory: str): - """添加插件目录""" - if os.path.isdir(directory) and directory not in self.plugin_dirs: - self.plugin_dirs.append(directory) - - def load_plugins_from_directory(self, directory: str) -> List[str]: - """ - 从指定目录加载插件 - :param directory: 插件目录路径 - :return: 成功加载的插件ID列表 - """ - loaded_plugins = [] - - if not os.path.isdir(directory): - logger.warning(f"Plugin directory does not exist: {directory}") - return loaded_plugins - - NON_PLUGIN_FILES = frozenset([ - 'base.py', 'models.py', 'admin.py', 'views.py', - 'urls.py', 'signals.py', 'django_integration.py', - 'apps.py', 'forms_admin.py', 'forms_provider.py', - 'available_plugins.py', - ]) - NON_PLUGIN_SUBDIRS = frozenset([ - 'core', 'migrations', 'management', 'sample_plugins', - ]) - - for item in os.listdir(directory): - item_path = os.path.join(directory, item) - if not os.path.isfile(item_path): - continue - if not item.endswith('.py') or item == '__init__.py': - continue - if item in NON_PLUGIN_FILES: - continue - - plugin_filename = item[:-3] - module_name = f"plugins.{plugin_filename}" - - try: - spec = importlib.util.spec_from_file_location( - module_name, item_path - ) - if spec and spec.loader: - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - loaded_plugins.extend( - self._extract_and_register_plugins( - module, plugin_filename - ) - ) - except ImportError: - pass - except Exception as e: - logger.error(f"Error loading plugin from {item_path}: {str(e)}") - - for subdir in os.listdir(directory): - subdir_path = os.path.join(directory, subdir) - if not os.path.isdir(subdir_path): - continue - if subdir.startswith('_') or subdir in NON_PLUGIN_SUBDIRS: - continue - init_path = os.path.join(subdir_path, '__init__.py') - if not os.path.isfile(init_path): - continue - - loaded_plugins.extend( - self._load_subdir_package(directory, subdir) - ) - - return loaded_plugins - - def _load_subdir_package( - self, parent_dir: str, subdir: str - ) -> List[str]: - loaded_plugins = [] - package_name = f"plugins.{subdir}" - - try: - if package_name in sys.modules: - module = sys.modules[package_name] - else: - module = importlib.import_module(package_name) - - loaded_plugins.extend( - self._extract_and_register_plugins(module, subdir) - ) - except ImportError as e: - logger.error( - f"Error importing plugin package " - f"{package_name}: {str(e)}" - ) - except Exception as e: - logger.error( - f"Error loading plugin package " - f"{package_name}: {str(e)}" - ) - - return loaded_plugins - - def _extract_and_register_plugins( - self, module, default_id_prefix: str - ) -> List[str]: - loaded = [] - for attr_name in dir(module): - attr = getattr(module, attr_name) - if ( - isinstance(attr, type) - and issubclass(attr, PluginInterface) - and attr != PluginInterface - ): - try: - plugin_instance = attr() - if not hasattr(plugin_instance, 'plugin_id'): - plugin_instance.plugin_id = ( - f"{default_id_prefix}_" - f"{attr.__name__.lower()}" - ) - if self.register_plugin(plugin_instance): - loaded.append(plugin_instance.plugin_id) - except Exception as e: - logger.error( - f"Error instantiating plugin " - f"{attr_name}: {str(e)}" - ) - return loaded - - def register_plugin(self, plugin: PluginInterface) -> bool: - """ - 注册插件 - :param plugin: 插件实例 - :return: 注册是否成功 - """ - if plugin.plugin_id in self.plugins: - logger.warning(f"Plugin with ID {plugin.plugin_id} already exists") - return False - - try: - # 初始化插件 - if plugin.initialize(): - self.plugins[plugin.plugin_id] = plugin - logger.info(f"Successfully registered plugin: {plugin.name} ({plugin.plugin_id})") - - # 同步到数据库(如果Django可用) - plugin_model = self._get_plugin_model() - if plugin_model: - try: - from django.db import transaction - # 使用事务和原子操作 - with transaction.atomic(): - plugin_record, created = plugin_model.objects.get_or_create( - plugin_id=plugin.plugin_id, - defaults={ - 'name': plugin.name, - 'version': plugin.version, - 'description': plugin.description, - 'is_active': plugin.enabled - } - ) - except Exception as db_error: - logger.error(f"Error syncing plugin to database: {str(db_error)}") - - return True - else: - logger.warning(f"Failed to initialize plugin: {plugin.name}") - return False - except Exception as e: - logger.error(f"Error initializing plugin {plugin.name}: {str(e)}") - return False - - def unregister_plugin(self, plugin_id: str) -> bool: - """ - 卸载插件 - :param plugin_id: 插件ID - :return: 卸载是否成功 - """ - if plugin_id not in self.plugins: - logger.warning(f"Plugin with ID {plugin_id} does not exist") - return False - - plugin = self.plugins[plugin_id] - - try: - # 关闭插件 - if plugin.shutdown(): - del self.plugins[plugin_id] - logger.info(f"Successfully unregistered plugin: {plugin.name}") - - return True - else: - logger.warning(f"Failed to shutdown plugin: {plugin.name}") - return False - except Exception as e: - logger.error(f"Error shutting down plugin {plugin.name}: {str(e)}") - return False - - def enable_plugin(self, plugin_id: str) -> bool: - """启用插件""" - if plugin_id in self.plugins: - self.plugins[plugin_id].enabled = True - - # 同步到数据库(如果Django可用) - plugin_model = self._get_plugin_model() - if plugin_model: - try: - # 使用 update 方法直接更新数据库,避免触发信号 - rows_updated = plugin_model.objects.filter(plugin_id=plugin_id).update(is_active=True) - if rows_updated > 0: - logger.info(f"Database updated for plugin {plugin_id} (enabled)") - except Exception as db_error: - logger.error(f"Error updating plugin status in database: {str(db_error)}") - - return True - return False - - def disable_plugin(self, plugin_id: str) -> bool: - """禁用插件""" - if plugin_id in self.plugins: - self.plugins[plugin_id].enabled = False - - # 同步到数据库(如果Django可用) - plugin_model = self._get_plugin_model() - if plugin_model: - try: - # 使用 update 方法直接更新数据库,避免触发信号 - rows_updated = plugin_model.objects.filter(plugin_id=plugin_id).update(is_active=False) - if rows_updated > 0: - logger.info(f"Database updated for plugin {plugin_id} (disabled)") - except Exception as db_error: - logger.error(f"Error updating plugin status in database: {str(db_error)}") - - return True - return False - - def get_plugin(self, plugin_id: str) -> Optional[PluginInterface]: - """获取插件实例""" - return self.plugins.get(plugin_id) - - def get_all_plugins(self) -> List[PluginInterface]: - """获取所有插件""" - return list(self.plugins.values()) - - def get_enabled_plugins(self) -> List[PluginInterface]: - """获取所有启用的插件""" - return [plugin for plugin in self.plugins.values() if plugin.enabled] - - def load_all_plugins(self) -> List[str]: - """ - 从所有已注册的目录加载插件 - :return: 成功加载的插件ID列表 - """ - all_loaded = [] - for directory in self.plugin_dirs: - loaded = self.load_all_plugins_from_directory(directory) - all_loaded.extend(loaded) - return all_loaded - - def load_all_plugins_from_directory(self, directory: str) -> List[str]: - """ - 从指定目录加载所有插件 - :param directory: 插件目录路径 - :return: 成功加载的插件ID列表 - """ - loaded_plugins = [] - - if not os.path.isdir(directory): - logger.warning(f"Plugin directory does not exist: {directory}") - return loaded_plugins - - for item in os.listdir(directory): - item_path = os.path.join(directory, item) - - # 检查是否是Python文件且不是__init__.py - if os.path.isfile(item_path) and item.endswith('.py') and item != '__init__.py': - plugin_filename = item[:-3] # 移除.py后缀 - module_name = f"sample_plugins_{plugin_filename}" # 使用不同的模块名避免冲突 - - try: - # 使用 importlib.util.spec_from_file_location 动态加载模块 - spec = importlib.util.spec_from_file_location(module_name, item_path) - if spec and spec.loader: - module = importlib.util.module_from_spec(spec) - # 添加到 sys.modules 以支持相对导入 - sys.modules[spec.name] = module - spec.loader.exec_module(module) - - # 查找插件类(继承自PluginInterface的类) - for attr_name in dir(module): - attr = getattr(module, attr_name) - if ( - isinstance(attr, type) and - issubclass(attr, PluginInterface) and - attr != PluginInterface - ): - plugin_class: Type[PluginInterface] = attr - - # 创建插件实例并初始化 - plugin_instance = plugin_class() - - # 如果插件实例没有设置ID,则使用默认值 - if not hasattr(plugin_instance, 'plugin_id'): - plugin_instance.plugin_id = f"{plugin_filename}_{plugin_class.__name__.lower()}" - - if self.register_plugin(plugin_instance): - loaded_plugins.append(plugin_instance.plugin_id) - - except ImportError as e: - logger.error(f"Failed to import plugin module from {item_path}: {str(e)}") - except Exception as e: - logger.error(f"Error loading plugin from {item_path}: {str(e)}") - - return loaded_plugins - - def register_hook(self, hook_name: str, handler: callable): - """ - 注册钩子处理器 - :param hook_name: 钩子名称 - :param handler: 处理器函数 - """ - if hook_name not in self.hooks: - self.hooks[hook_name] = EventHook(hook_name) - self.hooks[hook_name].register(handler) - - def unregister_hook(self, hook_name: str, handler: callable): - """ - 注销钩子处理器 - :param hook_name: 钩子名称 - :param handler: 处理器函数 - """ - if hook_name in self.hooks: - self.hooks[hook_name].unregister(handler) - - def trigger_hook(self, hook_name: str, *args, **kwargs) -> List[Any]: - """ - 触发钩子 - :param hook_name: 钩子名称 - :param args: 传递给处理器的位置参数 - :param kwargs: 传递给处理器的关键字参数 - :return: 所有处理器的返回值列表 - """ - if hook_name in self.hooks: - return self.hooks[hook_name].execute(*args, **kwargs) - return [] - - def get_hook(self, hook_name: str) -> Optional[EventHook]: - """获取钩子实例""" - return self.hooks.get(hook_name) - - def shutdown_all_plugins(self): - """关闭所有插件""" - for plugin_id in list(self.plugins.keys()): - self.unregister_plugin(plugin_id) \ No newline at end of file diff --git a/plugins/signals.py b/plugins/signals.py deleted file mode 100755 index 6c957f9..0000000 --- a/plugins/signals.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -插件系统信号处理器 -""" -from django.db.models.signals import post_save -from django.dispatch import receiver -from apps.operations.models import AccountOpeningRequest, CloudComputerUser -from .core.plugin_manager import get_plugin_manager -import logging - -logger = logging.getLogger(__name__) - - -@receiver(post_save, sender=AccountOpeningRequest) -def handle_account_opening_request(sender, instance, created, **kwargs): - """ - 处理开户申请的信号处理器 - 通过插件管理器调用相关插件 - """ - logger.info(f"[Post Save Signal] 处理开户申请: {instance.id}, 创建: {created}, 状态: {instance.status}") - - # 检查是否已处理过,避免无限循环 - if getattr(instance, '_plugin_processed', False): - return - - # 通过插件管理器获取所有可用的验证插件并执行验证 - plugin_manager = get_plugin_manager() - all_plugins = plugin_manager.get_all_plugins() - - validation_performed = False - validation_results = [] - - for plugin_id, plugin in all_plugins.items(): - if hasattr(plugin, 'validate_for_account_opening'): - try: - logger.info(f"[Post Save Signal] 使用插件 {plugin.name} 验证开户申请") - is_valid, reason = plugin.validate_for_account_opening( - account_request=instance - ) - - validation_performed = True - validation_results.append((plugin.name, is_valid, reason)) - - if not is_valid: - from django.contrib.auth import get_user_model - from django.utils import timezone - User = get_user_model() - - # 验证失败,自动拒绝申请 - instance.status = 'rejected' - instance.approval_notes = f'{plugin.name}验证失败:{reason}' - instance.approved_by = User.objects.filter(is_superuser=True).first() - instance.approval_date = timezone.now() - - # 标记已处理,避免无限循环 - instance._plugin_processed = True - instance.save(update_fields=['status', 'approval_notes', 'approved_by', 'approval_date']) - - logger.warning(f"[Post Save Signal] 申请 {instance.id} {plugin.name}验证失败,已拒绝:{reason}") - return # 一旦有任何验证失败就拒绝申请 - except Exception as e: - logger.error(f"[Post Save Signal] 插件 {plugin.name} 验证过程中出错: {str(e)}") - - if validation_performed: - logger.info(f"[Post Save Signal] 验证完成,结果: {validation_results}") - else: - logger.info(f"[Post Save Signal] 没有找到可执行验证的插件") - - -@receiver(post_save, sender=CloudComputerUser) -def handle_cloud_computer_user_update(sender, instance, **kwargs): - """ - 处理云电脑用户更新的信号处理器 - 通过插件管理器调用相关插件 - """ - logger.info(f"[Post Save Signal] 云电脑用户更新: {instance.username}, 状态: {instance.status}") - - # 通过插件管理器获取所有可用的验证插件并执行验证 - plugin_manager = get_plugin_manager() - all_plugins = plugin_manager.get_all_plugins() - - for plugin_id, plugin in all_plugins.items(): - if hasattr(plugin, 'validate_cloud_user'): - try: - logger.info(f"[Post Save Signal] 使用插件 {plugin.name} 验证云电脑用户") - is_valid, reason = plugin.validate_cloud_user( - cloud_user=instance - ) - - if not is_valid: - # 验证失败,根据插件策略处理用户 - if hasattr(plugin, 'should_disable_user_on_failure') and plugin.should_disable_user_on_failure(): - if instance.status != 'disabled': - instance.status = 'disabled' - instance.save(update_fields=['status']) - logger.warning(f"[Post Save Signal] {plugin.name}验证失败,禁用用户: {instance.username} - {reason}") - else: - logger.info(f"[Post Save Signal] {plugin.name}验证失败,但不执行禁用操作: {instance.username} - {reason}") - except Exception as e: - logger.error(f"[Post Save Signal] 插件 {plugin.name} 验证云用户时出错: {str(e)}") \ No newline at end of file diff --git a/plugins/templatetags/__init__.py b/plugins/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/templatetags/plugin_extensions.py b/plugins/templatetags/plugin_extensions.py deleted file mode 100644 index ecc04d4..0000000 --- a/plugins/templatetags/plugin_extensions.py +++ /dev/null @@ -1,91 +0,0 @@ -import logging - -from django import template -from django.utils.safestring import mark_safe - -from plugins.core.plugin_manager import get_plugin_manager - -register = template.Library() -logger = logging.getLogger(__name__) - - -@register.simple_tag(takes_context=True) -def plugin_extensions(context, slot): - """ - 在模板中渲染指定 slot 的所有插件 UI 扩展。 - - 用法: - {% load plugin_extensions %} - {% plugin_extensions "host_form_after_auth" %} - """ - pm = get_plugin_manager() - extensions = pm.get_ui_extensions(slot) - - if not extensions: - return '' - - request = context.get('request') - - parts = [] - for ext in extensions: - try: - rendered = ext.render(request=request) - if rendered: - parts.append(rendered) - except Exception as e: - logger.error( - f"渲染 UI 扩展失败 " - f"(slot={slot}, " - f"type={ext.extension_type}): {e}" - ) - - return mark_safe(''.join(parts)) - - -@register.simple_tag(takes_context=True) -def plugin_nav_items(context): - """ - 渲染所有插件注册的导航项扩展。 - - 用法: - {% load plugin_extensions %} - {% plugin_nav_items %} - """ - pm = get_plugin_manager() - extensions = pm.get_ui_extensions( - 'admin_sidebar_plugins' - ) - - if not extensions: - return '' - - request = context.get('request') - parts = [] - for ext in extensions: - try: - rendered = ext.render(request=request) - if rendered: - parts.append(rendered) - except Exception as e: - logger.error( - f"渲染导航扩展失败: {e}" - ) - - return mark_safe(''.join(parts)) - - -@register.simple_tag(takes_context=True) -def plugin_has_extensions(context, slot): - """ - 判断指定 slot 是否有插件注册了扩展。 - - 用法: - {% load plugin_extensions %} - {% plugin_has_extensions "host_form_after_auth" as has_ext %} - {% if has_ext %} - ... - {% endif %} - """ - pm = get_plugin_manager() - extensions = pm.get_ui_extensions(slot) - return len(extensions) > 0 diff --git a/plugins/test_demo_plugin/__init__.py b/plugins/test_demo_plugin/__init__.py deleted file mode 100644 index 3c07b3e..0000000 --- a/plugins/test_demo_plugin/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from plugins.core.base import PluginInterface - -class TestDemoPlugin(PluginInterface): - def __init__(self): - super().__init__( - plugin_id='test_demo', - name='Test Demo Plugin', - version='0.1.0', - description='A test plugin for verifying zip installation', - ) - - def initialize(self) -> bool: - return True - - def shutdown(self) -> bool: - return True diff --git a/plugins/tests.py b/plugins/tests.py deleted file mode 100644 index 3d0b104..0000000 --- a/plugins/tests.py +++ /dev/null @@ -1,58 +0,0 @@ -import pytest -from django.test import RequestFactory -from django.contrib.auth import get_user_model -from unittest.mock import MagicMock, patch - -User = get_user_model() - - -@pytest.mark.django_db -class TestPluginApiViewNoInfoLeak: - def setup_method(self): - self.factory = RequestFactory() - - def test_exception_returns_generic_error(self): - from plugins.django_integration import plugin_api_view - - plugin = MagicMock() - plugin.enabled = True - plugin.some_action.side_effect = RuntimeError("secret internal error") - - with patch("plugins.django_integration.get_plugin", return_value=plugin): - request = self.factory.post( - "/api/plugins/test/some_action", - data="{}", - content_type="application/json", - ) - response = plugin_api_view(request, "test", "some_action") - - assert response.status_code == 500 - assert b"Internal server error" in response.content - assert b"secret internal error" not in response.content - - -@pytest.mark.django_db -class TestPluginManagementApiNoInfoLeak: - def setup_method(self): - self.factory = RequestFactory() - - def test_exception_returns_generic_error(self): - from plugins.django_integration import plugin_management_api - - user = User.objects.create_user("testuser", password="testpass") - request = self.factory.post( - "/api/plugins/manage", - data='{"action":"enable","plugin_id":"nonexist"}', - content_type="application/json", - ) - request.user = user - - with patch( - "plugins.plugin_manager.PluginManager.enable_plugin", - side_effect=RuntimeError("db connection lost"), - ): - response = plugin_management_api(request) - - assert response.status_code == 500 - assert b"Internal server error" in response.content - assert b"db connection lost" not in response.content diff --git a/plugins/urls.py b/plugins/urls.py deleted file mode 100755 index 1a16967..0000000 --- a/plugins/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -插件系统URL配置 -""" - -from django.urls import path -from . import views - -app_name = 'plugins' - -urlpatterns = [ - # 插件管理相关视图 - path('', views.plugin_list, name='plugin_list'), - path('/', views.plugin_detail, name='plugin_detail'), - path('/toggle/', views.toggle_plugin, name='toggle_plugin'), - path('sync/', views.sync_plugins, name='sync_plugins'), -] \ No newline at end of file diff --git a/plugins/urls_admin.py b/plugins/urls_admin.py deleted file mode 100644 index 637600f..0000000 --- a/plugins/urls_admin.py +++ /dev/null @@ -1 +0,0 @@ -urlpatterns = [] diff --git a/plugins/urls_provider.py b/plugins/urls_provider.py deleted file mode 100644 index 637600f..0000000 --- a/plugins/urls_provider.py +++ /dev/null @@ -1 +0,0 @@ -urlpatterns = [] diff --git a/plugins/views.py b/plugins/views.py deleted file mode 100755 index 5767d5c..0000000 --- a/plugins/views.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -插件系统视图 -""" - -from django.shortcuts import render, get_object_or_404, redirect -from django.contrib import messages -from django.http import JsonResponse -from django.contrib.auth.decorators import login_required, user_passes_test -from django.views.decorators.http import require_POST -from .models import PluginRecord -from . import plugin_manager - - -@login_required -def plugin_list(request): - """ - 插件列表视图 - """ - plugins = PluginRecord.objects.all().order_by('-created_at') - context = { - 'plugins': plugins - } - return render(request, 'plugins/list.html', context) - - -@login_required -def plugin_detail(request, plugin_id): - """ - 插件详情视图 - """ - plugin_record = get_object_or_404(PluginRecord, plugin_id=plugin_id) - plugin_instance = plugin_manager.get_plugin(plugin_id) - - context = { - 'plugin_record': plugin_record, - 'plugin_instance': plugin_instance - } - return render(request, 'plugins/detail.html', context) - - -@user_passes_test(lambda u: u.is_staff) -@login_required -@require_POST -def toggle_plugin(request, plugin_id): - """ - 切换插件启用/禁用状态 - """ - try: - plugin_record = get_object_or_404(PluginRecord, plugin_id=plugin_id) - - # 切换状态 - new_status = not plugin_record.is_active - if new_status: - plugin_manager.enable_plugin(plugin_id) - messages.success(request, f'插件 "{plugin_record.name}" 已启用') - else: - plugin_manager.disable_plugin(plugin_id) - messages.warning(request, f'插件 "{plugin_record.name}" 已禁用') - - # 更新数据库记录 - plugin_record.is_active = new_status - plugin_record.save() - - return JsonResponse({ - 'success': True, - 'new_status': new_status, - 'message': f'插件状态已更新为 {"启用" if new_status else "禁用"}' - }) - except Exception as e: - logger = __import__('logging').getLogger(__name__) - logger.error(f"Error toggling plugin: {str(e)}", exc_info=True) - return JsonResponse({ - 'success': False, - 'error': '操作失败,请稍后重试' - }, status=400) - - -@user_passes_test(lambda u: u.is_staff) -@login_required -def sync_plugins(request): - """ - 同步插件状态视图 - """ - try: - # 从插件管理器同步插件到数据库 - for plugin in plugin_manager.get_all_plugins(): - plugin_record, created = PluginRecord.objects.get_or_create( - plugin_id=plugin.plugin_id, - defaults={ - 'name': plugin.name, - 'version': plugin.version, - 'description': plugin.description, - 'is_active': plugin.enabled - } - ) - - if not created: - # 更新现有记录 - plugin_record.name = plugin.name - plugin_record.version = plugin.version - plugin_record.description = plugin.description - plugin_record.is_active = plugin.enabled - plugin_record.save() - - messages.success(request, f'成功同步了 {len(plugin_manager.get_all_plugins())} 个插件') - return redirect('plugins:plugin_list') - except Exception as e: - messages.error(request, f'同步插件时出错: {str(e)}') - return redirect('plugins:plugin_list') \ No newline at end of file diff --git a/plugins/views_admin.py b/plugins/views_admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/views_provider.py b/plugins/views_provider.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index a23ce07..2c7bfb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,81 +1,69 @@ [project] name = "2c2a" -version = "1.0.0" -description = "2c2a - Django Web Application" -license = "AGPL-3.0-only" +version = "2.0.0" +description = "Zero Agent Security Control Architecture - async rewrite (Granian + FastAPI + SQLAlchemy 2.0 Async + HTMX OOB + RedisHuey + JinjaX + aiohttp)" +requires-python = ">=3.12" readme = "README.md" -requires-python = ">=3.10" +license = { text = "AGPL-3.0" } + dependencies = [ - "celery==5.4.0", - "cryptography==46.0.3", - "django-cors-headers==4.3.1", - "django-formtools>=2.5.1", - "Django==4.2.27", - "djangorestframework==3.15.2", - "idna==3.15", - "kombu==5.6.2", - "Markdown==3.10.1", - "pillow==12.1.0", - "PyJWT>=2.8.0", - "pyotp", - "python-dotenv==1.2.1", - "pywinrm==0.4.3", - "redis>=5.0.0", - "requests==2.32.3", - "toml", - "django-cotton @ git+https://github.com/2c2a/django-cotton.git@feature/x-prefix-tag-support", - "djlint>=1.36.4", - "heroicons>=2.14.0", - "cotton-icons>=0.2.0", - "whitenoise>=6.12.0", - "django-tianai-captcha", + # ASGI 服务器 + "granian>=1.6.0", + # Web 框架 + "fastapi>=0.115.0", + "starlette>=0.38.0", + # ORM (SQLAlchemy 2.0 异步) + "sqlalchemy[asyncio]>=2.0.32", + "aiosqlite>=0.20.0", + "asyncpg>=0.29.0", + "aiomysql>=0.2.0", + # 迁移 + "alembic>=1.13.0", + # 模板 (JinjaX) + "jinja2>=3.1.4", + "jinjax>=0.5.0", + # HTMX 服务端无直接依赖,仅前端 JS + # Redis (异步 + Huey) + "redis[hiredis]>=5.0.0", + "huey[redis]>=2.5.0", + # 异步 HTTP (替代同步 winrm) + "aiohttp>=3.10.0", + # 安全 / 加密 + "argon2-cffi>=23.1.0", + "pyjwt>=2.9.0", + "cryptography>=43.0.0", + # 配置 + "pydantic>=2.9.0", + "pydantic-settings>=2.5.0", + # 工具 + "python-multipart>=0.0.9", + "itsdangerous>=2.2.0", + "httpx>=0.27.0", + "structlog>=24.4.0", + "python-dotenv>=1.0.1", ] [project.optional-dependencies] -kerberos = [ - "gssapi>=1.11.1", - "krb5>=0.9.0", +dev = [ + "pytest>=8.3.0", + "pytest-asyncio>=0.24.0", + "httpx>=0.27.0", + "ruff>=0.6.0", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" -[dependency-groups] -dev = [ - "pytest>=9.0.3", - "pytest-django", - "black", - "django-stubs>=6.0.2", - "flake8", - "pyrefly>=0.60.0", - "pytest", - "pytest-django", - "redis>=7.4.0", -] - -[tool.hatch.metadata] -allow-direct-references = true - [tool.hatch.build.targets.wheel] -packages = ["."] - -[tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "config.settings" -pythonpath = ["."] -python_files = ["tests.py", "test_*.py", "*_tests.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -addopts = "-v --tb=short --import-mode=importlib" +packages = ["app"] -[tool.pyright] -venvPath = "." -venv = ".venv" -pythonVersion = "3.13" -typeCheckingMode = "basic" +[tool.ruff] +target-version = "py312" +line-length = 110 -[tool.pyrefly] -python-version = "3.13" +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] -[tool.uv.sources] -django-tianai-captcha = { git = "https://github.com/trustedinster/django-tianai-captcha.git" } +[tool.granian] +# 仅文档用,实际参数由命令行传入 diff --git a/scripts/deploy.py b/scripts/deploy.py deleted file mode 100755 index 2039406..0000000 --- a/scripts/deploy.py +++ /dev/null @@ -1,1553 +0,0 @@ -#!/usr/bin/env python3 -""" -2c2a 交互式部署脚本 -根据部署环境动态生成 pyproject.toml,仅安装所需依赖,避免冗余库 - -用法: - python3 scripts/deploy.py -""" - -import sys -import shutil -import subprocess -import curses -from pathlib import Path -import secrets -import string -from datetime import datetime - -BASE_DIR = Path(__file__).resolve().parent.parent - -DEPS_CORE = { - "Django": "Django==4.2.27", - "djangorestframework": "djangorestframework==3.15.2", - "django-cors-headers": "django-cors-headers==4.3.1", - "django-cotton": "django-cotton @ git+https://github.com/2c2a/django-cotton.git@feature/x-prefix-tag-support", - "django-formtools": "django-formtools>=2.5.1", - "python-dotenv": "python-dotenv==1.2.1", - "PyJWT": "PyJWT>=2.8.0", - "cryptography": "cryptography==46.0.3", - "requests": "requests==2.32.3", - "pillow": "pillow==12.1.0", - "toml": "toml", -} - -DEPS_CELERY = { - "celery": "celery==5.4.0", -} - -DEPS_MYSQL = { - "pymysql": "pymysql>=1.1.2", -} - -DEPS_POSTGRESQL = { - "psycopg": "psycopg[binary]>=3.0", -} - -DEPS_REDIS = { - "redis": "redis>=5.0.0", -} - -DEPS_SQLITE_BROKER = { - "sqlalchemy": "sqlalchemy>=2.0.0", -} - -DEPS_WINRM = { - "pywinrm": "pywinrm==0.4.3", -} - -DEPS_KERBEROS = { - "gssapi": "gssapi>=1.11.1", - "krb5": "krb5>=0.9.0", -} - -DEPS_MARKDOWN = { - "Markdown": "Markdown==3.10.1", -} - -DEPS_2FA = { - "pyotp": "pyotp", -} - -DEPS_DEV = { - "pytest": "pytest", - "pytest-django": "pytest-django", - "black": "black", - "flake8": "flake8", - "django-stubs": "django-stubs>=6.0.2", - "pyrefly": "pyrefly>=0.60.0", -} - -DB_OPTIONS = [ - ("sqlite", "SQLite", "零配置本地文件数据库,适合开发/小规模部署"), - ("mysql", "MySQL / MariaDB", "生产级关系数据库,适合中大规模部署"), - ("postgresql", "PostgreSQL", "生产级关系数据库,功能最丰富"), -] - -FEATURE_OPTIONS = [ - ("celery", "Celery 异步任务队列", "后台任务处理(主机操作、工单通知等)", True), - ("redis", "Redis 缓存/消息队列", "高性能缓存、会话存储、Celery Broker", False), - ("winrm", "WinRM Windows 远程管理", "远程管理 Windows 服务器", False), - ("kerberos", "Kerberos 域认证", "Windows 域环境集成认证", False), - ("markdown", "Markdown 渲染", "仪表盘/工单中的 Markdown 内容渲染", True), - ("2fa", "双因素认证 (TOTP)", "基于时间的一次性密码二次验证", True), -] - -DEV_OPTION = ("dev", "开发/测试工具", "pytest, black, flake8, django-stubs 等", False) - -C_TITLE = 1 -C_SUBTITLE = 2 -C_HIGHLIGHT = 3 -C_SELECTED = 4 -C_UNSELECTED = 5 -C_RADIO_ON = 6 -C_RADIO_OFF = 7 -C_CHECK_ON = 8 -C_CHECK_OFF = 9 -C_DESC = 10 -C_HINT = 11 -C_BORDER = 12 -C_SUCCESS = 13 -C_WARN = 14 -C_ERROR = 15 -C_DEP = 16 -C_DEP_DEV = 17 -C_BANNER = 18 - - -def _safe_addstr(stdscr, y, x, text, attr=0): - max_y, max_x = stdscr.getmaxyx() - if y < 0 or y >= max_y or x < 0 or x >= max_x: - return - available = max_x - x - 1 - if available <= 0: - return - text = text[:available] - try: - stdscr.addstr(y, x, text, attr) - except curses.error: - pass - - -def _init_colors(): - curses.start_color() - curses.use_default_colors() - curses.init_pair(C_TITLE, curses.COLOR_CYAN, -1) - curses.init_pair(C_SUBTITLE, curses.COLOR_WHITE, -1) - curses.init_pair(C_HIGHLIGHT, curses.COLOR_BLACK, curses.COLOR_CYAN) - curses.init_pair(C_SELECTED, curses.COLOR_CYAN, -1) - curses.init_pair(C_UNSELECTED, curses.COLOR_WHITE, -1) - curses.init_pair(C_RADIO_ON, curses.COLOR_GREEN, -1) - curses.init_pair(C_RADIO_OFF, curses.COLOR_WHITE, -1) - curses.init_pair(C_CHECK_ON, curses.COLOR_GREEN, -1) - curses.init_pair(C_CHECK_OFF, curses.COLOR_WHITE, -1) - curses.init_pair(C_DESC, 8, -1) - curses.init_pair(C_HINT, curses.COLOR_YELLOW, -1) - curses.init_pair(C_BORDER, curses.COLOR_CYAN, -1) - curses.init_pair(C_SUCCESS, curses.COLOR_GREEN, -1) - curses.init_pair(C_WARN, curses.COLOR_YELLOW, -1) - curses.init_pair(C_ERROR, curses.COLOR_RED, -1) - curses.init_pair(C_DEP, curses.COLOR_GREEN, -1) - curses.init_pair(C_DEP_DEV, curses.COLOR_YELLOW, -1) - curses.init_pair(C_BANNER, curses.COLOR_CYAN, -1) - - -def _draw_banner(stdscr, y, max_x): - title = " ZASCA 交互式部署管理脚本 " - x = max(0, (max_x - len(title)) // 2) - _safe_addstr(stdscr, y, x, title, curses.color_pair(C_BANNER) | curses.A_BOLD) - - -def _draw_box(stdscr, y, x, h, w): - _safe_addstr(stdscr, y, x, "┌" + "─" * (w - 2) + "┐", curses.color_pair(C_BORDER)) - for i in range(1, h - 1): - _safe_addstr(stdscr, y + i, x, "│", curses.color_pair(C_BORDER)) - _safe_addstr(stdscr, y + i, x + w - 1, "│", curses.color_pair(C_BORDER)) - _safe_addstr(stdscr, y + h - 1, x, "└" + "─" * (w - 2) + "┘", curses.color_pair(C_BORDER)) - - -def _draw_radio_item(stdscr, y, x, selected, active, label, desc, max_w): - if selected: - marker = "◉" - m_color = C_RADIO_ON | curses.A_BOLD - else: - marker = "○" - m_color = C_RADIO_OFF - - if active: - bg = curses.color_pair(C_HIGHLIGHT) | curses.A_BOLD - _safe_addstr(stdscr, y, x, f" {marker} {label}", bg) - else: - _safe_addstr(stdscr, y, x, " ", curses.color_pair(C_UNSELECTED)) - _safe_addstr(stdscr, y, x + 2, marker, curses.color_pair(m_color)) - _safe_addstr(stdscr, y, x + 4, label, curses.color_pair(C_SELECTED if selected else C_UNSELECTED) | curses.A_BOLD) - - if desc: - desc_x = x + 6 - remaining = max_w - (desc_x - x) - 1 - if remaining > 3: - _safe_addstr(stdscr, y + 1, desc_x, desc[:remaining], curses.color_pair(C_DESC) if not active else curses.color_pair(C_HIGHLIGHT)) - - -def _draw_check_item(stdscr, y, x, checked, active, label, desc, max_w): - if checked: - marker = "☑" - m_color = C_CHECK_ON | curses.A_BOLD - else: - marker = "☐" - m_color = C_CHECK_OFF - - if active: - bg = curses.color_pair(C_HIGHLIGHT) | curses.A_BOLD - _safe_addstr(stdscr, y, x, f" {marker} {label}", bg) - else: - _safe_addstr(stdscr, y, x, " ", curses.color_pair(C_UNSELECTED)) - _safe_addstr(stdscr, y, x + 2, marker, curses.color_pair(m_color)) - _safe_addstr(stdscr, y, x + 4, label, curses.color_pair(C_SELECTED if checked else C_UNSELECTED) | curses.A_BOLD) - - if desc: - desc_x = x + 6 - remaining = max_w - (desc_x - x) - 1 - if remaining > 3: - _safe_addstr(stdscr, y + 1, desc_x, desc[:remaining], curses.color_pair(C_DESC) if not active else curses.color_pair(C_HIGHLIGHT)) - - -def _draw_hint(stdscr, y, x, hint_text): - _safe_addstr(stdscr, y, x, hint_text, curses.color_pair(C_HINT)) - - -def _page_db(stdscr, selected): - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - box_w = min(72, max_x - 4) - box_h = 5 + len(DB_OPTIONS) * 3 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = " 数据库引擎 (单选) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - for i, (key, label, desc) in enumerate(DB_OPTIONS): - iy = box_y + 2 + i * 3 - _draw_radio_item(stdscr, iy, box_x + 2, i == selected, i == selected, label, desc, box_w - 4) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "↑↓ 移动 Enter 确认 Esc 退出") - - stdscr.refresh() - - -def _select_db(stdscr): - selected = 0 - while True: - _page_db(stdscr, selected) - key = stdscr.getch() - if key == curses.KEY_UP: - selected = (selected - 1) % len(DB_OPTIONS) - elif key == curses.KEY_DOWN: - selected = (selected + 1) % len(DB_OPTIONS) - elif key in (curses.KEY_ENTER, 10, 13): - return DB_OPTIONS[selected][0] - elif key == 27: - return None - elif key in (ord('1'), ord('2'), ord('3')): - idx = key - ord('1') - if 0 <= idx < len(DB_OPTIONS): - return DB_OPTIONS[idx][0] - - -def _page_features(stdscr, cursor, features_state, dev_state, page): - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - if page == 0: - items = FEATURE_OPTIONS - total = len(items) - box_w = min(76, max_x - 4) - box_h = 5 + total * 3 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = " 功能模块 (多选) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - for i, (key, label, desc, default) in enumerate(items): - iy = box_y + 2 + i * 3 - _draw_check_item(stdscr, iy, box_x + 2, features_state[key], i == cursor, label, desc, box_w - 4) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "↑↓ 移动 Space 切换 Enter 下一步 Esc 返回") - else: - box_w = min(76, max_x - 4) - box_h = 7 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = " 开发环境 " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - key, label, desc, default = DEV_OPTION - iy = box_y + 2 - _draw_check_item(stdscr, iy, box_x + 2, dev_state, True, label, desc, box_w - 4) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "Space 切换 Enter 确认 Esc 返回") - - stdscr.refresh() - - -def _select_features(stdscr): - features_state = {key: default for key, _, _, default in FEATURE_OPTIONS} - dev_state = DEV_OPTION[3] - cursor = 0 - page = 0 - - while True: - _page_features(stdscr, cursor, features_state, dev_state, page) - key = stdscr.getch() - - if page == 0: - if key == curses.KEY_UP: - cursor = (cursor - 1) % len(FEATURE_OPTIONS) - elif key == curses.KEY_DOWN: - cursor = (cursor + 1) % len(FEATURE_OPTIONS) - elif key == ord(' '): - fk = FEATURE_OPTIONS[cursor][0] - features_state[fk] = not features_state[fk] - elif key in (curses.KEY_ENTER, 10, 13): - page = 1 - cursor = 0 - elif key == 27: - return None - else: - if key == ord(' '): - dev_state = not dev_state - elif key in (curses.KEY_ENTER, 10, 13): - return features_state, dev_state - elif key == 27: - page = 0 - cursor = 0 - - -def _page_summary(stdscr, answers, deps, dev_deps, cursor, scroll=0): - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - db_names = {"sqlite": "SQLite", "mysql": "MySQL / MariaDB", "postgresql": "PostgreSQL"} - feature_names = { - "celery": "Celery", - "redis": "Redis", - "winrm": "WinRM", - "kerberos": "Kerberos", - "markdown": "Markdown", - "2fa": "2FA", - } - - lines = [] - lines.append(("title", " 部署摘要 ")) - lines.append(("blank", "")) - lines.append(("kv", f" 数据库: {db_names[answers['db']]}")) - - active_features = [feature_names[k] for k in ("celery", "redis", "winrm", "kerberos", "markdown", "2fa") if answers.get(k)] - if active_features: - lines.append(("kv", f" 功能模块: {', '.join(active_features)}")) - else: - lines.append(("kv", " 功能模块: 无(仅核心框架)")) - - if answers.get("dev"): - lines.append(("kv", " 开发工具: 已启用")) - else: - lines.append(("kv", " 开发工具: 未启用")) - - lines.append(("blank", "")) - lines.append(("dep_title", f" 生产依赖 ({len(deps)} 个):")) - for name, spec in deps.items(): - lines.append(("dep", f" + {spec}")) - - if dev_deps: - lines.append(("blank", "")) - lines.append(("dep_dev_title", f" 开发依赖 ({len(dev_deps)} 个):")) - for name, spec in dev_deps.items(): - lines.append(("dep_dev", f" + {spec}")) - - total = len(deps) + len(dev_deps) - lines.append(("blank", "")) - lines.append(("total", f" 合计: {total} 个直接依赖(传递依赖由 uv 自动解析)")) - - box_w = min(76, max_x - 4) - box_h = min(len(lines) + 4, max_y - 6) - box_x = max(0, (max_x - box_w) // 2) - box_y = 1 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - max_visible = box_h - 3 - total_lines = len(lines) - max_scroll = max(0, total_lines - max_visible) - scroll = min(scroll, max_scroll) - scroll = max(0, scroll) - - visible = lines[scroll:scroll + max_visible] - for i, (kind, text) in enumerate(visible): - ry = box_y + 1 + i - rx = box_x + 2 - remaining = box_w - 5 - t = text[:remaining] - - if kind == "title": - tx = box_x + max(0, (box_w - len(t)) // 2) - _safe_addstr(stdscr, ry, tx, t, curses.color_pair(C_TITLE) | curses.A_BOLD) - elif kind == "blank": - pass - elif kind == "kv": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_SUBTITLE) | curses.A_BOLD) - elif kind == "dep_title": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_DEP) | curses.A_BOLD) - elif kind == "dep": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_DEP)) - elif kind == "dep_dev_title": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_DEP_DEV) | curses.A_BOLD) - elif kind == "dep_dev": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_DEP_DEV)) - elif kind == "total": - _safe_addstr(stdscr, ry, rx, t, curses.color_pair(C_TITLE) | curses.A_BOLD) - - if max_scroll > 0: - scroll_info = f" [{scroll + 1}-{min(scroll + max_visible, total_lines)}/{total_lines}] " - _safe_addstr(stdscr, box_y + box_h - 2, box_x + box_w - len(scroll_info) - 2, scroll_info, curses.color_pair(C_DESC)) - - btn_y = box_y + box_h + 1 - btn_labels = ["[✔ 确认生成]", "[✘ 取消]"] - btn_x = box_x + 4 - for i, label in enumerate(btn_labels): - if i == cursor: - _safe_addstr(stdscr, btn_y, btn_x, label, curses.color_pair(C_HIGHLIGHT) | curses.A_BOLD) - else: - _safe_addstr(stdscr, btn_y, btn_x, label, curses.color_pair(C_UNSELECTED)) - btn_x += len(label) + 3 - - hint_y = btn_y + 2 - hint = "←→ 选择 Enter 确认 Esc 返回" - if max_scroll > 0: - hint = "↑↓ 滚动 " + hint - _draw_hint(stdscr, hint_y, box_x + 2, hint) - - stdscr.refresh() - return scroll - - -def _confirm_summary(stdscr, answers, deps, dev_deps): - cursor = 0 - scroll = 0 - while True: - scroll = _page_summary(stdscr, answers, deps, dev_deps, cursor, scroll) - key = stdscr.getch() - if key == curses.KEY_LEFT: - cursor = 0 - elif key == curses.KEY_RIGHT: - cursor = 1 - elif key == curses.KEY_UP: - scroll = max(0, scroll - 1) - elif key == curses.KEY_DOWN: - scroll += 1 - elif key in (curses.KEY_ENTER, 10, 13): - return cursor == 0 - elif key == 27: - return None - elif key == ord('1'): - return True - elif key == ord('2'): - return False - - -def _page_progress(stdscr, step, total_steps, message): - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - _draw_banner(stdscr, 0, max_x) - - box_w = min(60, max_x - 4) - box_h = 5 - box_x = max(0, (max_x - box_w) // 2) - box_y = 3 - - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" Step {step}/{total_steps} " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - msg_x = box_x + 3 - _safe_addstr(stdscr, box_y + 2, msg_x, message[:box_w - 6], curses.color_pair(C_SUBTITLE)) - - stdscr.refresh() - - -def _page_result(stdscr, success, answers, backup_name=None, env_configured=False, env_backup_name=None, migrate_success=False): - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - _draw_banner(stdscr, 0, max_x) - - box_w = min(72, max_x - 4) - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - if success: - title = " ✔ 部署完成 " - steps = [] - n = 1 - if env_configured: - steps.append(f"{n}. .env 已配置 (可手动编辑调整)") - else: - steps.append(f"{n}. 配置 .env 文件 (参考 .env.example)") - n += 1 - if migrate_success: - steps.append(f"{n}. 数据库迁移 已完成") - else: - steps.append(f"{n}. 初始化数据库 uv run python manage.py migrate") - n += 1 - steps.append(f"{n}. 创建管理员 uv run python manage.py createsuperuser") - n += 1 - steps.append(f"{n}. 启动服务 uv run python manage.py runserver") - if answers.get("celery"): - n += 1 - steps.append(f"{n}. 启动 Celery uv run celery -A config worker -l info") - - box_h = 4 + len(steps) + (2 if not answers.get("redis") and answers.get("celery") else 0) + (1 if backup_name else 0) + (1 if env_backup_name else 0) - box_h = max(box_h, 8) - - _draw_box(stdscr, box_y, box_x, box_h, box_w) - _safe_addstr(stdscr, box_y, box_x + max(0, (box_w - len(title)) // 2), title, curses.color_pair(C_SUCCESS) | curses.A_BOLD) - - for i, step in enumerate(steps): - ry = box_y + 2 + i - _safe_addstr(stdscr, ry, box_x + 3, step[:box_w - 6], curses.color_pair(C_SUBTITLE)) - - row = box_y + 2 + len(steps) - if not answers.get("redis") and answers.get("celery"): - _safe_addstr(stdscr, row, box_x + 3, "⚠ Celery 使用 SQLite Broker,生产环境建议启用 Redis", curses.color_pair(C_WARN)) - row += 2 - - if backup_name: - _safe_addstr(stdscr, row, box_x + 3, f"备份: {backup_name}", curses.color_pair(C_DESC)) - row += 1 - if env_backup_name: - _safe_addstr(stdscr, row, box_x + 3, f".env 备份: {env_backup_name}", curses.color_pair(C_DESC)) - else: - box_h = 8 - _draw_box(stdscr, box_y, box_x, box_h, box_w) - title = " ✘ 部署失败 " - _safe_addstr(stdscr, box_y, box_x + max(0, (box_w - len(title)) // 2), title, curses.color_pair(C_ERROR) | curses.A_BOLD) - _safe_addstr(stdscr, box_y + 2, box_x + 3, "依赖同步失败,请检查错误信息", curses.color_pair(C_ERROR)) - if backup_name: - _safe_addstr(stdscr, box_y + 4, box_x + 3, f"恢复: cp {backup_name} pyproject.toml", curses.color_pair(C_WARN)) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "按任意键退出") - - stdscr.refresh() - stdscr.getch() - - -def _tui_main(stdscr): - curses.curs_set(0) - curses.noecho() - stdscr.keypad(True) - _init_colors() - - db = _select_db(stdscr) - if db is None: - return - - result = _select_features(stdscr) - if result is None: - return - features_state, dev_state = result - - answers = {"db": db} - answers.update(features_state) - answers["dev"] = dev_state - - deps, dev_deps = compute_dependencies(answers) - - confirmed = _confirm_summary(stdscr, answers, deps, dev_deps) - if confirmed is None or not confirmed: - return - - toml_path = BASE_DIR / "pyproject.toml" - backup = backup_file(toml_path) - backup_name = backup.name if backup else None - - _page_progress(stdscr, 1, 3, "正在生成 pyproject.toml ...") - content = generate_pyproject_toml(answers, deps, dev_deps) - toml_path.write_text(content, encoding="utf-8") - - _page_progress(stdscr, 2, 3, "正在执行 uv sync ...") - try: - proc = subprocess.run( - ["uv", "sync"], - cwd=BASE_DIR, - capture_output=True, - text=True, - timeout=300, - ) - success = proc.returncode == 0 - except FileNotFoundError: - success = False - except subprocess.TimeoutExpired: - success = False - - env_configured = False - env_backup_name = None - migrate_success = False - if success: - env_values = _configure_env(stdscr, answers) - if env_values is not None: - env_path = BASE_DIR / ".env" - backup_env = backup_file(env_path) - env_backup_name = backup_env.name if backup_env else None - env_content = generate_env_content(answers, env_values) - env_path.write_text(env_content, encoding="utf-8") - env_configured = True - - _page_progress(stdscr, 3, 3, "正在执行数据库迁移 ...") - try: - migrate_proc = subprocess.run( - ["uv", "run", "python", "manage.py", "migrate"], - cwd=BASE_DIR, - capture_output=True, - text=True, - timeout=120, - ) - migrate_success = migrate_proc.returncode == 0 - except FileNotFoundError: - migrate_success = False - except subprocess.TimeoutExpired: - migrate_success = False - - _page_result(stdscr, success, answers, backup_name, env_configured, env_backup_name, migrate_success) - - -def _generate_secret(length=50): - alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_=+" - return ''.join(secrets.choice(alphabet) for _ in range(length)) - - -def _load_existing_env(env_path): - values = {} - if env_path.exists(): - for line in env_path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line or line.startswith('#'): - continue - if '=' in line: - key, _, val = line.partition('=') - values[key.strip()] = val.strip() - return values - - -def _get_env_items(answers): - db = answers.get("db", "sqlite") - db_port_default = {"mysql": "3306", "postgresql": "5432"}.get(db, "3306") - db_user_default = {"mysql": "root", "postgresql": "postgres"}.get(db, "root") - - items = [] - - items.append(("group", "核心配置")) - items.append(("field", {"key": "DEBUG", "default": "True", "desc": "调试模式(生产环境必须 False)", "type": "bool"})) - items.append(("field", {"key": "DJANGO_SECRET_KEY", "default": "", "desc": "Django 密钥", "type": "secret"})) - items.append(("field", {"key": "ALLOWED_HOSTS", "default": "localhost,127.0.0.1", "desc": "允许访问的主机", "type": "list"})) - items.append(("field", {"key": "CSRF_TRUSTED_ORIGINS", "default": "https://localhost,https://127.0.0.1", "desc": "CSRF 可信来源", "type": "list"})) - - items.append(("group", "数据库配置")) - items.append(("field", {"key": "DB_ENGINE", "default": db, "desc": "数据库引擎(根据之前的选择自动设置)", "type": "auto"})) - if db != "sqlite": - items.append(("field", {"key": "DB_HOST", "default": "127.0.0.1", "desc": "数据库主机", "type": "text"})) - items.append(("field", {"key": "DB_PORT", "default": db_port_default, "desc": "数据库端口", "type": "text"})) - items.append(("field", {"key": "DB_NAME", "default": "zasca", "desc": "数据库名称", "type": "text"})) - items.append(("field", {"key": "DB_USER", "default": db_user_default, "desc": "数据库用户", "type": "text"})) - items.append(("field", {"key": "DB_PASSWORD", "default": "", "desc": "数据库密码", "type": "secret"})) - - if answers.get("redis"): - items.append(("group", "Redis 配置")) - items.append(("field", {"key": "REDIS_URL", "default": "redis://localhost:6379/0", "desc": "Redis 连接地址", "type": "text"})) - - if answers.get("celery"): - items.append(("group", "Celery 配置")) - items.append(("field", {"key": "CELERY_BROKER_URL", "default": "", "desc": "Celery Broker(留空自动选择)", "type": "text"})) - items.append(("field", {"key": "CELERY_RESULT_BACKEND", "default": "", "desc": "Celery 结果后端(留空自动选择)", "type": "text"})) - - items.append(("group", "演示模式")) - items.append(("field", {"key": "ZASCA_DEMO", "default": "0", "desc": "演示模式(1=启用)", "type": "text"})) - - items.append(("group", "安全配置")) - items.append(("field", {"key": "SECURE_SSL_REDIRECT", "default": "False", "desc": "SSL 重定向", "type": "bool"})) - items.append(("field", {"key": "SESSION_COOKIE_SECURE", "default": "False", "desc": "会话 Cookie 安全", "type": "bool"})) - items.append(("field", {"key": "CSRF_COOKIE_SECURE", "default": "False", "desc": "CSRF Cookie 安全", "type": "bool"})) - - items.append(("group", "日志配置")) - items.append(("field", {"key": "LOG_LEVEL", "default": "DEBUG", "desc": "日志级别", "type": "text"})) - items.append(("field", {"key": "LOG_FILE", "default": "/var/log/2c2a/application.log", "desc": "日志文件路径", "type": "text"})) - - if answers.get("winrm"): - items.append(("group", "WinRM 配置")) - items.append(("field", {"key": "WINRM_TIMEOUT", "default": "30", "desc": "WinRM 超时(秒)", "type": "text"})) - items.append(("field", {"key": "WINRM_RETRY_COUNT", "default": "3", "desc": "WinRM 重试次数", "type": "text"})) - - items.append(("group", "Gateway 配置")) - items.append(("field", {"key": "GATEWAY_ENABLED", "default": "False", "desc": "Gateway 开关", "type": "bool"})) - items.append(("field", {"key": "GATEWAY_CONTROL_SOCKET", "default": "/run/2c2a/control.sock", "desc": "Gateway 控制套接字", "type": "text"})) - - items.append(("group", "Beta 数据库配置(可选)")) - items.append(("field", {"key": "BETA_DB_NAME", "default": "", "desc": "Beta 数据库名称(留空跳过)", "type": "text"})) - items.append(("field", {"key": "BETA_DB_USER", "default": "", "desc": "Beta 数据库用户", "type": "text"})) - items.append(("field", {"key": "BETA_DB_PASSWORD", "default": "", "desc": "Beta 数据库密码", "type": "secret"})) - items.append(("field", {"key": "BETA_DB_HOST", "default": "", "desc": "Beta 数据库主机", "type": "text"})) - items.append(("field", {"key": "BETA_DB_PORT", "default": "", "desc": "Beta 数据库端口", "type": "text"})) - - items.append(("group", "Bootstrap 认证配置")) - items.append(("field", {"key": "BOOTSTRAP_SHARED_SALT", "default": "", "desc": "Bootstrap 共享盐值", "type": "secret"})) - - return items - - -def _step_bool(stdscr, field_data, current_val, step, total): - key = field_data["key"] - desc = field_data.get("desc", "") - selected = 0 if current_val == "True" else 1 - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(72, max_x - 4) - box_h = 10 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" 环境变量配置 ({step}/{total}) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - ry = box_y + 2 - _safe_addstr(stdscr, ry, box_x + 4, key, curses.color_pair(C_SELECTED) | curses.A_BOLD) - - ry += 1 - if desc: - _safe_addstr(stdscr, ry, box_x + 4, desc[:box_w - 8], curses.color_pair(C_DESC)) - - ry += 2 - options = [("True", "启用"), ("False", "禁用")] - for i, (val, label) in enumerate(options): - is_sel = (i == selected) - marker = "◉" if is_sel else "○" - m_color = C_RADIO_ON | curses.A_BOLD if is_sel else C_RADIO_OFF - ox = box_x + 6 + i * 22 - _safe_addstr(stdscr, ry, ox, marker, curses.color_pair(m_color)) - _safe_addstr(stdscr, ry, ox + 2, f" {label} ({val})", curses.color_pair(C_SELECTED if is_sel else C_UNSELECTED) | curses.A_BOLD) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "←→/Space 切换 Enter 确认 Esc 返回") - - stdscr.refresh() - - ch = stdscr.getch() - if ch == curses.KEY_LEFT: - selected = 0 - elif ch == curses.KEY_RIGHT: - selected = 1 - elif ch == ord(' '): - selected = 1 - selected - elif ch in (curses.KEY_ENTER, 10, 13): - return "True" if selected == 0 else "False" - elif ch == 27: - return None - - -def _step_secret(stdscr, field_data, current_val, is_auto, step, total): - key = field_data["key"] - desc = field_data.get("desc", "") - - if is_auto or not current_val: - radio = 0 - else: - radio = 1 - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(72, max_x - 4) - box_h = 12 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" 环境变量配置 ({step}/{total}) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - ry = box_y + 2 - _safe_addstr(stdscr, ry, box_x + 4, key, curses.color_pair(C_SELECTED) | curses.A_BOLD) - - ry += 1 - if desc: - _safe_addstr(stdscr, ry, box_x + 4, desc[:box_w - 8], curses.color_pair(C_DESC)) - - ry += 2 - is_sel0 = (radio == 0) - marker0 = "◉" if is_sel0 else "○" - m_color0 = C_RADIO_ON | curses.A_BOLD if is_sel0 else C_RADIO_OFF - _safe_addstr(stdscr, ry, box_x + 4, marker0, curses.color_pair(m_color0)) - _safe_addstr(stdscr, ry, box_x + 7, "随机生成(推荐)", curses.color_pair(C_SELECTED if is_sel0 else C_UNSELECTED) | curses.A_BOLD) - - ry += 1 - sub_desc = "自动生成包含字母、数字和符号的50位密钥" - _safe_addstr(stdscr, ry, box_x + 7, sub_desc[:box_w - 10], curses.color_pair(C_DESC)) - - if is_sel0 and is_auto and current_val: - ry += 1 - preview = current_val[:8] + "..." + current_val[-4:] if len(current_val) > 12 else current_val - _safe_addstr(stdscr, ry, box_x + 7, f"预览: {preview}", curses.color_pair(C_DESC)) - - ry += 2 - is_sel1 = (radio == 1) - marker1 = "◉" if is_sel1 else "○" - m_color1 = C_RADIO_ON | curses.A_BOLD if is_sel1 else C_RADIO_OFF - _safe_addstr(stdscr, ry, box_x + 4, marker1, curses.color_pair(m_color1)) - _safe_addstr(stdscr, ry, box_x + 7, "手动输入", curses.color_pair(C_SELECTED if is_sel1 else C_UNSELECTED) | curses.A_BOLD) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "↑↓ 选择 Enter 确认 Esc 返回") - - stdscr.refresh() - - ch = stdscr.getch() - if ch == curses.KEY_UP: - radio = 0 - elif ch == curses.KEY_DOWN: - radio = 1 - elif ch in (curses.KEY_ENTER, 10, 13): - if radio == 0: - if not current_val or not is_auto: - current_val = _generate_secret(50) - return current_val, True - else: - result = _step_text(stdscr, field_data, current_val, step, total) - if result is not None: - return result, False - continue - elif ch == 27: - return None, None - - -def _step_text(stdscr, field_data, current_val, step, total): - key = field_data["key"] - desc = field_data.get("desc", "") - curses.curs_set(1) - buf = list(current_val) - pos = len(buf) - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(72, max_x - 4) - box_h = 10 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" 环境变量配置 ({step}/{total}) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - ry = box_y + 2 - _safe_addstr(stdscr, ry, box_x + 4, key, curses.color_pair(C_SELECTED) | curses.A_BOLD) - - ry += 1 - if desc: - _safe_addstr(stdscr, ry, box_x + 4, desc[:box_w - 8], curses.color_pair(C_DESC)) - - ry += 2 - input_x = box_x + 4 - input_w = box_w - 8 - _safe_addstr(stdscr, ry, input_x, " " * input_w, curses.color_pair(C_HIGHLIGHT)) - - text = "".join(buf) - if len(text) > input_w: - start = max(0, pos - input_w + 1) - visible_text = text[start:start + input_w] - cursor_offset = pos - start - else: - visible_text = text - cursor_offset = pos - - _safe_addstr(stdscr, ry, input_x, visible_text[:input_w], curses.color_pair(C_HIGHLIGHT)) - - try: - stdscr.move(ry, input_x + min(cursor_offset, input_w - 1)) - except curses.error: - # Cursor move can fail when terminal is resized or too small; ignore and continue rendering. - pass - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "Enter 确认 Esc 返回 Ctrl+U 清空") - - stdscr.refresh() - - ch = stdscr.getch() - if ch in (curses.KEY_ENTER, 10, 13): - curses.curs_set(0) - return "".join(buf) - elif ch == 27: - curses.curs_set(0) - return None - elif ch in (curses.KEY_BACKSPACE, 127, 8): - if pos > 0: - buf.pop(pos - 1) - pos -= 1 - elif ch == curses.KEY_DC: - if pos < len(buf): - buf.pop(pos) - elif ch == curses.KEY_LEFT: - if pos > 0: - pos -= 1 - elif ch == curses.KEY_RIGHT: - if pos < len(buf): - pos += 1 - elif ch == curses.KEY_HOME: - pos = 0 - elif ch == curses.KEY_END: - pos = len(buf) - elif ch == 21: - buf.clear() - pos = 0 - elif 32 <= ch < 127: - buf.insert(pos, chr(ch)) - pos += 1 - - -def _step_auto(stdscr, field_data, current_val, step, total): - key = field_data["key"] - desc = field_data.get("desc", "") - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(72, max_x - 4) - box_h = 10 - box_x = max(0, (max_x - box_w) // 2) - box_y = 2 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" 环境变量配置 ({step}/{total}) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - ry = box_y + 2 - _safe_addstr(stdscr, ry, box_x + 4, key, curses.color_pair(C_SELECTED) | curses.A_BOLD) - - ry += 1 - if desc: - _safe_addstr(stdscr, ry, box_x + 4, desc[:box_w - 8], curses.color_pair(C_DESC)) - - ry += 2 - _safe_addstr(stdscr, ry, box_x + 4, f"▸ {current_val}", curses.color_pair(C_SUCCESS) | curses.A_BOLD) - - ry += 1 - _safe_addstr(stdscr, ry, box_x + 4, "(此值由之前的选择自动确定)", curses.color_pair(C_DESC)) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "Enter 继续 Esc 返回") - - stdscr.refresh() - - ch = stdscr.getch() - if ch in (curses.KEY_ENTER, 10, 13): - return current_val - elif ch == 27: - return None - - -def _step_list(stdscr, field_data, current_val, step, total): - key = field_data["key"] - desc = field_data.get("desc", "") - items = [x.strip() for x in current_val.split(",") if x.strip()] if current_val else [] - cursor = 0 - scroll = 0 - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(72, max_x - 4) - box_h = max(14, min(20, max_y - 4)) - box_x = max(0, (max_x - box_w) // 2) - box_y = 1 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = f" 环境变量配置 ({step}/{total}) " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - ry = box_y + 2 - _safe_addstr(stdscr, ry, box_x + 4, key, curses.color_pair(C_SELECTED) | curses.A_BOLD) - - ry += 1 - if desc: - _safe_addstr(stdscr, ry, box_x + 4, desc[:box_w - 8], curses.color_pair(C_DESC)) - - ry += 2 - max_list_visible = box_h - 8 - max_scroll = max(0, len(items) - max_list_visible) - if cursor < scroll: - scroll = cursor - elif cursor >= scroll + max_list_visible: - scroll = cursor - max_list_visible + 1 - scroll = max(0, min(scroll, max_scroll)) - - if not items: - _safe_addstr(stdscr, ry, box_x + 6, "(空列表,按 + 添加项)", curses.color_pair(C_DESC)) - else: - visible_items = items[scroll:scroll + max_list_visible] - for i, item in enumerate(visible_items): - actual_idx = scroll + i - iy = ry + i - is_active = (actual_idx == cursor) - marker = "▸" if is_active else " " - line = f" {marker} {item}" - if is_active: - _safe_addstr(stdscr, iy, box_x + 4, line[:box_w - 8], curses.color_pair(C_HIGHLIGHT) | curses.A_BOLD) - else: - _safe_addstr(stdscr, iy, box_x + 4, line[:box_w - 8], curses.color_pair(C_UNSELECTED)) - - count_y = box_y + box_h - 3 - _safe_addstr(stdscr, count_y, box_x + 4, f"共 {len(items)} 项", curses.color_pair(C_DESC)) - - if max_scroll > 0: - scroll_info = f" [{scroll + 1}-{min(scroll + max_list_visible, len(items))}/{len(items)}] " - _safe_addstr(stdscr, count_y, box_x + box_w - len(scroll_info) - 2, scroll_info, curses.color_pair(C_DESC)) - - hint_y = box_y + box_h + 1 - _draw_hint(stdscr, hint_y, box_x + 2, "↑↓ 切换 + 添加 - 删除 Enter 编辑 Ctrl+D 完成 Esc 返回") - - stdscr.refresh() - - ch = stdscr.getch() - if ch == curses.KEY_UP: - if items: - cursor = max(0, cursor - 1) - elif ch == curses.KEY_DOWN: - if items: - cursor = min(len(items) - 1, cursor + 1) - elif ch in (ord('+'), ord('=')): - new_item = _step_text(stdscr, {"key": f"{key}[新项]", "desc": "输入新项的值"}, "", step, total) - if new_item is not None and new_item.strip(): - items.append(new_item.strip()) - cursor = len(items) - 1 - elif ch in (ord('-'), ord('_')): - if items and 0 <= cursor < len(items): - items.pop(cursor) - if cursor >= len(items) and len(items) > 0: - cursor = len(items) - 1 - elif ch in (curses.KEY_ENTER, 10, 13): - if items and 0 <= cursor < len(items): - new_item = _step_text(stdscr, {"key": f"{key}[{cursor}]", "desc": "编辑当前项"}, items[cursor], step, total) - if new_item is not None: - items[cursor] = new_item.strip() if new_item.strip() else items[cursor] - elif ch == 4: - return ",".join(items) - elif ch == 27: - return None - - -def _env_preview(stdscr, items, values, secret_auto): - field_indices = [i for i, (kind, _) in enumerate(items) if kind == "field"] - cursor = 0 - scroll = 0 - - while True: - _init_colors() - stdscr.clear() - max_y, max_x = stdscr.getmaxyx() - - box_w = min(76, max_x - 4) - box_h = max_y - 5 - box_x = max(0, (max_x - box_w) // 2) - box_y = 1 - - _draw_banner(stdscr, 0, max_x) - _draw_box(stdscr, box_y, box_x, box_h, box_w) - - title = " .env 配置预览 " - tx = box_x + max(0, (box_w - len(title)) // 2) - _safe_addstr(stdscr, box_y, tx, title, curses.color_pair(C_TITLE) | curses.A_BOLD) - - cursor_item_idx = field_indices[cursor] if cursor < len(field_indices) else 0 - - max_visible = box_h - 3 - total_items = len(items) - max_scroll = max(0, total_items - max_visible) - - if cursor_item_idx < scroll: - scroll = cursor_item_idx - elif cursor_item_idx >= scroll + max_visible: - scroll = cursor_item_idx - max_visible + 1 - scroll = max(0, min(scroll, max_scroll)) - - visible = items[scroll:scroll + max_visible] - current_visible_idx = cursor_item_idx - scroll - - for i, (kind, data) in enumerate(visible): - ry = box_y + 1 + i - rx = box_x + 2 - remaining = box_w - 5 - - if kind == "group": - _safe_addstr(stdscr, ry, rx, f" {data}", curses.color_pair(C_SUBTITLE) | curses.A_BOLD) - elif kind == "field": - is_active = (i == current_visible_idx) - key = data["key"] - val = values.get(key, data["default"]) - ftype = data.get("type", "text") - - if ftype == "secret": - if key in secret_auto: - if val: - preview = val[:6] + "..." + val[-4:] if len(val) > 10 else val - display = f"(随机: {preview})" - else: - display = "(随机生成)" - elif val: - display = "******" - else: - display = "(未设置)" - elif ftype == "auto": - display = f"{val} (自动)" - elif ftype == "bool": - display = val - elif ftype == "list": - list_items = [x.strip() for x in val.split(",") if x.strip()] if val else [] - if list_items: - display = ", ".join(list_items) - else: - display = "(空列表)" - else: - display = val if val else "(空)" - - line = f" {key} = {display}" - - if is_active: - _safe_addstr(stdscr, ry, rx, line[:remaining], curses.color_pair(C_HIGHLIGHT) | curses.A_BOLD) - else: - color = C_DESC if ftype == "auto" else C_UNSELECTED - _safe_addstr(stdscr, ry, rx, line[:remaining], curses.color_pair(color)) - - desc_y = box_y + box_h - 2 - if 0 <= current_visible_idx < len(visible) and visible[current_visible_idx][0] == "field": - field_data = visible[current_visible_idx][1] - desc = field_data.get("desc", "") - if desc: - _safe_addstr(stdscr, desc_y, box_x + 3, f" {desc}"[:box_w - 6], curses.color_pair(C_DESC)) - - if max_scroll > 0: - scroll_info = f" [{scroll + 1}-{min(scroll + max_visible, total_items)}/{total_items}] " - _safe_addstr(stdscr, desc_y, box_x + box_w - len(scroll_info) - 2, scroll_info, curses.color_pair(C_DESC)) - - hint_y = box_y + box_h + 1 - hint = "↑↓ 移动 Enter 编辑 S 保存 Esc 取消" - if max_scroll > 0: - hint = "↑↓/PgUp/PgDn 滚动 " + hint - _draw_hint(stdscr, hint_y, box_x + 2, hint) - - stdscr.refresh() - - ch = stdscr.getch() - if ch == curses.KEY_UP: - cursor = max(0, cursor - 1) - elif ch == curses.KEY_DOWN: - cursor = min(len(field_indices) - 1, cursor + 1) - elif ch == curses.KEY_PPAGE: - cursor = max(0, cursor - 5) - elif ch == curses.KEY_NPAGE: - cursor = min(len(field_indices) - 1, cursor + 5) - elif ch in (curses.KEY_ENTER, 10, 13): - field_data = items[field_indices[cursor]][1] - ftype = field_data.get("type", "text") - key = field_data["key"] - current = values.get(key, field_data["default"]) - - if ftype == "bool": - result = _step_bool(stdscr, field_data, current, cursor + 1, len(field_indices)) - if result is not None: - values[key] = result - elif ftype == "secret": - is_auto_flag = key in secret_auto - result, auto = _step_secret(stdscr, field_data, current, is_auto_flag, cursor + 1, len(field_indices)) - if result is not None: - values[key] = result - if auto: - secret_auto.add(key) - else: - secret_auto.discard(key) - elif ftype == "list": - result = _step_list(stdscr, field_data, current, cursor + 1, len(field_indices)) - if result is not None: - values[key] = result - else: - result = _step_text(stdscr, field_data, current, cursor + 1, len(field_indices)) - if result is not None: - values[key] = result - elif ch in (ord('s'), ord('S')): - return values - elif ch == 27: - return None - - -def _configure_env(stdscr, answers): - items = _get_env_items(answers) - field_items = [(i, data) for i, (kind, data) in enumerate(items) if kind == "field"] - total = len(field_items) - - existing = _load_existing_env(BASE_DIR / ".env") - values = {} - secret_auto = set() - - for idx, data in field_items: - key = data["key"] - if data.get("type") == "auto": - values[key] = data["default"] - elif key in existing: - values[key] = existing[key] - else: - values[key] = data["default"] - - step = 0 - while 0 <= step < total: - idx, data = field_items[step] - ftype = data.get("type", "text") - key = data["key"] - current = values.get(key, data["default"]) - - if ftype == "bool": - result = _step_bool(stdscr, data, current, step + 1, total) - if result is None: - step -= 1 - continue - values[key] = result - step += 1 - elif ftype == "secret": - is_auto_flag = key in secret_auto - result, auto = _step_secret(stdscr, data, current, is_auto_flag, step + 1, total) - if result is None: - step -= 1 - continue - values[key] = result - if auto: - secret_auto.add(key) - else: - secret_auto.discard(key) - step += 1 - elif ftype == "auto": - result = _step_auto(stdscr, data, current, step + 1, total) - if result is None: - step -= 1 - continue - step += 1 - elif ftype == "list": - result = _step_list(stdscr, data, current, step + 1, total) - if result is None: - step -= 1 - continue - values[key] = result - step += 1 - else: - result = _step_text(stdscr, data, current, step + 1, total) - if result is None: - step -= 1 - continue - values[key] = result - step += 1 - - if step < 0: - return None - - result = _env_preview(stdscr, items, values, secret_auto) - if result is None: - return None - - return result - - -def generate_env_content(answers, values): - lines = [] - lines.append("# ZASCA 环境配置文件") - lines.append("# 由 deploy.py 自动生成") - lines.append("") - - lines.append("# ========== 核心配置 ==========") - lines.append(f"DEBUG={values.get('DEBUG', 'True')}") - - secret_key = values.get('DJANGO_SECRET_KEY', '') - if not secret_key: - secret_key = _generate_secret(50) - lines.append(f"DJANGO_SECRET_KEY={secret_key}") - - lines.append(f"ALLOWED_HOSTS={values.get('ALLOWED_HOSTS', 'localhost,127.0.0.1')}") - lines.append(f"CSRF_TRUSTED_ORIGINS={values.get('CSRF_TRUSTED_ORIGINS', 'https://localhost,https://127.0.0.1')}") - lines.append("") - - db = answers.get("db", "sqlite") - lines.append("# ========== 数据库配置 ==========") - lines.append(f"DB_ENGINE={values.get('DB_ENGINE', db)}") - if db != "sqlite": - lines.append(f"DB_HOST={values.get('DB_HOST', '127.0.0.1')}") - lines.append(f"DB_PORT={values.get('DB_PORT', '3306')}") - lines.append(f"DB_NAME={values.get('DB_NAME', 'zasca')}") - lines.append(f"DB_USER={values.get('DB_USER', 'root')}") - db_pass = values.get('DB_PASSWORD', '') - if not db_pass: - db_pass = _generate_secret(50) - lines.append(f"DB_PASSWORD={db_pass}") - lines.append("") - - if answers.get("redis"): - lines.append("# ========== Redis 配置 ==========") - lines.append(f"REDIS_URL={values.get('REDIS_URL', 'redis://localhost:6379/0')}") - lines.append("") - - if answers.get("celery"): - lines.append("# ========== Celery 配置 ==========") - broker = values.get('CELERY_BROKER_URL', '') - backend = values.get('CELERY_RESULT_BACKEND', '') - if not broker and answers.get("redis"): - redis_url = values.get('REDIS_URL', 'redis://localhost:6379/0') - broker = redis_url.replace('/0', '/1') - if not backend and answers.get("redis"): - redis_url = values.get('REDIS_URL', 'redis://localhost:6379/0') - backend = redis_url.replace('/0', '/2') - if broker: - lines.append(f"CELERY_BROKER_URL={broker}") - if backend: - lines.append(f"CELERY_RESULT_BACKEND={backend}") - lines.append("") - - lines.append("# ========== 演示模式 ==========") - lines.append(f"ZASCA_DEMO={values.get('ZASCA_DEMO', '0')}") - lines.append("") - - lines.append("# ========== 安全配置 ==========") - lines.append(f"SECURE_SSL_REDIRECT={values.get('SECURE_SSL_REDIRECT', 'False')}") - lines.append(f"SESSION_COOKIE_SECURE={values.get('SESSION_COOKIE_SECURE', 'False')}") - lines.append(f"CSRF_COOKIE_SECURE={values.get('CSRF_COOKIE_SECURE', 'False')}") - lines.append("") - - lines.append("# ========== 日志配置 ==========") - lines.append(f"LOG_LEVEL={values.get('LOG_LEVEL', 'DEBUG')}") - lines.append(f"LOG_FILE={values.get('LOG_FILE', '/var/log/2c2a/application.log')}") - lines.append("") - - if answers.get("winrm"): - lines.append("# ========== WinRM 配置 ==========") - lines.append(f"WINRM_TIMEOUT={values.get('WINRM_TIMEOUT', '30')}") - lines.append(f"WINRM_RETRY_COUNT={values.get('WINRM_RETRY_COUNT', '3')}") - lines.append("") - - lines.append("# ========== Gateway 配置 ==========") - lines.append(f"GATEWAY_ENABLED={values.get('GATEWAY_ENABLED', 'False')}") - lines.append(f"GATEWAY_CONTROL_SOCKET={values.get('GATEWAY_CONTROL_SOCKET', '/run/2c2a/control.sock')}") - lines.append("") - - beta_fields = ['BETA_DB_NAME', 'BETA_DB_USER', 'BETA_DB_PASSWORD', 'BETA_DB_HOST', 'BETA_DB_PORT'] - has_beta = any(values.get(f, '') for f in beta_fields) - if has_beta: - lines.append("# ========== Beta 数据库配置 ==========") - for f in beta_fields: - if f == 'BETA_DB_PASSWORD': - bp = values.get(f, '') - if not bp: - bp = _generate_secret(50) - lines.append(f"{f}={bp}") - else: - lines.append(f"{f}={values.get(f, '')}") - lines.append("") - - lines.append("# ========== Bootstrap 认证配置 ==========") - salt = values.get('BOOTSTRAP_SHARED_SALT', '') - if not salt: - salt = _generate_secret(50) - lines.append(f"BOOTSTRAP_SHARED_SALT={salt}") - lines.append("") - - return "\n".join(lines) - - -def compute_dependencies(answers): - deps = dict(DEPS_CORE) - - if answers.get("celery"): - deps.update(DEPS_CELERY) - - if answers["db"] == "mysql": - deps.update(DEPS_MYSQL) - elif answers["db"] == "postgresql": - deps.update(DEPS_POSTGRESQL) - - if answers.get("redis"): - deps.update(DEPS_REDIS) - - if answers.get("celery") and not answers.get("redis"): - deps.update(DEPS_SQLITE_BROKER) - - if answers.get("winrm"): - deps.update(DEPS_WINRM) - - if answers.get("kerberos"): - deps.update(DEPS_KERBEROS) - - if answers.get("markdown"): - deps.update(DEPS_MARKDOWN) - - if answers.get("2fa"): - deps.update(DEPS_2FA) - - dev_deps = dict(DEPS_DEV) if answers.get("dev") else {} - - return deps, dev_deps - - -def generate_pyproject_toml(answers, deps, dev_deps): - lines = [] - - lines.append("[project]") - lines.append('name = "2c2a"') - lines.append('version = "1.0.0"') - lines.append('description = "2c2a - Django Web Application"') - lines.append('license = "AGPL-3.0-only"') - lines.append('readme = "README.md"') - lines.append('requires-python = ">=3.10"') - - sorted_deps = sorted(deps.values(), key=_dep_sort_key) - lines.append("dependencies = [") - for spec in sorted_deps: - lines.append(f' "{spec}",') - lines.append("]") - - optional_groups = {} - if not answers.get("redis"): - optional_groups["redis"] = sorted(DEPS_REDIS.values()) - if not answers.get("kerberos"): - optional_groups["kerberos"] = sorted(DEPS_KERBEROS.values()) - if not answers.get("winrm"): - optional_groups["winrm"] = sorted(DEPS_WINRM.values()) - - if optional_groups: - lines.append("") - lines.append("[project.optional-dependencies]") - for group_name, group_deps in optional_groups.items(): - lines.append(f"{group_name} = [") - for spec in group_deps: - lines.append(f' "{spec}",') - lines.append("]") - - lines.append("") - lines.append("[build-system]") - lines.append('requires = ["hatchling"]') - lines.append('build-backend = "hatchling.build"') - - if dev_deps: - lines.append("") - lines.append("[dependency-groups]") - lines.append("dev = [") - for spec in sorted(dev_deps.values()): - lines.append(f' "{spec}",') - lines.append("]") - - lines.append("") - lines.append("[tool.hatch.metadata]") - lines.append("allow-direct-references = true") - - lines.append("") - lines.append("[tool.hatch.build.targets.wheel]") - lines.append('packages = ["."]') - - lines.append("") - lines.append("[tool.pyright]") - lines.append('venvPath = "."') - lines.append('venv = ".venv"') - lines.append('pythonVersion = "3.13"') - lines.append('typeCheckingMode = "basic"') - - lines.append("") - lines.append("[tool.pyrefly]") - lines.append('python-version = "3.13"') - - lines.append("") - return "\n".join(lines) - - -def _dep_sort_key(spec): - if "@" in spec: - return (1, spec.lower()) - return (0, spec.lower()) - - -def backup_file(path): - if not path.exists(): - return None - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - backup = path.with_name(f"{path.stem}.bak.{ts}{path.suffix}") - shutil.copy2(path, backup) - return backup - - -def main(): - if not sys.stdin.isatty(): - print("错误: 此脚本需要交互式终端") - sys.exit(1) - - try: - curses.wrapper(_tui_main) - except KeyboardInterrupt: - pass - - -if __name__ == "__main__": - main() diff --git a/scripts/tailwind-build.sh b/scripts/tailwind-build.sh deleted file mode 100755 index bd0736e..0000000 --- a/scripts/tailwind-build.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# 2c2a - Tailwind CSS v4 Build Script -# ============================================================================ -# Usage: -# ./scripts/tailwind-build.sh # One-time build -# ./scripts/tailwind-build.sh --watch # Watch mode (auto-rebuild on changes) -# ============================================================================ - -set -euo pipefail - -PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$PROJECT_ROOT" - -TAILWIND_BIN="./static/vendor/tailwindcss" -INPUT_CSS="./static/src/tailwind.css" -OUTPUT_CSS="./static/css/provider.css" -TAILWIND_VERSION="v4.2.4" - -detect_platform() { - local os arch - os="$(uname -s | tr '[:upper:]' '[:lower:]')" - arch="$(uname -m)" - case "$os" in - linux) os="linux" ;; - darwin) os="macos" ;; - *) echo "[ERROR] Unsupported OS: $os"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) arch="x64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "[ERROR] Unsupported architecture: $arch"; exit 1 ;; - esac - echo "tailwindcss-${os}-${arch}" -} - -download_tailwind() { - local platform filename url - platform="$(detect_platform)" - filename="$platform" - url="https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/${filename}" - - echo "[INFO] Downloading Tailwind CSS CLI ${TAILWIND_VERSION}..." - echo " Platform: $platform" - echo " URL: $url" - - mkdir -p "$(dirname "$TAILWIND_BIN")" - if command -v curl &>/dev/null; then - curl -fSL -o "$TAILWIND_BIN" "$url" - elif command -v wget &>/dev/null; then - wget -O "$TAILWIND_BIN" "$url" - else - echo "[ERROR] Neither curl nor wget found. Please install one and retry." - exit 1 - fi - - chmod +x "$TAILWIND_BIN" - echo "[INFO] Tailwind CSS CLI downloaded successfully." -} - -if [[ ! -x "$TAILWIND_BIN" ]]; then - echo "[WARN] Tailwind CSS CLI not found at: $TAILWIND_BIN" - read -rp "Download it now from GitHub? [Y/n] " answer - case "${answer:-Y}" in - [yY]|[yY][eE][sS]|"") download_tailwind ;; - *) echo "[ERROR] Cannot build without Tailwind CSS CLI. Exiting."; exit 1 ;; - esac -fi - -mkdir -p "$(dirname "$OUTPUT_CSS")" - -if [[ "${1:-}" == "--watch" ]]; then - echo "[INFO] Starting Tailwind CSS in watch mode..." - echo " Input: $INPUT_CSS" - echo " Output: $OUTPUT_CSS" - "$TAILWIND_BIN" -i "$INPUT_CSS" -o "$OUTPUT_CSS" --watch -else - echo "[INFO] Building Tailwind CSS (one-time)..." - echo " Input: $INPUT_CSS" - echo " Output: $OUTPUT_CSS" - "$TAILWIND_BIN" -i "$INPUT_CSS" -o "$OUTPUT_CSS" - echo "[INFO] Build complete." -fi diff --git a/static/admin/css/bootstrap-deploy-button.css b/static/admin/css/bootstrap-deploy-button.css deleted file mode 100755 index d728d73..0000000 --- a/static/admin/css/bootstrap-deploy-button.css +++ /dev/null @@ -1,871 +0,0 @@ -/* 三步骤部署流程模态框样式 */ -#deploy-flow-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 10000; - display: none; -} - -#deploy-flow-modal-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: white; - padding: 25px; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - min-width: 600px; - max-width: 800px; - max-height: 85vh; - overflow-y: auto; -} - -#deploy-flow-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 25px; - padding-bottom: 15px; - border-bottom: 1px solid #eee; -} - -#deploy-flow-title { - margin: 0; - font-size: 1.4em; - font-weight: bold; - color: #333; -} - -#close-deploy-flow-modal { - background: none; - border: none; - font-size: 1.8em; - cursor: pointer; - padding: 0; - width: 35px; - height: 35px; - display: flex; - align-items: center; - justify-content: center; - color: #999; - transition: color 0.2s; -} - -#close-deploy-flow-modal:hover { - color: #333; -} - -/* 步骤指示器 */ -#step-indicator { - display: flex; - justify-content: space-between; - margin-bottom: 30px; - position: relative; -} - -.step-item { - display: flex; - flex-direction: column; - align-items: center; - flex: 1; - position: relative; - z-index: 2; -} - -.step-item:not(:last-child)::after { - content: ''; - position: absolute; - top: 20px; - left: 50%; - right: -50%; - height: 2px; - background-color: #ddd; - z-index: 1; -} - -.step-item.active::after, -.step-item.completed::after { - background-color: #007cba; -} - -.step-number { - width: 40px; - height: 40px; - border-radius: 50%; - background-color: #ddd; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - color: #666; - margin-bottom: 8px; - transition: all 0.3s ease; -} - -.step-item.active .step-number { - background-color: #007cba; - color: white; -} - -.step-item.completed .step-number { - background-color: #28a745; - color: white; -} - -.step-text { - font-size: 0.9em; - color: #666; - text-align: center; -} - -.step-item.active .step-text { - color: #007cba; - font-weight: 500; -} - -.step-item.completed .step-text { - color: #28a745; -} - -/* 步骤面板 */ -.step-panel { - display: none; - animation: fadeIn 0.3s ease; -} - -.step-panel.active { - display: block; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.step-panel h4 { - margin-top: 0; - margin-bottom: 20px; - color: #333; - font-size: 1.2em; -} - -.step-instructions { - margin-bottom: 25px; -} - -.step-instructions p { - margin: 0 0 15px 0; - line-height: 1.5; -} - -/* 下载部分 */ -.download-section { - margin: 20px 0; - text-align: center; -} - -.download-btn { - display: inline-block; - background-color: #007cba; - color: white; - padding: 12px 24px; - text-decoration: none; - border-radius: 4px; - font-weight: 500; - transition: background-color 0.2s; - margin-bottom: 10px; -} - -.download-btn:hover { - background-color: #005a87; - color: white; -} - -.download-hint { - display: block; - font-size: 0.9em; - color: #666; -} - -.note { - font-size: 0.9em; - color: #666; - background-color: #f8f9fa; - padding: 12px; - border-radius: 4px; - border-left: 3px solid #007cba; -} - -/* 命令部分 */ -.command-section { - margin: 20px 0; -} - -.command-display { - background-color: #f8f9fa; - border: 1px solid #ddd; - border-radius: 4px; - padding: 15px; - margin-bottom: 12px; - font-family: 'Courier New', monospace; - font-size: 0.95em; - white-space: pre-wrap; - word-break: break-all; - min-height: 60px; - display: flex; - align-items: center; -} - -.copy-btn { - background-color: #007cba; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - font-size: 0.9em; - transition: background-color 0.2s; -} - -.copy-btn:hover { - background-color: #005a87; -} - -.copy-btn.copied { - background-color: #28a745; -} - -.command-info ul { - margin: 10px 0; - padding-left: 20px; -} - -.command-info li { - margin-bottom: 5px; - line-height: 1.4; -} - -/* 配对码部分 */ -.pairing-section { - margin: 20px 0; - text-align: center; -} - -.pairing-code { - font-size: 2.5em; - font-weight: bold; - letter-spacing: 5px; - color: #007cba; - background-color: #f8f9fa; - padding: 20px; - border-radius: 8px; - border: 2px dashed #007cba; - margin: 15px 0; - font-family: monospace; -} - -.pairing-info { - margin: 15px 0; -} - -.pairing-info p { - margin: 5px 0; -} - -.warning { - color: #dc3545; - font-weight: 500; -} - -/* 状态部分 */ -.status-section { - margin: 20px 0; - padding: 15px; - background-color: #f8f9fa; - border-radius: 4px; - text-align: center; -} - -#pairing-status { - font-weight: 500; - color: #666; -} - -.loading { - color: #666; - font-style: italic; -} - -.error { - color: #dc3545; -} - -/* 按钮区域 */ -.step-actions { - display: flex; - justify-content: space-between; - gap: 15px; - margin-top: 25px; - padding-top: 20px; - border-top: 1px solid #eee; -} - -.prev-btn, .next-btn, .finish-btn { - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.95em; - transition: all 0.2s; -} - -.prev-btn { - background-color: #6c757d; - color: white; -} - -.prev-btn:hover { - background-color: #5a6268; -} - -.next-btn, .finish-btn { - background-color: #007cba; - color: white; -} - -.next-btn:hover, .finish-btn:hover { - background-color: #005a87; -} - -.finish-btn { - background-color: #28a745; -} - -.finish-btn:hover { - background-color: #218838; -} - -/* 深色模式支持 */ -@media (prefers-color-scheme: dark) { - #deploy-flow-modal-content { - background-color: #2d2d2d; - color: #fff; - } - - #deploy-flow-title, - .step-panel h4, - .step-text { - color: #fff; - } - - #close-deploy-flow-modal { - color: #aaa; - } - - #close-deploy-flow-modal:hover { - color: #fff; - } - - .step-number { - background-color: #555; - color: #ccc; - } - - .step-item.active .step-number { - background-color: #007cba; - color: white; - } - - .step-item.completed .step-number { - background-color: #28a745; - color: white; - } - - .step-text, - .step-item .step-text { - color: #aaa; - } - - .step-item.active .step-text { - color: #007cba; - } - - .step-item.completed .step-text { - color: #28a745; - } - - .command-display, - .pairing-code { - background-color: #333; - border-color: #555; - color: #fff; - } - - .note, - .status-section { - background-color: #333; - color: #fff; - } - - .download-btn:hover { - background-color: #005a87; - } -} - -/* Django Admin 深色模式支持 */ -body[data-admin-theme="dark"] #deploy-flow-modal-content, -.theme-dark #deploy-flow-modal-content { - background-color: #2d2d2d; - color: #fff; -} - -body[data-admin-theme="dark"] #deploy-flow-title, -.theme-dark #deploy-flow-title, -body[data-admin-theme="dark"] .step-panel h4, -.theme-dark .step-panel h4 { - color: #fff; -} - -body[data-admin-theme="dark"] .step-number, -.theme-dark .step-number { - background-color: #555; - color: #ccc; -} - -body[data-admin-theme="dark"] .command-display, -.theme-dark .command-display, -body[data-admin-theme="dark"] .pairing-code, -.theme-dark .pairing-code { - background-color: #333; - border-color: #555; - color: #fff; -} - -/* 快速部署对话框样式 */ -#quick-deploy-dialog, -#quick-register-dialog, -#verify-host-dialog { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 10001; - display: none; -} - -#quick-deploy-dialog-content, -#quick-register-dialog-content, -#verify-host-dialog-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: white; - padding: 25px; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - min-width: 500px; - max-width: 900px; - max-height: 80vh; - overflow-y: auto; -} - -#quick-deploy-dialog-header, -#quick-register-dialog-header, -#verify-host-dialog-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - padding-bottom: 15px; - border-bottom: 1px solid #eee; -} - -#quick-deploy-dialog-header h3, -#quick-register-dialog-header h3, -#verify-host-dialog-header h3 { - margin: 0; - font-size: 1.3em; - color: #333; -} - -#close-quick-deploy-dialog, -#close-quick-register-dialog, -#close-verify-host-dialog { - background: none; - border: none; - font-size: 1.8em; - cursor: pointer; - padding: 0; - width: 35px; - height: 35px; - display: flex; - align-items: center; - justify-content: center; - color: #999; - transition: color 0.2s; -} - -#close-quick-deploy-dialog:hover, -#close-quick-register-dialog:hover, -#close-verify-host-dialog:hover { - color: #333; -} - -/* 验证对话框样式 */ -.verify-section { - margin-bottom: 20px; -} - -.verify-section h4 { - margin: 0 0 15px 0; - color: #333; -} - -.pending-hosts-grid { - display: grid; - gap: 10px; -} - -.pending-host-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 15px; - background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; -} - -.pending-host-item:hover { - background-color: #e9ecef; - border-color: #007cba; -} - -.pending-host-item .host-info { - flex: 1; -} - -.pending-host-item .host-info strong { - display: block; - margin-bottom: 4px; - color: #333; -} - -.pending-host-item .host-time { - font-size: 0.85em; - color: #666; -} - -.verify-btn-small { - background-color: #007cba; - color: white; - border: none; - padding: 6px 12px; - border-radius: 3px; - cursor: pointer; - font-size: 0.85em; - transition: background-color 0.2s; -} - -.verify-btn-small:hover { - background-color: #005a87; -} - -.revoke-btn-small { - background-color: #dc3545; - color: white; - border: none; - padding: 6px 12px; - border-radius: 3px; - cursor: pointer; - font-size: 0.85em; - transition: background-color 0.2s; - margin-left: 8px; -} - -.revoke-btn-small:hover { - background-color: #c82333; -} - -.host-actions { - display: flex; - gap: 5px; -} - -.verify-form { - padding: 20px; - background-color: #f8f9fa; - border-radius: 4px; - border: 1px solid #e9ecef; -} - -.verify-form h4 { - margin: 0 0 15px 0; - color: #333; -} - -.verify-form p { - margin: 0 0 15px 0; - color: #666; -} - -.verify-form .form-group { - margin-bottom: 15px; -} - -.verify-form label { - display: block; - margin-bottom: 5px; - font-weight: 500; - color: #333; -} - -.verify-form input[type="text"] { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1em; - font-family: monospace; - letter-spacing: 2px; -} - -.verify-form input[type="text"]:focus { - outline: none; - border-color: #007cba; - box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2); -} - -/* 注册说明 */ -.register-instructions { - margin-bottom: 20px; -} - -.register-instructions h4 { - margin: 0 0 10px 0; - color: #333; -} - -.register-instructions p { - margin: 0; - color: #666; - line-height: 1.5; -} - -.register-info { - margin-top: 20px; - padding: 15px; - background-color: #f8f9fa; - border-radius: 4px; - border-left: 3px solid #007cba; -} - -.register-info p { - margin: 0 0 10px 0; -} - -.register-info ul { - margin: 0; - padding-left: 20px; -} - -.register-info li { - margin-bottom: 5px; - line-height: 1.4; -} - -/* 主机选择列表 */ -.host-select-list { - max-height: 400px; - overflow-y: auto; -} - -.host-select-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 15px; - margin-bottom: 8px; - background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; -} - -.host-select-item:hover { - background-color: #e9ecef; - border-color: #007cba; -} - -.host-info { - flex: 1; -} - -.host-info strong { - display: block; - margin-bottom: 4px; - color: #333; -} - -.host-detail { - font-size: 0.9em; - color: #666; -} - -.host-status { - margin: 0 15px; - padding: 4px 8px; - border-radius: 3px; - font-size: 0.85em; - background-color: #fff; - border: 1px solid #ddd; -} - -.deploy-btn-small { - background-color: #007cba; - color: white; - border: none; - padding: 6px 12px; - border-radius: 3px; - cursor: pointer; - font-size: 0.85em; - transition: background-color 0.2s; -} - -.deploy-btn-small:hover { - background-color: #005a87; -} - -/* 内联部署按钮 */ -.inline-deploy-btn { - display: inline-block; - margin-left: 10px; - padding: 3px 8px; - background-color: #007cba; - color: white; - text-decoration: none; - border-radius: 3px; - font-size: 0.85em; - transition: background-color 0.2s; -} - -.inline-deploy-btn:hover { - background-color: #005a87; - color: white; - text-decoration: none; -} - -/* 深色模式支持 */ -@media (prefers-color-scheme: dark) { - #quick-deploy-dialog-content, - #quick-register-dialog-content { - background-color: #2d2d2d; - color: #fff; - } - - #quick-deploy-dialog-header h3, - #quick-register-dialog-header h3 { - color: #fff; - } - - #close-quick-deploy-dialog, - #close-quick-register-dialog { - color: #aaa; - } - - #close-quick-deploy-dialog:hover, - #close-quick-register-dialog:hover { - color: #fff; - } - - .host-select-item { - background-color: #333; - border-color: #555; - } - - .host-select-item:hover { - background-color: #444; - border-color: #007cba; - } - - .host-info strong { - color: #fff; - } - - .host-detail { - color: #aaa; - } - - .host-status { - background-color: #444; - border-color: #666; - color: #fff; - } - - .register-instructions h4 { - color: #fff; - } - - .register-instructions p { - color: #aaa; - } - - .register-info { - background-color: #333; - border-color: #007cba; - } -} - -/* Django Admin 深色模式 */ -body[data-admin-theme="dark"] #quick-deploy-dialog-content, -.theme-dark #quick-deploy-dialog-content, -body[data-admin-theme="dark"] #quick-register-dialog-content, -.theme-dark #quick-register-dialog-content { - background-color: #2d2d2d; - color: #fff; -} - -body[data-admin-theme="dark"] #quick-deploy-dialog-header h3, -.theme-dark #quick-deploy-dialog-header h3, -body[data-admin-theme="dark"] #quick-register-dialog-header h3, -.theme-dark #quick-register-dialog-header h3 { - color: #fff; -} - -body[data-admin-theme="dark"] .host-select-item, -.theme-dark .host-select-item { - background-color: #333; - border-color: #555; -} - -body[data-admin-theme="dark"] .host-info strong, -.theme-dark .host-info strong { - color: #fff; -} - -body[data-admin-theme="dark"] .register-instructions h4, -.theme-dark .register-instructions h4 { - color: #fff; -} - -body[data-admin-theme="dark"] .register-instructions p, -.theme-dark .register-instructions p { - color: #aaa; -} - -body[data-admin-theme="dark"] .register-info, -.theme-dark .register-info { - background-color: #333; - border-color: #007cba; -} \ No newline at end of file diff --git a/static/admin/css/bootstrap_admin.css b/static/admin/css/bootstrap_admin.css deleted file mode 100644 index 52d9198..0000000 --- a/static/admin/css/bootstrap_admin.css +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Bootstrap Admin样式 - 配对码认证版本 - */ - -/* 配对码显示样式 */ -.pairing-code-display { - background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); - border: 2px solid #90caf9; - border-radius: 8px; - padding: 12px 16px; - margin: 8px 0; - display: inline-block; - min-width: 120px; - text-align: center; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - transition: all 0.3s ease; -} - -.pairing-code-display:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.15); -} - -.pairing-code-display .code-number { - font-size: 1.8em; - font-weight: bold; - color: #1976d2; - letter-spacing: 2px; - font-family: 'Courier New', monospace; -} - -.pairing-code-display .code-label { - font-size: 0.85em; - color: #666; - margin-top: 4px; -} - -.pairing-code-display .code-expiry { - font-size: 0.75em; - color: #888; - margin-top: 2px; -} - -/* 过期警告样式 */ -.pairing-code-expiring { - background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%); - border-color: #ffd54f; -} - -.pairing-code-expired { - background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%); - border-color: #ef9a9a; - opacity: 0.8; -} - -.pairing-code-expiring .code-number { - color: #f57f17; - animation: pulse 2s infinite; -} - -.pairing-code-expired .code-number { - color: #c62828; - text-decoration: line-through; -} - -@keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.7; } - 100% { opacity: 1; } -} - -/* 操作按钮样式 */ -.action-buttons { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin-top: 12px; -} - -.action-buttons .btn { - font-size: 0.85em; - padding: 6px 12px; - border-radius: 4px; -} - -.btn-copy-config { - background: #4caf50; - border-color: #4caf50; - color: white; -} - -.btn-copy-config:hover { - background: #45a049; - border-color: #45a049; -} - -.btn-refresh-code { - background: #2196f3; - border-color: #2196f3; - color: white; -} - -.btn-refresh-code:hover { - background: #1976d2; - border-color: #1976d2; -} - -.btn-verify-status { - background: #ff9800; - border-color: #ff9800; - color: white; -} - -.btn-verify-status:hover { - background: #f57c00; - border-color: #f57c00; -} - -/* 状态指示器 */ -.status-indicator { - display: inline-block; - width: 12px; - height: 12px; - border-radius: 50%; - margin-right: 8px; -} - -.status-issued { - background-color: #ff9800; -} - -.status-paired { - background-color: #4caf50; -} - -.status-consumed { - background-color: #2196f3; -} - -.status-expired { - background-color: #f44336; -} - -/* 配对码信息卡片 */ -.pairing-info-card { - background: white; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 16px; - margin: 16px 0; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.pairing-info-card .card-title { - font-size: 1.1em; - font-weight: bold; - color: #333; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 2px solid #e0e0e0; -} - -.pairing-info-card .info-item { - display: flex; - justify-content: space-between; - padding: 8px 0; - border-bottom: 1px solid #f0f0f0; -} - -.pairing-info-card .info-item:last-child { - border-bottom: none; -} - -.pairing-info-card .info-label { - font-weight: 500; - color: #666; -} - -.pairing-info-card .info-value { - font-weight: bold; - color: #333; -} - -/* 倒计时动画 */ -.countdown-timer { - font-family: 'Courier New', monospace; - font-size: 1.2em; - font-weight: bold; - color: #f44336; - animation: countdown-pulse 1s infinite; -} - -@keyframes countdown-pulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.1); } - 100% { transform: scale(1); } -} - -/* 响应式设计 */ -@media (max-width: 768px) { - .pairing-code-display { - min-width: 100px; - padding: 8px 12px; - } - - .pairing-code-display .code-number { - font-size: 1.5em; - } - - .action-buttons { - flex-direction: column; - } - - .action-buttons .btn { - width: 100%; - } -} - -/* 深色模式适配 */ -@media (prefers-color-scheme: dark) { - .pairing-code-display { - background: linear-gradient(135deg, #1a237e 0%, #283593 100%); - border-color: #303f9f; - } - - .pairing-code-display .code-number { - color: #64b5f6; - } - - .pairing-code-display .code-label, - .pairing-code-display .code-expiry { - color: #bbbbbb; - } - - .pairing-info-card { - background: #2d2d2d; - border-color: #444; - color: #ffffff; - } - - .pairing-info-card .card-title { - color: #ffffff; - border-bottom-color: #444; - } - - .pairing-info-card .info-item { - border-bottom-color: #444; - } - - .pairing-info-card .info-label { - color: #bbbbbb; - } - - .pairing-info-card .info-value { - color: #ffffff; - } -} - -/* 快速操作面板 */ -.quick-actions-panel { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 12px; - padding: 20px; - margin: 20px 0; - color: white; - box-shadow: 0 4px 15px rgba(0,0,0,0.2); -} - -.quick-actions-panel h4 { - margin: 0 0 15px 0; - font-size: 1.3em; - font-weight: 600; -} - -.quick-actions-panel .action-buttons { - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -.quick-actions-panel .btn { - background: rgba(255,255,255,0.2); - border: 2px solid rgba(255,255,255,0.3); - color: white; - padding: 10px 20px; - border-radius: 8px; - font-weight: 500; - transition: all 0.3s ease; -} - -.quick-actions-panel .btn:hover { - background: rgba(255,255,255,0.3); - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.2); -} - -/* 统计卡片增强 */ -.stats-panel { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin: 20px 0; -} - -.stat-card { - background: white; - border-radius: 12px; - padding: 20px; - text-align: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - transition: transform 0.3s ease; -} - -.stat-card:hover { - transform: translateY(-5px); -} - -.stat-value { - font-size: 2.5em; - font-weight: bold; - color: #2196f3; - margin-bottom: 8px; -} - -.stat-label { - font-size: 1em; - color: #666; - font-weight: 500; -} - -/* 加载动画 */ -.loading-spinner { - display: inline-block; - width: 20px; - height: 20px; - border: 3px solid #f3f3f3; - border-top: 3px solid #2196f3; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-right: 8px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* 通知样式 */ -.admin-notification { - position: fixed; - top: 20px; - right: 20px; - z-index: 9999; - min-width: 300px; - padding: 16px; - border-radius: 4px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - animation: slideIn 0.3s ease-out; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -.admin-notification.success { - background: #d4edda; - border: 1px solid #c3e6cb; - color: #155724; -} - -.admin-notification.error { - background: #f8d7da; - border: 1px solid #f5c6cb; - color: #721c24; -} - -.admin-notification.info { - background: #d1ecf1; - border: 1px solid #bee5eb; - color: #0c5460; -} - -.admin-notification.warning { - background: #fff3cd; - border: 1px solid #ffeaa7; - color: #856404; -} \ No newline at end of file diff --git a/static/admin/js/bootstrap-deploy-button.js b/static/admin/js/bootstrap-deploy-button.js deleted file mode 100644 index d8e6895..0000000 --- a/static/admin/js/bootstrap-deploy-button.js +++ /dev/null @@ -1,656 +0,0 @@ -// 三步骤部署流程模态框 -var deployFlowModule = (function() { - var $; - var currentStep = 1; - var deployData = {}; - - // 等待django.jQuery可用 - function init() { - if (typeof django !== 'undefined' && django.jQuery) { - $ = django.jQuery; - setupDeployButton(); - setupQuickDeployButton(); - } else { - setTimeout(init, 100); - } - } - - function setupDeployButton() { - $(document).ready(function() { - // 添加部署按钮到页面顶部工具栏(修改页面) - var header = $('.object-tools'); - if (header.length > 0) { - var heading = $('#content h1').text(); - if (heading.includes('主机') || heading.includes('Host')) { - var objectIdMatch = window.location.pathname.match(/\/(\d+)\/change\//); - if (objectIdMatch && objectIdMatch[1]) { - var hostId = objectIdMatch[1]; - var hostName = $('input[name="name"]').val() || 'Unknown'; - - var deployButtonHtml = ` -
  • - - 开始部署 - -
  • - `; - - header.prepend(deployButtonHtml); - } - } - } - - // 创建三步骤模态框 - createDeployModal(); - bindModalEvents(); - }); - } - - function setupQuickDeployButton() { - $(document).ready(function() { - // 检查是否在主机列表页面 - var heading = $('#content h1').text(); - var isHostListPage = heading.includes('选择要修改的主机') || - heading.includes('Select host to change') || - window.location.pathname.includes('/admin/hosts/host/'); - - if (isHostListPage && !window.location.pathname.includes('/change/')) { - // 在列表页面添加一键注册按钮到右上角 - var objectTools = $('.object-tools'); - if (objectTools.length > 0) { - // 添加验证主机按钮 - var verifyButton = ` -
  • - - 验证主机 - -
  • - `; - objectTools.prepend(verifyButton); - - // 添加一键注册按钮 - var quickRegisterButton = ` -
  • - - 一键注册主机 - -
  • - `; - objectTools.prepend(quickRegisterButton); - } - - // 创建快速注册对话框 - createQuickRegisterDialog(); - - // 创建验证对话框 - createVerifyDialog(); - } - }); - } - - function createVerifyDialog() { - var dialogHtml = ` - - `; - - $('body').append(dialogHtml); - - // 绑定关闭事件 - $('#close-verify-host-dialog').click(function() { - $('#verify-host-dialog').hide(); - }); - - $('#verify-host-dialog').click(function(e) { - if (e.target === this) { - $(this).hide(); - } - }); - - // 绑定验证按钮事件 - $(document).on('click', '#submit-verify-btn', function() { - submitVerification(); - }); - - // TOTP输入框自动格式化 - $(document).on('input', '#totp-code', function() { - this.value = this.value.replace(/[^0-9]/g, '').substring(0, 6); - }); - } - - window.showVerifyDialog = function() { - loadPendingHosts(); - $('#verify-host-dialog').show(); - }; - - function loadPendingHosts() { - var baseUrl = window.location.origin; - $.ajax({ - url: baseUrl + '/bootstrap/api/pending-hosts/', - method: 'GET', - success: function(response) { - if (response.success) { - displayPendingHosts(response.data.hosts); - } else { - $('#pending-hosts-list').html('

    加载失败: ' + response.error + '

    '); - } - }, - error: function() { - $('#pending-hosts-list').html('

    加载失败

    '); - } - }); - } - - function displayPendingHosts(hosts) { - if (hosts.length === 0) { - $('#pending-hosts-list').html('

    当前没有待验证的主机

    '); - return; - } - - var html = '
    '; - hosts.forEach(function(host) { - html += ` -
    -
    - ${host.hostname} - ${host.created_at} -
    -
    - - -
    -
    - `; - }); - html += '
    '; - - $('#pending-hosts-list').html(html); - } - - window.selectHostForVerify = function(token, hostname) { - $('#verify-hostname').text(hostname); - $('#verify-form').data('token', token); - $('#verify-form').show(); - $('#totp-code').val('').focus(); - }; - - function submitVerification() { - var token = $('#verify-form').data('token'); - var totpCode = $('#totp-code').val(); - - if (!totpCode || totpCode.length !== 6) { - alert('请输入6位数字验证码'); - return; - } - - var baseUrl = window.location.origin; - $.ajax({ - url: baseUrl + '/bootstrap/api/verify-pairing-code/', - method: 'POST', - data: JSON.stringify({ - token: token, - pairing_code: totpCode - }), - contentType: 'application/json', - success: function(response) { - if (response.success) { - alert('验证成功!主机已激活'); - $('#verify-host-dialog').hide(); - location.reload(); - } else { - alert('验证失败: ' + response.error); - } - }, - error: function() { - alert('验证请求失败'); - } - }); - } - - window.revokePendingHost = function(token, hostname) { - if (!confirm('确定要吊销主机 "' + hostname + '" 吗?\n\n吊销后将删除该主机记录及其令牌,此操作不可恢复。')) { - return; - } - - var baseUrl = window.location.origin; - $.ajax({ - url: baseUrl + '/bootstrap/api/revoke-pending-host/', - method: 'POST', - data: JSON.stringify({ - token: token - }), - contentType: 'application/json', - success: function(response) { - if (response.success) { - alert('吊销成功!\n' + response.message); - loadPendingHosts(); - $('#verify-form').hide(); - } else { - alert('吊销失败: ' + response.error); - } - }, - error: function(xhr) { - var errorMsg = '吊销请求失败'; - if (xhr.responseJSON && xhr.responseJSON.error) { - errorMsg = xhr.responseJSON.error; - } - alert(errorMsg); - } - }); - }; - - function createQuickRegisterDialog() { - var dialogHtml = ` - - `; - - $('body').append(dialogHtml); - - // 绑定关闭事件 - $('#close-quick-register-dialog').click(function() { - $('#quick-register-dialog').hide(); - }); - - $('#quick-register-dialog').click(function(e) { - if (e.target === this) { - $(this).hide(); - } - }); - - // 绑定复制按钮事件 - $(document).on('click', '#copy-register-command-btn', function() { - var commandText = $('#register-command-display').text(); - copyToClipboard(commandText, $(this)); - }); - } - - window.showQuickRegisterDialog = function() { - // 生成一键注册命令 - generateRegisterCommand(); - $('#quick-register-dialog').show(); - }; - - function generateRegisterCommand() { - // 获取当前站点URL - var baseUrl = window.location.origin; - - // 生成极简的一行命令 - var psCommand = `iex (irm ${baseUrl}/bootstrap/api/auto-register/?hostname=$env:COMPUTERNAME).data.script`; - - $('#register-command-display').text(psCommand); - } - - function createDeployModal() { - var modalHtml = ` - - `; - - $('body').append(modalHtml); - } - - function bindModalEvents() { - // 关闭按钮事件 - $('#close-deploy-flow-modal').click(function() { - closeDeployFlow(); - }); - - // 点击背景关闭 - $('#deploy-flow-modal').click(function(e) { - if (e.target === this) { - closeDeployFlow(); - } - }); - - // 复制命令按钮事件 - $(document).on('click', '#copy-command-btn', function() { - var commandText = $('#deploy-command-display').text(); - copyToClipboard(commandText, $(this)); - }); - - // ESC键关闭 - $(document).keydown(function(e) { - if (e.keyCode === 27 && $('#deploy-flow-modal').is(':visible')) { - closeDeployFlow(); - } - }); - } - - function copyToClipboard(text, button) { - navigator.clipboard.writeText(text).then(function() { - var originalText = button.text(); - button.text('已复制!').addClass('copied'); - setTimeout(function() { - button.text(originalText).removeClass('copied'); - }, 2000); - }).catch(function(err) { - console.error('复制失败: ', err); - // 备选方案 - fallbackCopyTextToClipboard(text, button); - }); - } - - function fallbackCopyTextToClipboard(text, button) { - var textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.cssText = 'position: fixed; top: -1000px; left: -1000px; opacity: 0;'; - - document.body.appendChild(textArea); - textArea.select(); - - try { - var successful = document.execCommand('copy'); - if (successful) { - var originalText = button.text(); - button.text('已复制!').addClass('copied'); - setTimeout(function() { - button.text(originalText).removeClass('copied'); - }, 2000); - } else { - alert('复制失败,请手动选择文本并复制'); - } - } catch (err) { - console.error('复制命令失败: ', err); - alert('复制失败,请手动选择文本并复制'); - } - - document.body.removeChild(textArea); - } - - // 全局函数 - window.startDeployFlow = function(hostId, hostName) { - if (!$) { - if (typeof django !== 'undefined' && django.jQuery) { - $ = django.jQuery; - } else { - alert('页面加载中,请稍后重试'); - return; - } - } - - // 重置状态 - currentStep = 1; - deployData = {}; - updateStepIndicator(1); - showStepPanel(1); - - // 显示模态框 - $('#deploy-flow-modal').show(); - - // 获取部署数据 - fetchDeployData(hostId); - }; - - window.nextStep = function() { - if (currentStep < 3) { - currentStep++; - updateStepIndicator(currentStep); - showStepPanel(currentStep); - } - }; - - window.prevStep = function() { - if (currentStep > 1) { - currentStep--; - updateStepIndicator(currentStep); - showStepPanel(currentStep); - } - }; - - window.finishDeploy = function() { - closeDeployFlow(); - alert('部署流程已完成!'); - }; - - function closeDeployFlow() { - $('#deploy-flow-modal').hide(); - currentStep = 1; - deployData = {}; - updateStepIndicator(1); - showStepPanel(1); - } - - function fetchDeployData(hostId) { - // 显示加载状态 - $('#deploy-command-display').html('
    正在生成部署信息...
    '); - $('#pairing-code-display').html('
    正在生成配对码...
    '); - - // 构建API URL - 支持列表页面和修改页面 - var apiUrl; - if (window.location.pathname.includes('/change/')) { - // 修改页面:使用相对路径 - apiUrl = window.location.pathname.replace(/\/change\/?$/, '') + '/generate-deploy-command/'; - } else { - // 列表页面:使用绝对路径 - apiUrl = '/admin/hosts/host/quick-deploy/' + hostId + '/'; - } - - $.ajax({ - url: apiUrl, - method: 'GET', - headers: { - 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || - document.querySelector('input[name="csrfmiddlewaretoken"]')?.value || '' - }, - success: function(response) { - if (response.success) { - deployData = response; - updateDeployInfo(); - } else { - showError('生成部署信息失败: ' + response.error); - } - }, - error: function(xhr) { - var errorMsg = xhr.responseJSON ? xhr.responseJSON.error : '请求失败'; - showError('获取部署信息失败: ' + errorMsg); - } - }); - } - - function updateDeployInfo() { - // 更新部署命令 - $('#deploy-command-display').text(deployData.deploy_command); - - // 更新配对码 - $('#pairing-code-display').text(deployData.pairing_code); - - // 更新配对码过期时间 - var expiryTime = new Date(deployData.pairing_code_expiry); - $('#pairing-expiry').text(expiryTime.toLocaleString('zh-CN')); - - // 启动配对状态检查 - startPairingCheck(); - } - - function showError(message) { - $('#deploy-command-display').html(`${message}`); - $('#pairing-code-display').html(`${message}`); - } - - function updateStepIndicator(step) { - $('.step-item').removeClass('active completed'); - $('.step-item').each(function() { - var itemStep = parseInt($(this).data('step')); - if (itemStep < step) { - $(this).addClass('completed'); - } else if (itemStep === step) { - $(this).addClass('active'); - } - }); - } - - function showStepPanel(step) { - $('.step-panel').removeClass('active'); - $('#step-panel-' + step).addClass('active'); - } - - function startPairingCheck() { - // 每5秒检查一次配对状态 - setInterval(function() { - if ($('#deploy-flow-modal').is(':visible') && currentStep === 3) { - checkPairingStatus(); - } - }, 5000); - } - - function checkPairingStatus() { - // 这里可以添加实际的配对状态检查逻辑 - // 目前只是示例 - var statusElement = $('#pairing-status'); - var currentTime = new Date(); - var expiryTime = new Date(deployData.pairing_code_expiry); - - if (currentTime > expiryTime) { - statusElement.html('配对码已过期,请重新开始部署流程'); - } else { - // 模拟检查状态 - var timeLeft = Math.floor((expiryTime - currentTime) / 1000 / 60); - statusElement.text(`等待配对... (${timeLeft}分钟有效)`); - } - } - - // 初始化模块 - init(); -})(); \ No newline at end of file diff --git a/static/admin/js/bootstrap_admin.js b/static/admin/js/bootstrap_admin.js deleted file mode 100644 index 782c203..0000000 --- a/static/admin/js/bootstrap_admin.js +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Bootstrap Admin前端功能 - 配对码认证版本 - */ - -document.addEventListener('DOMContentLoaded', function() { - // 初始化所有功能 - initCopyButtons(); - initRefreshPairingCode(); - initAutoRefresh(); -}); - -/** - * 初始化复制按钮功能 - */ -function initCopyButtons() { - // 为复制按钮添加点击事件 - document.addEventListener('click', function(e) { - if (e.target.classList.contains('copy-btn')) { - copyToClipboard(e.target); - } - }); -} - -/** - * 复制到剪贴板功能 - */ -function copyToClipboard(button) { - const value = button.getAttribute('data-value'); - if (!value) return; - - // 创建临时textarea元素 - const textarea = document.createElement('textarea'); - textarea.value = value; - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - textarea.style.top = '-9999px'; - document.body.appendChild(textarea); - - // 选中并复制 - textarea.select(); - textarea.setSelectionRange(0, 99999); // 移动端兼容 - - try { - const successful = document.execCommand('copy'); - if (successful) { - // 显示成功提示 - showNotification('配置信息已复制到剪贴板', 'success'); - // 更改按钮状态 - const originalText = button.textContent; - button.textContent = '已复制!'; - button.classList.add('btn-success'); - setTimeout(() => { - button.textContent = originalText; - button.classList.remove('btn-success'); - }, 2000); - } else { - showNotification('复制失败,请手动复制', 'error'); - } - } catch (err) { - console.error('复制失败:', err); - showNotification('复制失败,请手动复制', 'error'); - } - - // 清理 - document.body.removeChild(textarea); -} - -/** - * 初始化刷新配对码功能 - */ -function initRefreshPairingCode() { - window.refreshPairingCode = function(tokenId) { - if (!confirm('确定要刷新配对码吗?旧的配对码将失效。')) { - return; - } - - // 发送AJAX请求 - const xhr = new XMLHttpRequest(); - const url = `/admin/bootstrap/initialtoken/${tokenId}/refresh-pairing-code/`; - - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken')); - - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - const response = JSON.parse(xhr.responseText); - if (response.success) { - showNotification(`配对码已刷新为: ${response.pairing_code}`, 'success'); - // 刷新页面以显示新配对码 - setTimeout(() => { - location.reload(); - }, 1500); - } else { - showNotification(`刷新失败: ${response.error}`, 'error'); - } - } catch (e) { - showNotification('响应解析失败', 'error'); - } - } else { - showNotification(`请求失败: ${xhr.status}`, 'error'); - } - } - }; - - xhr.send(JSON.stringify({})); - }; -} - -/** - * 初始化自动刷新功能 - */ -function initAutoRefresh() { - // 每30秒检查一次配对码状态 - setInterval(function() { - const pairingCodeElements = document.querySelectorAll('[id^="pairing_code_"]'); - pairingCodeElements.forEach(element => { - const tokenId = element.id.replace('pairing_code_', ''); - updatePairingCodeStatus(tokenId, element); - }); - - // 同时更新倒计时显示 - updateAllCountdowns(); - }, 30000); -} - -/** - * 更新所有倒计时显示 - */ -function updateAllCountdowns() { - document.querySelectorAll('.countdown-timer').forEach(timer => { - const parent = timer.closest('.pairing-code-display'); - if (parent && parent.dataset.expiry) { - const expiryTime = parent.dataset.expiry; - const timeRemaining = formatTimeRemaining(expiryTime); - timer.textContent = timeRemaining; - } - }); -} - -/** - * 更新配对码状态 - */ -function updatePairingCodeStatus(tokenId, element) { - // 这里可以实现配对码状态的实时更新 - // 目前作为预留功能 - console.log(`检查令牌 ${tokenId} 的配对码状态`); -} - -/** - * 显示通知消息 - */ -function showNotification(message, type = 'info') { - // 创建通知元素 - const notification = document.createElement('div'); - notification.className = `alert alert-${type} alert-dismissible fade show`; - notification.style.position = 'fixed'; - notification.style.top = '20px'; - notification.style.right = '20px'; - notification.style.zIndex = '9999'; - notification.style.minWidth = '300px'; - notification.innerHTML = ` - ${message} - - `; - - // 添加到页面 - document.body.appendChild(notification); - - // 3秒后自动消失 - setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 3000); -} - -/** - * 获取Cookie值 - */ -function getCookie(name) { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; -} - -/** - * 格式化时间显示 - */ -function formatTimeRemaining(expiryTime) { - const now = new Date(); - const expiry = new Date(expiryTime); - const diffMs = expiry - now; - - if (diffMs <= 0) { - return '已过期'; - } - - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); - const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000); - - if (diffHours > 0) { - return `${diffHours}小时${diffMinutes}分钟`; - } else if (diffMinutes > 0) { - return `${diffMinutes}分钟${diffSeconds}秒`; - } else { - return `${diffSeconds}秒`; - } -} - -/** - * 批量刷新配对码 - */ -window.batchRefreshPairingCodes = function(selectedIds) { - if (!selectedIds || selectedIds.length === 0) { - showNotification('请选择要刷新的令牌', 'warning'); - return; - } - - if (!confirm(`确定要刷新选中的 ${selectedIds.length} 个令牌的配对码吗?`)) { - return; - } - - let successCount = 0; - let failCount = 0; - - selectedIds.forEach((tokenId, index) => { - setTimeout(() => { - refreshSinglePairingCode(tokenId, () => { - successCount++; - if (successCount + failCount === selectedIds.length) { - showNotification(`批量刷新完成: 成功${successCount}个,失败${failCount}个`, - successCount > 0 ? 'success' : 'error'); - if (successCount > 0) { - setTimeout(() => location.reload(), 2000); - } - } - }, () => { - failCount++; - if (successCount + failCount === selectedIds.length) { - showNotification(`批量刷新完成: 成功${successCount}个,失败${failCount}个`, - successCount > 0 ? 'success' : 'error'); - if (successCount > 0) { - setTimeout(() => location.reload(), 2000); - } - } - }); - }, index * 500); // 间隔500ms发送请求 - }); -}; - -/** - * 刷新单个配对码 - */ -function refreshSinglePairingCode(tokenId, onSuccess, onError) { - const xhr = new XMLHttpRequest(); - const url = `/admin/bootstrap/initialtoken/${tokenId}/refresh-pairing-code/`; - - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken')); - - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - const response = JSON.parse(xhr.responseText); - if (response.success) { - onSuccess && onSuccess(response); - } else { - onError && onError(response); - } - } catch (e) { - onError && onError({error: '响应解析失败'}); - } - } else { - onError && onError({error: `请求失败: ${xhr.status}`}); - } - } - }; - - xhr.send(JSON.stringify({})); -} - -/** - * 高亮显示即将过期的配对码 - */ -function highlightExpiringCodes() { - const codeElements = document.querySelectorAll('.pairing-code-display'); - codeElements.forEach(element => { - const expiryAttr = element.getAttribute('data-expiry'); - if (expiryAttr) { - const expiryTime = new Date(expiryAttr); - const now = new Date(); - const minutesRemaining = (expiryTime - now) / (1000 * 60); - - if (minutesRemaining <= 1) { - element.style.backgroundColor = '#ffebee'; - element.style.borderColor = '#ffcdd2'; - element.style.color = '#c62828'; - } else if (minutesRemaining <= 3) { - element.style.backgroundColor = '#fff8e1'; - element.style.borderColor = '#ffecb3'; - element.style.color = '#f57f17'; - } - } - }); -} - -// 页面加载完成后执行高亮 -document.addEventListener('DOMContentLoaded', highlightExpiringCodes); \ No newline at end of file diff --git a/static/css/accounts.css b/static/css/accounts.css deleted file mode 100755 index 813da35..0000000 --- a/static/css/accounts.css +++ /dev/null @@ -1,702 +0,0 @@ -/** - * 2c2a 用户账户样式 - 现代毛玻璃风格 - * 特性:简洁优雅、响应式设计、深浅模式支持 - */ - -/* ==================== 页面基础布局 (Page Layout) ==================== */ -html, body { - margin: 0; - padding: 0; - overflow-x: hidden; - width: 100%; - height: 100%; - min-height: 100vh; - font-family: var(--font-family); -} - -body.login-page, -body.register-page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - position: relative; - overflow: hidden; - margin: 0; - padding: 0; - width: 100%; - min-width: 100vw; - - /* 浅色模式背景 */ - background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #f1f5f9 100%); - - /* 深色模式背景 */ - [data-theme="dark"] & { - background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%); - } -} - -/* 背景装饰元素 */ -body.login-page::before, -body.register-page::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: - radial-gradient(circle at 20% 30%, rgba(16, 185, 129, 0.08) 0%, transparent 50%), - radial-gradient(circle at 80% 70%, rgba(30, 58, 95, 0.08) 0%, transparent 50%); - z-index: 0; - pointer-events: none; -} - -[data-theme="dark"] body.login-page::before, -[data-theme="dark"] body.register-page::before { - background: - radial-gradient(circle at 20% 30%, rgba(16, 185, 129, 0.15) 0%, transparent 50%), - radial-gradient(circle at 80% 70%, rgba(96, 165, 250, 0.15) 0%, transparent 50%); -} - -/* 背景装饰圆圈 */ -.background-decor { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - pointer-events: none; -} - -.bg-circle { - position: absolute; - border-radius: 50%; - opacity: 0.4; - animation: floatGentle 12s infinite ease-in-out; -} - -.bg-circle:nth-child(1) { - width: 400px; - height: 400px; - top: -150px; - left: -150px; - background: radial-gradient(circle, rgba(16, 185, 129, 0.12) 0%, transparent 70%); - animation-delay: 0s; -} - -.bg-circle:nth-child(2) { - width: 300px; - height: 300px; - bottom: -120px; - right: 10%; - background: radial-gradient(circle, rgba(30, 58, 95, 0.12) 0%, transparent 70%); - animation-delay: 4s; -} - -.bg-circle:nth-child(3) { - width: 200px; - height: 200px; - top: 40%; - right: -80px; - background: radial-gradient(circle, rgba(6, 182, 212, 0.08) 0%, transparent 70%); - animation-delay: 8s; -} - -@keyframes floatGentle { - 0%, 100% { transform: translate(0, 0) scale(1); } - 25% { transform: translate(-15px, -15px) scale(1.05); } - 50% { transform: translate(15px, 15px) scale(0.95); } - 75% { transform: translate(-10px, 10px) scale(1.02); } -} - -/* ==================== 认证容器 (Auth Container) ==================== */ -.auth-container { - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-6); - position: relative; - z-index: 10; - margin: 0; - width: 100%; -} - -/* ==================== 认证卡片 (Auth Card) - 毛玻璃效果 ==================== */ -.auth-card { - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: var(--radius-xl); - padding: var(--spacing-10) var(--spacing-8); - width: 100%; - max-width: 440px; - position: relative; - overflow: hidden; - border: var(--glass-border); - box-shadow: var(--glass-shadow-lg); - - /* 内部光晕效果 */ - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent - ); - transition: left 0.6s ease; - } - - &:hover::before { - left: 100%; - } - - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - transform: translateY(-4px); - box-shadow: 0 20px 60px rgba(31, 38, 135, 0.25); - } -} - -/* ==================== 认证头部 (Auth Header) ==================== */ -.auth-header { - text-align: center; - margin-bottom: var(--margin-8); -} - -.auth-logo { - font-size: var(--font-size-4xl); - font-weight: var(--font-weight-bold); - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: var(--margin-4); - letter-spacing: 2px; - text-transform: uppercase; - display: inline-block; - transition: all var(--duration-normal) var(--ease-default); -} - -.auth-logo:hover { - transform: scale(1.05); - filter: drop-shadow(0 4px 12px rgba(var(--primary-color-rgb), 0.3)); -} - -.auth-title { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-semibold); - margin: 0; - color: var(--text-primary); - letter-spacing: -0.02em; -} - -.auth-subtitle { - font-size: var(--font-size-sm); - color: var(--text-secondary); - margin-top: var(--margin-2); - line-height: var(--line-height-relaxed); -} - -/* ==================== 表单样式 (Form Styles) ==================== */ -.auth-form { - margin-bottom: var(--margin-6); -} - -.form-group { - margin-bottom: var(--margin-5); - position: relative; -} - -.form-label { - display: block; - margin-bottom: var(--margin-2); - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - font-size: var(--font-size-sm); - letter-spacing: 0.01em; -} - -.form-control { - width: 100%; - padding: var(--input-padding-y) var(--input-padding-x); - font-size: var(--font-size-base); - line-height: var(--line-height-normal); - color: var(--text-primary); - background: var(--bg-glass-heavy); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - background-clip: padding-box; - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - transition: all var(--duration-normal) var(--ease-default); - outline: none; -} - -.form-control:hover { - border-color: var(--primary-color-light); - box-shadow: 0 0 0 4px rgba(var(--primary-color-rgb), 0.05); -} - -.form-control:focus { - color: var(--text-primary); - background: var(--surface-color-elevated); - border-color: var(--primary-color); - box-shadow: 0 0 0 4px rgba(var(--primary-color-rgb), 0.15), var(--shadow-sm); -} - -.form-control::placeholder { - color: var(--text-tertiary); - opacity: 0.7; -} - -/* 输入框验证状态 */ -.form-control.is-invalid { - border-color: var(--danger-color); - background-color: rgba(239, 68, 68, 0.05); - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ef4444' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ef4444' stroke='none'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} - -.invalid-feedback { - display: none; - width: 100%; - margin-top: var(--margin-2); - font-size: var(--font-size-xs); - color: var(--danger-color); - font-weight: var(--font-weight-medium); -} - -.form-control.is-invalid ~ .invalid-feedback { - display: block; -} - -.form-control.is-valid { - border-color: var(--success-color); - background-color: rgba(16, 185, 129, 0.05); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2310b981' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} - -/* ==================== 按钮样式 (Button Styles) ==================== */ -.btn-block { - display: flex; - width: 100%; - padding: var(--spacing-4) var(--spacing-6); - font-size: var(--font-size-base); - font-weight: var(--font-weight-semibold); - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--duration-normal) var(--ease-default); - position: relative; - overflow: hidden; - justify-content: center; - align-items: center; - gap: var(--spacing-2); - letter-spacing: 0.02em; - - /* 渐变背景 */ - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - color: white; - box-shadow: var(--shadow-primary); - - /* 波纹效果 */ - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.3); - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; - } - - &:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg), 0 10px 25px rgba(var(--primary-color-rgb), 0.3); - } - - &:hover::after { - width: 400px; - height: 400px; - } - - &:active { - transform: translateY(0); - } -} - -.btn-primary { - color: var(--text-inverse); - background-color: transparent; - border: none; -} - -.btn-link { - color: var(--primary-color); - text-decoration: none; - font-weight: var(--font-weight-medium); - transition: all var(--duration-fast) var(--ease-default); - position: relative; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 0; - height: 2px; - background: var(--primary-color); - transition: width var(--duration-fast) var(--ease-default); - } - - &:hover { - color: var(--secondary-color); - } - - &:hover::after { - width: 100%; - } -} - -/* ==================== 用户资料页面 (Profile Page) ==================== */ -.profile-container { - background: var(--surface-color); - border-radius: var(--radius-xl); - padding: var(--spacing-8); - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); - max-width: 900px; - margin: var(--margin-8) auto; -} - -.profile-header { - display: flex; - align-items: center; - margin-bottom: var(--margin-8); - padding-bottom: var(--margin-6); - border-bottom: 2px solid var(--border-color); - position: relative; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 120px; - height: 2px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); - } -} - -.profile-avatar { - width: 120px; - height: 120px; - border-radius: 50%; - object-fit: cover; - margin-right: var(--margin-6); - border: 4px solid var(--bg-glass); - backdrop-filter: blur(8px); - box-shadow: var(--shadow-lg); - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - transform: scale(1.05); - box-shadow: var(--shadow-xl), 0 0 30px rgba(var(--primary-color-rgb), 0.2); - } -} - -.profile-info { - flex: 1; -} - -.profile-name { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-bold); - margin-bottom: var(--margin-2); - color: var(--text-primary); -} - -.profile-email { - color: var(--text-secondary); - font-size: var(--font-size-sm); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.profile-actions { - display: flex; - gap: var(--spacing-3); -} - -/* ==================== 资料表单 (Profile Form) ==================== */ -.profile-form { - max-width: 700px; -} - -.form-section { - margin-bottom: var(--margin-8); - padding: var(--spacing-6); - background: var(--bg-secondary); - border-radius: var(--radius-lg); - border: 1px solid var(--border-color); - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - border-color: var(--primary-color-light); - box-shadow: var(--shadow-sm); - } -} - -.form-section-title { - font-size: var(--font-size-xl); - font-weight: var(--font-weight-semibold); - margin-bottom: var(--margin-5); - padding-bottom: var(--margin-3); - border-bottom: 2px solid var(--border-color); - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-3); - - &::before { - content: ''; - width: 4px; - height: 24px; - background: linear-gradient(180deg, var(--primary-color), var(--secondary-color)); - border-radius: var(--radius-full); - } -} - -.form-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-5); - margin-bottom: var(--margin-4); -} - -/* ==================== 登录历史 (Login History) ==================== */ -.login-history { - background: var(--bg-secondary); - border-radius: var(--radius-lg); - padding: var(--spacing-6); - border: 1px solid var(--border-color); -} - -.login-history-item { - padding: var(--spacing-5); - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - margin-bottom: var(--margin-4); - display: flex; - justify-content: space-between; - align-items: center; - transition: all var(--duration-fast) var(--ease-default); - background: var(--bg-primary); - - &:last-child { - margin-bottom: 0; - } - - &:hover { - border-color: var(--primary-color-light); - transform: translateX(4px); - box-shadow: var(--shadow-sm); - } -} - -.login-history-info { - flex: 1; -} - -.login-history-time { - font-weight: var(--font-weight-semibold); - margin-bottom: var(--margin-1); - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.login-history-details { - font-size: var(--font-size-sm); - color: var(--text-secondary); -} - -.login-history-status { - display: inline-flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-2) var(--spacing-4); - border-radius: var(--radius-full); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.login-history-status.success { - background: rgba(16, 185, 129, 0.15); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.login-history-status.failed { - background: rgba(239, 68, 68, 0.15); - color: #dc2626; - border: 1px solid rgba(239, 68, 68, 0.3); -} - -/* ==================== 消息提示样式 (Alert Messages) ==================== */ -.alert { - border-radius: var(--radius-lg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - background: var(--glass-bg); - border: var(--glass-border); - color: var(--text-primary); - box-shadow: var(--glass-shadow); - display: flex; - align-items: flex-start; - gap: var(--spacing-3); - animation: slideInDown 0.3s ease-out; -} - -@keyframes slideInDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.alert-success { - background: rgba(16, 185, 129, 0.12); - border-left: 4px solid var(--success-color); - color: #065f46; -} - -.alert-danger { - background: rgba(239, 68, 68, 0.12); - border-left: 4px solid var(--danger-color); - color: #991b1b; -} - -.alert-warning { - background: rgba(245, 158, 11, 0.12); - border-left: 4px solid var(--warning-color); - color: #92400e; -} - -.alert-info { - background: rgba(59, 130, 246, 0.12); - border-left: 4px solid var(--info-color); - color: #1e40af; -} - -.alert-dismissible .btn-close { - filter: grayscale(100%) brightness(0.7); - opacity: 0.7; - transition: all var(--duration-fast) var(--ease-default); - - &:hover { - filter: grayscale(0%) brightness(1); - opacity: 1; - } -} - -/* ==================== 页脚样式 (Footer) ==================== */ -footer { - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-top: var(--glass-border); - color: var(--text-secondary); - margin: 0; - padding: var(--spacing-6) 0; - position: relative; - width: 100%; - text-align: center; - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05); -} - -footer p { - color: var(--text-tertiary); - margin: 0; - font-size: var(--font-size-sm); -} - -/* ==================== 响应式设计 (Responsive Design) ==================== */ -@media (max-width: 768px) { - .auth-card { - padding: var(--spacing-8) var(--spacing-6); - margin: var(--margin-4); - max-width: calc(100% - 2rem); - } - - .auth-logo { - font-size: var(--font-size-3xl); - } - - .auth-title { - font-size: var(--font-size-xl); - } - - .background-decor { - display: none; - } - - footer { - position: relative; - } - - .profile-header { - flex-direction: column; - text-align: center; - } - - .profile-avatar { - margin-right: 0; - margin-bottom: var(--margin-4); - } - - .profile-actions { - justify-content: center; - } - - .form-row { - grid-template-columns: 1fr; - } - - .login-history-item { - flex-direction: column; - align-items: flex-start; - gap: var(--margin-3); - } - - .profile-container { - margin: var(--margin-4); - padding: var(--spacing-6); - } -} diff --git a/static/css/base.css b/static/css/base.css deleted file mode 100755 index 97081ff..0000000 --- a/static/css/base.css +++ /dev/null @@ -1,947 +0,0 @@ - /** - * 2c2a 基础样式文件 - * 特性:毛玻璃效果、现代化设计、响应式布局 - */ - -/* ==================== 全局样式重置与基础 ==================== */ -:root { - /* 兼容性颜色变量映射到新主题系统 */ - --primary-color: var(--primary-color); - --secondary-color: var(--secondary-color); - --success-color: var(--success-color); - --danger-color: var(--danger-color); - --warning-color: var(--warning-color); - --info-color: var(--info-color); - --light-color: var(--surface-color); - --dark-color: var(--text-primary); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; - color-scheme: dark; -} - -select option { - background-color: #1e293b; - color: #ffffff; -} - -select option:hover, -select option:checked { - background-color: #334155; - color: #ffffff; -} - -body { - font-family: var(--font-family); - font-size: var(--font-size-base); - line-height: var(--line-height-normal); - color: var(--text-primary); - background-color: var(--bg-secondary); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transition: background-color var(--duration-normal) var(--ease-default), - color var(--duration-normal) var(--ease-default); -} - -/* ==================== 毛玻璃基础类 (Glassmorphism Utilities) ==================== */ - -/* 标准毛玻璃效果 */ -.glass { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border: var(--glass-border); - box-shadow: var(--glass-shadow); -} - -/* 强毛玻璃效果 */ -.glass-heavy { - background: var(--glass-heavy-bg); - backdrop-filter: var(--glass-heavy-blur); - -webkit-backdrop-filter: var(--glass-heavy-blur); - border: var(--glass-border); - box-shadow: var(--glass-shadow-lg); -} - -/* 轻量毛玻璃效果 */ -.glass-light { - background: var(--glass-light-bg); - backdrop-filter: var(--glass-light-blur); - -webkit-backdrop-filter: var(--glass-light-blur); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -/* ==================== 导航栏样式 (Glassmorphism Navbar) ==================== */ -.navbar, -.md-navbar { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-bottom: var(--glass-border); - box-shadow: var(--shadow-md); - transition: all var(--duration-normal) var(--ease-default); - position: sticky; - top: 0; - z-index: 1000; -} - -.navbar-brand, -.md-navbar-brand { - font-weight: var(--font-weight-bold); - letter-spacing: 0.5px; - color: var(--primary-color) !important; - text-decoration: none !important; - display: flex; - align-items: center; - gap: var(--spacing-2); - transition: all var(--duration-fast) var(--ease-default); -} - -.navbar-brand:hover, -.md-navbar-brand:hover { - transform: translateY(-1px); - opacity: 0.9; -} - -/* ==================== 卡片样式 (Glass Card) ==================== */ -.card { - border: none; - border-radius: var(--card-radius); - background: var(--surface-color); - box-shadow: var(--card-shadow); - margin-bottom: var(--margin-6); - overflow: hidden; - transition: all var(--duration-normal) var(--ease-default); - position: relative; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - overflow: hidden; -} - -/* 毛玻璃卡片变体 */ -.card-glass { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border: var(--glass-border); - box-shadow: var(--glass-shadow); -} - -.card-header { - background: transparent; - border-bottom: 1px solid var(--border-color); - font-weight: var(--font-weight-medium); - padding: var(--spacing-5) var(--spacing-6); - color: var(--text-primary); -} - -.card-body { - padding: var(--spacing-6); -} - -/* ==================== 表格样式 (Modern Table) ==================== */ -.table { - margin-bottom: 0; - width: 100%; - border-collapse: separate; - border-spacing: 0; -} - -.table th { - border-top: none; - font-weight: var(--font-weight-semibold); - color: var(--text-secondary); - background-color: var(--bg-tertiary); - padding: var(--spacing-3) var(--spacing-4); - white-space: nowrap; - position: sticky; - top: 0; - z-index: 10; -} - -.table td { - padding: var(--spacing-3) var(--spacing-4); - border-top: 1px solid var(--border-color); - vertical-align: middle; -} - -.table tbody tr { - transition: all var(--duration-fast) var(--ease-default); -} - -.table tbody tr:hover { - background-color: var(--bg-tertiary); - transform: scale(1.01); -} - -/* ==================== 按钮样式 (Modern Buttons) ==================== */ -.btn { - border-radius: var(--button-radius); - padding: var(--button-padding-y) var(--button-padding-x); - font-weight: var(--font-weight-medium); - font-size: var(--font-size-sm); - transition: all var(--duration-normal) var(--ease-default); - cursor: pointer; - outline: none; - border: none; - position: relative; - overflow: hidden; - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-2); -} - -.btn::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.3); - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; -} - -.btn:hover::before { - width: 300px; - height: 300px; -} - -.btn:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-primary); -} - -.btn:active { - transform: translateY(0); -} - -/* 主要按钮 */ -.btn-primary { - background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); - color: var(--text-inverse); - box-shadow: var(--shadow-primary); -} - -/* 次要按钮(绿色) */ -.btn-secondary { - background: linear-gradient(135deg, var(--secondary-color), var(--secondary-color-light)); - color: var(--text-inverse); - box-shadow: var(--shadow-secondary); -} - -/* 成功按钮 */ -.btn-success { - background: linear-gradient(135deg, var(--success-color), var(--secondary-color-light)); - color: var(--text-inverse); -} - -/* 危险按钮 */ -.btn-danger { - background: linear-gradient(135deg, var(--danger-color), #f87171); - color: var(--text-inverse); -} - -/* 警告按钮 */ -.btn-warning { - background: linear-gradient(135deg, var(--warning-color), #fbbf24); - color: #000; -} - -/* 信息按钮 */ -.btn-info { - background: linear-gradient(135deg, var(--info-color), var(--accent-color)); - color: var(--text-inverse); -} - -/* 毛玻璃按钮 */ -.btn-glass { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border: var(--glass-border); - color: var(--text-primary); - box-shadow: var(--glass-shadow); -} - -.btn-glass:hover { - background: var(--glass-heavy-bg); - box-shadow: var(--glass-shadow-lg); -} - -/* ==================== 表单样式 (Modern Form Controls) ==================== */ -.form-control, -.form-select { - border-radius: var(--input-radius); - border: 2px solid var(--border-color); - padding: var(--input-padding-y) var(--input-padding-x); - font-size: var(--font-size-base); - line-height: var(--line-height-normal); - color: var(--text-primary); - background-color: var(--surface-color); - transition: all var(--duration-normal) var(--ease-default); - outline: none; - width: 100%; -} - -.form-control:focus, -.form-select:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 4px rgba(var(--primary-color-rgb), 0.15); - background-color: var(--surface-color-elevated); -} - -.form-control::placeholder { - color: var(--text-tertiary); -} - -/* 毛玻璃输入框 */ -.form-control-glass { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border: var(--glass-border); -} - -.form-control-glass:focus { - background: var(--glass-heavy-bg); - box-shadow: var(--glass-shadow-lg); -} - -/* 表单标签 */ -.form-label { - display: block; - margin-bottom: var(--spacing-2); - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - font-size: var(--font-size-sm); -} - -/* ==================== 徽章样式 (Modern Badges) ==================== */ -.badge { - padding: var(--spacing-1) var(--spacing-3); - font-weight: var(--font-weight-semibold); - border-radius: var(--radius-full); - font-size: var(--font-size-xs); - text-transform: uppercase; - letter-spacing: 0.5px; - display: inline-flex; - align-items: center; - gap: var(--spacing-1); -} - -.badge-success { - background: linear-gradient(135deg, var(--success-color), var(--secondary-color-light)); - color: var(--text-inverse); - box-shadow: 0 2px 8px rgba(var(--success-color-rgb), 0.25); -} - -.badge-danger { - background: linear-gradient(135deg, var(--danger-color), #f87171); - color: var(--text-inverse); - box-shadow: 0 2px 8px rgba(var(--danger-color-rgb), 0.25); -} - -.badge-warning { - background: linear-gradient(135deg, var(--warning-color), #fbbf24); - color: #000; - box-shadow: 0 2px 8px rgba(var(--warning-color-rgb), 0.25); -} - -.badge-info { - background: linear-gradient(135deg, var(--info-color), var(--accent-color)); - color: var(--text-inverse); - box-shadow: 0 2px 8px rgba(var(--info-color-rgb), 0.25); -} - -.badge-secondary { - background: var(--bg-tertiary); - color: var(--text-secondary); -} - -/* ==================== 统计卡片样式 (Stat Cards with Glass Effect) ==================== */ -.stat-card { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-radius: var(--card-radius); - padding: var(--spacing-8); - box-shadow: var(--glass-shadow); - border: var(--glass-border); - transition: all var(--duration-normal) var(--ease-default); - position: relative; - overflow: hidden; -} - -.stat-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color)); - opacity: 0; - transition: opacity var(--duration-normal) var(--ease-default); -} - -.stat-card:hover { - transform: translateY(-4px); - box-shadow: var(--glass-shadow-lg); -} - -.stat-card:hover::before { - opacity: 1; -} - -.stat-card .stat-value { - font-size: var(--font-size-3xl); - font-weight: var(--font-weight-bold); - margin: var(--margin-4) 0; - color: var(--primary-color); - line-height: var(--line-height-tight); -} - -.stat-card .stat-label { - font-size: var(--font-size-sm); - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: var(--font-weight-medium); -} - -.stat-card .stat-icon { - font-size: var(--font-size-3xl); - margin-bottom: var(--margin-3); - opacity: 0.8; -} - -/* ==================== 加载动画 (Loading States) ==================== */ -.spinner-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -} - -.loading-spinner { - width: 48px; - height: 48px; - border: 4px solid var(--border-color); - border-top-color: var(--primary-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ==================== 提示框/警告框样式 (Alert Boxes) ==================== */ -.alert { - border-radius: var(--radius-md); - padding: var(--spacing-4) var(--spacing-6); - margin-bottom: var(--margin-4); - border: none; - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - display: flex; - align-items: flex-start; - gap: var(--spacing-3); - animation: slideIn 0.3s ease-out; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.alert-success { - background: rgba(16, 185, 129, 0.15); - border-left: 4px solid var(--success-color); - color: var(--secondary-color-dark); -} - -.alert-danger { - background: rgba(239, 68, 68, 0.15); - border-left: 4px solid var(--danger-color); - color: #dc2626; -} - -.alert-warning { - background: rgba(245, 158, 11, 0.15); - border-left: 4px solid var(--warning-color); - color: #d97706; -} - -.alert-info { - background: rgba(59, 130, 246, 0.15); - border-left: 4px solid var(--info-color); - color: #2563eb; -} - -.alert-dismissible .btn-close { - filter: grayscale(100%); - opacity: 0.7; - transition: all var(--duration-fast) var(--ease-default); -} - -.alert-dismissible .btn-close:hover { - opacity: 1; - filter: grayscale(0%); -} - -/* ==================== 页面标题 (Page Header) ==================== */ -.page-header { - margin-bottom: var(--margin-8); - padding-bottom: var(--margin-6); - border-bottom: 2px solid var(--border-color); - position: relative; -} - -.page-header::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 80px; - height: 2px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); -} - -.page-header h1 { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-bold); - margin: 0; - color: var(--text-primary); -} - -/* ==================== 操作按钮组 (Action Buttons) ==================== */ -.action-buttons { - display: flex; - gap: var(--spacing-2); - align-items: center; - flex-wrap: wrap; -} - -.action-buttons .btn { - padding: var(--spacing-2) var(--spacing-4); - font-size: var(--font-size-sm); -} - -/* ==================== 状态指示器 (Status Indicators) ==================== */ -.status-indicator { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: var(--spacing-2); - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.status-indicator.online { - background: var(--success-color); - box-shadow: 0 0 8px rgba(var(--success-color-rgb), 0.5); -} - -.status-indicator.offline { - background: var(--danger-color); - box-shadow: 0 0 8px rgba(var(--danger-color-rgb), 0.5); -} - -.status-indicator.unknown { - background: var(--text-tertiary); -} - -.status-indicator.warning { - background: var(--warning-color); - box-shadow: 0 0 8px rgba(var(--warning-color-rgb), 0.5); -} - -/* ==================== 菜单样式 (Menu Components) ==================== */ -.md-menu { - position: relative; - display: inline-block; -} - -.md-menu__trigger { - background: transparent; - border: none; - padding: var(--spacing-2) var(--spacing-4); - color: var(--text-secondary); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--duration-fast) var(--ease-default); -} - -.md-menu__trigger:hover { - background: var(--bg-tertiary); - color: var(--text-primary); -} - -.md-menu__content { - position: absolute; - top: 100%; - right: 0; - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-radius: var(--radius-md); - box-shadow: var(--glass-shadow-lg); - border: var(--glass-border); - min-width: 200px; - padding: var(--spacing-2) 0; - margin-top: var(--spacing-2); - z-index: 1001; - display: none; - animation: fadeInDown 0.2s ease-out; -} - -@keyframes fadeInDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.md-menu:hover .md-menu__content { - display: block; -} - -.md-menu__item { - display: block; - padding: var(--spacing-3) var(--spacing-6); - color: var(--text-primary); - text-decoration: none; - font-size: var(--font-size-sm); - transition: all var(--duration-fast) var(--ease-default); -} - -.md-menu__item:hover { - background: var(--bg-tertiary); - padding-left: var(--spacing-7); -} - -.md-divider { - margin: var(--spacing-2) 0; - border: none; - border-top: 1px solid var(--border-color); -} - -/* ==================== 主题切换按钮 (Theme Toggle) ==================== */ -.theme-toggle { - position: relative; - width: 56px; - height: 28px; - background: var(--bg-tertiary); - border-radius: var(--radius-full); - cursor: pointer; - transition: all var(--duration-normal) var(--ease-default); - border: 2px solid var(--border-color); - display: flex; - align-items: center; - padding: 0 4px; -} - -.theme-toggle:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow-sm); -} - -.theme-toggle-knob { - width: 20px; - height: 20px; - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - border-radius: 50%; - transition: all var(--duration-normal) var(--ease-bounce); - display: flex; - align-items: center; - justify-content: center; - box-shadow: var(--shadow-md); -} - -.theme-toggle-knob i { - font-size: 12px; - color: white; -} - -[data-theme="dark"] .theme-toggle { - background: var(--surface-color-elevated); -} - -[data-theme="dark"] .theme-toggle-knob { - transform: translateX(28px); - background: linear-gradient(135deg, #fbbf24, #f59e0b); -} - -/* ==================== 响应式设计 (Responsive Design) ==================== */ -@media (max-width: 640px) { - :root { - --navbar-height: var(--navbar-height-mobile); - } - - .card { - margin-bottom: var(--margin-4); - } - - .action-buttons { - flex-direction: column; - width: 100%; - } - - .action-buttons .btn { - width: 100%; - } - - .stat-card { - padding: var(--spacing-5); - } - - .stat-card .stat-value { - font-size: var(--font-size-2xl); - } - - .page-header h1 { - font-size: var(--font-size-xl); - } - - .table { - font-size: var(--font-size-sm); - } - - .table th, - .table td { - padding: var(--spacing-2) var(--spacing-3); - } -} - -@media (max-width: 768px) { - .card { - margin-bottom: var(--margin-4); - } - - .action-buttons { - flex-direction: column; - } - - .md-main-content { - padding: var(--spacing-4); - } -} - -/* ==================== 工具类 (Utility Classes) ==================== */ - -/* 文本工具类 */ -.text-gradient { - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.text-glow { - text-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5); -} - -/* 阴影工具类 */ -.shadow-glow { - box-shadow: var(--shadow-primary); -} - -.shadow-glow-secondary { - box-shadow: var(--shadow-secondary); -} - -/* 动画工具类 */ -.hover-lift { - transition: transform var(--duration-normal) var(--ease-default); -} - -.hover-lift:hover { - transform: translateY(-4px); -} - -.hover-scale { - transition: transform var(--duration-normal) var(--ease-default); -} - -.hover-scale:hover { - transform: scale(1.05); -} - -/* 渐入动画 */ -.fade-in { - animation: fadeIn 0.3s ease-out; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 从下方滑入 */ -.slide-up { - animation: slideUp 0.4s ease-out; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ==================== 页脚样式 (Footer - Glassmorphism) ==================== */ -.footer-glass { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-top: var(--glass-border); - color: var(--text-secondary); - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05); - transition: all var(--duration-normal) var(--ease-default); -} - -.footer-glass p { - color: var(--text-tertiary); - margin: 0; - font-size: var(--font-size-sm); -} - -[data-theme="dark"] .footer-glass { - background: rgba(15, 23, 42, 0.8); - border-top-color: rgba(255, 255, 255, 0.1); - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); -} - -/* ==================== 模态框样式 (Modal) ==================== */ -.modal-content { - background-color: var(--surface-color); - color: var(--text-primary); - border: 1px solid var(--border-color); -} - -.modal-header { - border-bottom-color: var(--border-color); - background-color: var(--surface-color); -} - -.modal-title { - color: var(--text-primary); -} - -.modal-footer { - border-top-color: var(--border-color); - background-color: var(--surface-color); -} - -.btn-close { - filter: var(--btn-close-filter, none); -} - -[data-theme="dark"] .modal-content { - background-color: var(--surface-color-elevated); - box-shadow: var(--shadow-xl); -} - -[data-theme="dark"] .modal-header { - background-color: var(--surface-color-elevated); - border-bottom-color: var(--border-color); -} - -[data-theme="dark"] .modal-title { - color: var(--text-primary); -} - -[data-theme="dark"] .modal-footer { - background-color: var(--surface-color-elevated); - border-top-color: var(--border-color); -} - -[data-theme="dark"] .btn-close { - --btn-close-filter: invert(1) grayscale(100%) brightness(200%); -} - -[data-theme="dark"] .form-control, -[data-theme="dark"] .form-select { - background-color: var(--bg-secondary); - color: var(--text-primary); - border-color: var(--border-color); -} - -[data-theme="dark"] .form-control:focus, -[data-theme="dark"] .form-select:focus { - background-color: var(--surface-color); - color: var(--text-primary); -} - -[data-theme="dark"] .form-label { - color: var(--text-secondary); -} - -/* ==================== 主题感知按钮 (Theme Aware Buttons) ==================== */ -.theme-aware-btn.btn-primary { - background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); - color: var(--text-inverse); - box-shadow: var(--shadow-primary); - border: 1px solid var(--primary-color); -} - -[data-theme="dark"] .theme-aware-btn.btn-primary { - background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); - color: #0f172a; - box-shadow: 0 4px 14px 0 rgba(var(--primary-color-rgb), 0.45); -} - -.theme-aware-btn.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-primary); -} diff --git a/static/css/dashboard.css b/static/css/dashboard.css deleted file mode 100755 index cc00309..0000000 --- a/static/css/dashboard.css +++ /dev/null @@ -1,497 +0,0 @@ -/** - * 2c2a 仪表盘样式 - * 特性:毛玻璃效果、渐变色彩、现代化设计 - */ - -/* ==================== 统计卡片容器 (Stats Container) ==================== */ -.stats-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-6); - margin-bottom: var(--margin-8); -} - -/* ==================== 统计卡片 (Stat Cards) ==================== */ -.stat-card { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-radius: var(--radius-xl); - padding: var(--spacing-8); - border: var(--glass-border); - box-shadow: var(--glass-shadow); - transition: all var(--duration-normal) var(--ease-default); - position: relative; - overflow: hidden; - cursor: pointer; -} - -/* 卡片顶部装饰条 */ -.stat-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--card-accent, var(--primary-color)), transparent); - opacity: 0; - transition: opacity var(--duration-normal) var(--ease-default); -} - -/* 悬停时的光晕效果 */ -.stat-card::after { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: radial-gradient(circle, rgba(var(--card-accent-rgb, var(--primary-color-rgb)), 0.1) 0%, transparent 70%); - opacity: 0; - transition: opacity var(--duration-slow) var(--ease-default); - pointer-events: none; -} - -.stat-card:hover { - transform: translateY(-6px) scale(1.02); - box-shadow: var(--glass-shadow-lg), 0 20px 40px rgba(var(--card-accent-rgb, var(--primary-color-rgb)), 0.15); -} - -.stat-card:hover::before { - opacity: 1; -} - -.stat-card:hover::after { - opacity: 1; -} - -/* 不同颜色的统计卡片变体 */ -.stat-card.primary { - --card-accent: #1e3a5f; - --card-accent-rgb: 30, 58, 95; - background: linear-gradient(135deg, rgba(30, 58, 95, 0.08) 0%, rgba(16, 185, 129, 0.05) 100%); -} - -.stat-card.success { - --card-accent: #10b981; - --card-accent-rgb: 16, 185, 129; - background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(6, 182, 212, 0.05) 100%); -} - -.stat-card.info { - --card-accent: #3b82f6; - --card-accent-rgb: 59, 130, 246; - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(139, 92, 246, 0.05) 100%); -} - -.stat-card.warning { - --card-accent: #f59e0b; - --card-accent-rgb: 245, 158, 11; - background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(251, 191, 36, 0.05) 100%); -} - -/* 图标样式 */ -.stat-card .stat-icon { - font-size: var(--font-size-4xl); - margin-bottom: var(--margin-4); - display: inline-flex; - align-items: center; - justify-content: center; - width: 64px; - height: 64px; - border-radius: var(--radius-lg); - background: var(--bg-glass); - backdrop-filter: blur(8px); - color: var(--card-accent, var(--primary-color)); - box-shadow: 0 4px 12px rgba(var(--card-accent-rgb, var(--primary-color-rgb)), 0.15); - transition: all var(--duration-normal) var(--ease-default); -} - -.stat-card:hover .stat-icon { - transform: scale(1.1) rotate(5deg); - box-shadow: 0 8px 20px rgba(var(--card-accent-rgb, var(--primary-color-rgb)), 0.25); -} - -/* 数值样式 */ -.stat-card .stat-value { - font-size: var(--font-size-4xl); - font-weight: var(--font-weight-bold); - margin: var(--margin-4) 0 var(--margin-2); - color: var(--text-primary); - line-height: var(--line-height-tight); - letter-spacing: -0.02em; -} - -/* 标签样式 */ -.stat-card .stat-label { - font-size: var(--font-size-sm); - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 1px; - font-weight: var(--font-weight-semibold); - opacity: 0.8; -} - -/* ==================== 图表容器 (Chart Container) ==================== */ -.chart-container { - position: relative; - height: 350px; - width: 100%; - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-radius: var(--radius-xl); - padding: var(--spacing-6); - border: var(--glass-border); - box-shadow: var(--glass-shadow); - overflow: hidden; -} - -.chart-wrapper { - padding: var(--spacing-4); - height: 100%; - position: relative; -} - -/* ==================== 操作记录表格 (Operation Table) ==================== */ -.operation-table { - background: var(--surface-color); - border-radius: var(--radius-xl); - overflow: hidden; - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); -} - -.operation-table thead { - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - border-bottom: 2px solid var(--border-color); -} - -.operation-table thead th { - font-weight: var(--font-weight-semibold); - color: var(--text-secondary); - text-transform: uppercase; - font-size: var(--font-size-xs); - letter-spacing: 0.5px; - padding: var(--spacing-4) var(--spacing-5); - white-space: nowrap; -} - -.operation-table tbody tr { - transition: all var(--duration-fast) var(--ease-default); - border-bottom: 1px solid var(--border-color-light); -} - -.operation-table tbody tr:last-child { - border-bottom: none; -} - -.operation-table tbody tr:hover { - background: var(--bg-tertiary); - transform: scale(1.01); - box-shadow: inset 0 0 0 2px var(--primary-color); -} - -.operation-table tbody td { - padding: var(--spacing-4) var(--spacing-5); - vertical-align: middle; -} - -/* ==================== 组件配置页面 (Widget Configuration) ==================== */ -.widget-config-list { - background: var(--surface-color); - border-radius: var(--radius-xl); - padding: var(--spacing-8); - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); -} - -.widget-item { - padding: var(--spacing-5); - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - margin-bottom: var(--margin-4); - transition: all var(--duration-normal) var(--ease-default); - background: var(--bg-primary); - position: relative; - overflow: hidden; -} - -.widget-item::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: linear-gradient(180deg, var(--primary-color), var(--secondary-color)); - opacity: 0; - transition: opacity var(--duration-fast) var(--ease-default); -} - -.widget-item:last-child { - margin-bottom: 0; -} - -.widget-item:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow-lg), 0 0 30px rgba(var(--primary-color-rgb), 0.1); - transform: translateX(4px); -} - -.widget-item:hover::before { - opacity: 1; -} - -.widget-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--margin-3); -} - -.widget-type { - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-lg); - color: var(--primary-color); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.widget-controls { - display: flex; - gap: var(--spacing-4); - align-items: center; -} - -/* ==================== 开关样式 (Toggle Switch) ==================== */ -.form-switch .form-check-input { - width: 48px; - height: 24px; - cursor: pointer; - background-color: var(--bg-tertiary); - border: none; - position: relative; - transition: all var(--duration-normal) var(--ease-default); -} - -.form-switch .form-check-input:checked { - background-color: var(--secondary-color); - box-shadow: 0 0 12px rgba(var(--success-color-rgb), 0.4); -} - -.form-switch .form-check-input::before { - width: 20px; - height: 20px; - background: white; - border-radius: 50%; - box-shadow: var(--shadow-md); - transition: all var(--duration-normal) var(--ease-bounce); -} - -.form-switch .form-check-input:checked::before { - transform: translateX(24px); -} - -/* ==================== 加载状态 (Loading States) ==================== */ -.loading-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--glass-bg); - backdrop-filter: var(--glass-blur); - -webkit-backdrop-filter: var(--glass-blur); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -} - -.loading-spinner { - width: 56px; - height: 56px; - border: 4px solid var(--border-color); - border-top-color: var(--primary-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; - position: relative; -} - -.loading-spinner::before, -.loading-spinner::after { - content: ''; - position: absolute; - border-radius: 50%; - border: 4px solid transparent; -} - -.loading-spinner::before { - top: 4px; - left: 4px; - right: 4px; - bottom: 4px; - border-top-color: var(--secondary-color); - animation: spin 1.5s linear infinite reverse; -} - -.loading-spinner::after { - top: 10px; - left: 10px; - right: 10px; - bottom: 10px; - border-top-color: var(--accent-color); - animation: spin 2s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* ==================== 动画效果 (Animations) ==================== */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.fade-in { - animation: fadeInUp 0.4s ease-out; -} - -/* 渐入动画延迟(用于卡片依次出现) */ -.stats-container .stat-card:nth-child(1) { animation-delay: 0ms; } -.stats-container .stat-card:nth-child(2) { animation-delay: 100ms; } -.stats-container .stat-card:nth-child(3) { animation-delay: 200ms; } -.stats-container .stat-card:nth-child(4) { animation-delay: 300ms; } - -.stats-container .stat-card { - animation: fadeInUp 0.5s ease-out both; -} - -/* ==================== 状态徽章 (Status Badges) ==================== */ -.status-badge { - display: inline-flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-2) var(--spacing-4); - border-radius: var(--radius-full); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - text-transform: uppercase; - letter-spacing: 0.5px; - position: relative; - overflow: hidden; -} - -.status-badge::before { - content: ''; - width: 8px; - height: 8px; - border-radius: 50%; - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.6; transform: scale(0.9); } -} - -.status-badge.success { - background: rgba(16, 185, 129, 0.15); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.status-badge.success::before { - background: #10b981; - box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); -} - -.status-badge.failed { - background: rgba(239, 68, 68, 0.15); - color: #dc2626; - border: 1px solid rgba(239, 68, 68, 0.3); -} - -.status-badge.failed::before { - background: #ef4444; - box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); -} - -.status-badge.pending { - background: rgba(245, 158, 11, 0.15); - color: #d97706; - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.status-badge.pending::before { - background: #f59e0b; - box-shadow: 0 0 8px rgba(245, 158, 11, 0.6); -} - -/* ==================== 响应式设计 (Responsive Design) ==================== */ -@media (max-width: 768px) { - .stats-container { - grid-template-columns: 1fr; - gap: var(--spacing-4); - } - - .stat-card { - padding: var(--spacing-6); - } - - .stat-card .stat-value { - font-size: var(--font-size-3xl); - } - - .chart-container { - height: 280px; - padding: var(--spacing-4); - } - - .widget-controls { - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-3); - } - - .operation-table { - font-size: var(--font-size-sm); - } - - .operation-table th, - .operation-table td { - padding: var(--spacing-3); - } - - .widget-config-list { - padding: var(--spacing-5); - } -} - -@media (max-width: 480px) { - .stats-container { - grid-template-columns: 1fr; - } - - .stat-card .stat-icon { - width: 48px; - height: 48px; - font-size: var(--font-size-2xl); - } - - .stat-card .stat-value { - font-size: var(--font-size-2xl); - } -} diff --git a/static/css/operations.css b/static/css/operations.css deleted file mode 100755 index 551e772..0000000 --- a/static/css/operations.css +++ /dev/null @@ -1,859 +0,0 @@ -/** - * 2c2a 操作记录相关页面样式 - * 特性:毛玻璃效果、现代化设计、响应式布局 - */ - -/* ==================== 页面基础 (Page Base) ==================== */ -.operations-page { - padding: var(--spacing-6) 0; -} - -.page-header { - margin-bottom: var(--margin-8); - padding-bottom: var(--margin-6); - border-bottom: 2px solid var(--border-color); - position: relative; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 100px; - height: 2px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); - } -} - -/* ==================== 卡片样式 (Cards) ==================== */ -.card { - background: var(--surface-color); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); - overflow: hidden; - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - border-color: var(--primary-color-light); - overflow: hidden; - } -} - -.card-title { - color: var(--text-primary); - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-xl); - padding-bottom: var(--margin-4); - margin-bottom: var(--margin-5); - border-bottom: 2px solid var(--border-color); - display: flex; - align-items: center; - gap: var(--spacing-3); - - &::before { - content: ''; - width: 4px; - height: 24px; - background: linear-gradient(180deg, var(--primary-color), var(--secondary-color)); - border-radius: var(--radius-full); - } -} - -.card-body { - padding: var(--spacing-6); -} - -/* ==================== 任务列表 (Task List) ==================== */ -.task-list { - margin-top: var(--margin-5); -} - -.task-item { - padding: var(--spacing-5); - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - margin-bottom: var(--margin-4); - transition: all var(--duration-normal) var(--ease-default); - background: var(--bg-primary); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - background: linear-gradient(180deg, var(--primary-color), var(--secondary-color)); - opacity: 0; - transition: opacity var(--duration-fast) var(--ease-default); - } - - &:last-child { - margin-bottom: 0; - } - - &:hover { - border-color: var(--primary-color); - transform: translateX(4px); - box-shadow: var(--shadow-md); - - &::before { - opacity: 1; - } - } -} - -.task-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--margin-3); -} - -.task-name { - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-lg); - margin: 0; - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.task-type { - font-size: var(--font-size-sm); - color: var(--text-secondary); - background: var(--bg-tertiary); - padding: var(--spacing-1) var(--spacing-3); - border-radius: var(--radius-full); - font-weight: var(--font-weight-medium); -} - -/* 进度条 */ -.task-progress { - height: 8px; - background: var(--bg-tertiary); - border-radius: var(--radius-full); - overflow: hidden; - margin: var(--margin-3) 0; - position: relative; -} - -.task-progress-bar { - height: 100%; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); - border-radius: var(--radius-full); - transition: width var(--duration-slow) var(--ease-default); - position: relative; - overflow: hidden; - - /* 动态光效 */ - &::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.4), - transparent - ); - animation: shimmer 2s infinite; - } -} - -@keyframes shimmer { - to { left: 100%; } -} - -/* 状态徽章 */ -.task-status { - display: inline-flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-2) var(--spacing-4); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-bold); - line-height: 1; - text-align: center; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.task-status.pending { - background: rgba(245, 158, 11, 0.15); - color: #d97706; - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.task-status.running { - background: rgba(6, 182, 212, 0.15); - color: #0891b2; - border: 1px solid rgba(6, 182, 212, 0.3); - animation: pulseGlow 2s ease-in-out infinite; -} - -@keyframes pulseGlow { - 0%, 100% { box-shadow: 0 0 0 0 rgba(6, 182, 212, 0.4); } - 50% { box-shadow: 0 0 0 8px rgba(6, 182, 212, 0); } -} - -.task-status.success { - background: rgba(16, 185, 129, 0.15); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.task-status.failed { - background: rgba(239, 68, 68, 0.15); - color: #dc2626; - border: 1px solid rgba(239, 68, 68, 0.3); -} - -.task-status.cancelled { - background: rgba(107, 114, 128, 0.15); - color: #4b5563; - border: 1px solid rgba(107, 114, 128, 0.3); -} - -/* ==================== 申请列表 (Application List) ==================== */ -.application-list { - margin-top: var(--margin-5); -} - -.application-item { - padding: var(--spacing-5); - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - margin-bottom: var(--margin-4); - transition: all var(--duration-normal) var(--ease-default); - background: var(--bg-primary); - - &:last-child { - margin-bottom: 0; - } - - &:hover { - border-color: var(--secondary-color); - box-shadow: var(--shadow-md); - transform: translateY(-2px); - } -} - -.application-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--margin-3); -} - -.application-user { - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-lg); - margin: 0; - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.application-status { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); -} - -/* ==================== 表单样式 (Form Styles) ==================== */ -.form-label { - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - margin-bottom: var(--margin-2); - font-size: var(--font-size-sm); - letter-spacing: 0.01em; -} - -.form-control { - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - padding: var(--input-padding-y) var(--input-padding-x); - font-size: var(--font-size-base); - line-height: var(--line-height-normal); - transition: all var(--duration-normal) var(--ease-default); - background: var(--surface-color); - color: var(--text-primary); - outline: none; -} - -.form-control:hover { - border-color: var(--primary-color-light); -} - -.form-control:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 4px rgba(var(--primary-color-rgb), 0.12); - background: var(--surface-color-elevated); -} - -.form-text { - color: var(--text-tertiary); - font-size: var(--font-size-sm); - margin-top: var(--margin-2); -} - -/* ==================== 按钮样式 (Button Styles) ==================== */ -.btn { - padding: var(--button-padding-y) var(--button-padding-x); - font-size: var(--font-size-base); - border-radius: var(--button-radius); - cursor: pointer; - transition: all var(--duration-normal) var(--ease-default); - font-weight: var(--font-weight-medium); - display: inline-flex; - align-items: center; - gap: var(--spacing-2); - border: none; - outline: none; - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.3); - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; - } - - &:hover::before { - width: 300px; - height: 300px; - } - - &:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); - } - - &:active { - transform: translateY(0); - } -} - -.btn-primary { - background: linear-gradient(135deg, var(--primary-color), var(--primary-color-light)); - color: white; - box-shadow: var(--shadow-primary); -} - -.btn-secondary { - background: linear-gradient(135deg, var(--secondary-color), var(--secondary-color-light)); - color: white; - box-shadow: var(--shadow-secondary); -} - -.btn-success { - background: linear-gradient(135deg, var(--success-color), var(--secondary-color-light)); - color: white; -} - -.btn-danger { - background: linear-gradient(135deg, var(--danger-color), #f87171); - color: white; -} - -.btn-warning { - background: linear-gradient(135deg, var(--warning-color), #fbbf24); - color: #000; -} - -.btn-info { - background: linear-gradient(135deg, var(--info-color), var(--accent-color)); - color: white; -} - -/* ==================== 过滤器样式 (Filter Section) ==================== */ -.filter-section { - margin-bottom: var(--margin-6); - padding: var(--spacing-5); - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border-radius: var(--radius-lg); - border: var(--glass-border); - box-shadow: var(--glass-shadow); -} - -.filter-form { - margin-bottom: 0; -} - -.filter-label { - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - font-size: var(--font-size-sm); - margin-bottom: var(--margin-2); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* ==================== 表格样式 (Table Styles) ==================== */ -.table { - width: 100%; - margin-bottom: 0; - border-collapse: separate; - border-spacing: 0; -} - -.table th { - border-top: none; - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - font-weight: var(--font-weight-semibold); - color: var(--text-secondary); - text-transform: uppercase; - font-size: var(--font-size-xs); - letter-spacing: 0.5px; - padding: var(--spacing-4) var(--spacing-5); - white-space: nowrap; - border-bottom: 2px solid var(--border-color); -} - -.table td { - padding: var(--spacing-4) var(--spacing-5); - vertical-align: middle; - border-top: 1px solid var(--border-color-light); - color: var(--text-primary); -} - -.table tbody tr { - transition: all var(--duration-fast) var(--ease-default); -} - -.table tbody tr:hover { - background: var(--bg-tertiary); - transform: scale(1.005); -} - -/* ==================== 状态徽章 (Status Badges) ==================== */ -.status-badge { - padding: var(--spacing-2) var(--spacing-4); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-bold); - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.5px; - display: inline-flex; - align-items: center; - gap: var(--spacing-2); -} - -.status-pending { - background: rgba(245, 158, 11, 0.15); - color: #d97706; - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.status-approved, -.status-completed { - background: rgba(16, 185, 129, 0.15); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.status-rejected, -.status-failed { - background: rgba(239, 68, 68, 0.15); - color: #dc2626; - border: 1px solid rgba(239, 68, 68, 0.3); -} - -.status-processing, -.status-running { - background: rgba(6, 182, 212, 0.15); - color: #0891b2; - border: 1px solid rgba(6, 182, 212, 0.3); -} - -/* ==================== 操作按钮组 (Action Buttons) ==================== */ -.action-buttons { - display: flex; - gap: var(--spacing-2); - align-items: center; - flex-wrap: wrap; -} - -.action-buttons .btn { - padding: var(--spacing-2) var(--spacing-4); - font-size: var(--font-size-sm); -} - -/* ==================== 响应式表格 (Responsive Table) ==================== */ -.table-responsive { - overflow-x: auto; - border-radius: var(--radius-lg); - -ms-overflow-style: none; - scrollbar-width: none; -} - -/* 隐藏滚动条但保留功能 */ -.table-responsive::-webkit-scrollbar { - height: 0; - width: 0; -} - -/* ==================== 云电脑用户列表 (User Cards) ==================== */ -.user-card { - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--spacing-5); - margin-bottom: var(--margin-4); - transition: all var(--duration-normal) var(--ease-default); - background: var(--bg-primary); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); - opacity: 0; - transition: opacity var(--duration-fast) var(--ease-default); - } - - &:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow-lg); - transform: translateY(-4px); - - &::before { - opacity: 1; - } - } -} - -.user-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--margin-4); -} - -.user-name { - font-weight: var(--font-weight-semibold); - font-size: var(--font-size-lg); - margin: 0; - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.user-status { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); -} - -/* ==================== 信息组样式 (Info Groups) ==================== */ -.info-group { - margin-bottom: var(--margin-5); - padding-bottom: var(--margin-5); - border-bottom: 1px solid var(--border-color); - - &:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; - } -} - -.info-label { - font-weight: var(--font-weight-semibold); - color: var(--text-secondary); - font-size: var(--font-size-sm); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: var(--margin-2); -} - -.info-value { - font-size: var(--font-size-base); - color: var(--text-primary); - word-break: break-word; - font-weight: var(--font-weight-medium); -} - -/* ==================== 确认页面样式 (Confirmation Page) ==================== */ -.confirmation-section { - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - padding: var(--spacing-6); - border-radius: var(--radius-xl); - border: var(--glass-border); - margin: var(--margin-6) 0; - box-shadow: var(--glass-shadow); -} - -.confirmation-header { - font-size: var(--font-size-xl); - font-weight: var(--font-weight-bold); - margin-bottom: var(--margin-5); - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-3); - - &::before { - content: '✓'; - width: 32px; - height: 32px; - background: linear-gradient(135deg, var(--success-color), var(--secondary-color-light)); - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--font-size-sm); - } -} - -.confirmation-details { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-5); - margin-bottom: var(--margin-6); -} - -.confirmation-item { - padding: var(--spacing-4); - background: var(--surface-color-elevated); - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - transition: all var(--duration-fast) var(--ease-default); - - &:hover { - border-color: var(--primary-color-light); - box-shadow: var(--shadow-sm); - } -} - -.confirmation-label { - font-weight: var(--font-weight-medium); - color: var(--text-secondary); - font-size: var(--font-size-sm); - margin-bottom: var(--margin-2); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.confirmation-value { - font-size: var(--font-size-lg); - color: var(--text-primary); - font-weight: var(--font-weight-semibold); -} - -/* ==================== 产品卡片样式 (Product Cards) ==================== */ -.product-card { - border: 2px solid var(--border-color); - border-radius: var(--radius-xl); - padding: var(--spacing-6); - margin-bottom: var(--margin-5); - transition: all var(--duration-normal) var(--ease-default); - background: var(--bg-primary); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color)); - opacity: 0; - transition: opacity var(--duration-fast) var(--ease-default); - } - - &:hover { - border-color: var(--primary-color); - box-shadow: var(--shadow-lg), 0 10px 30px rgba(var(--primary-color-rgb), 0.15); - transform: translateY(-4px); - - &::before { - opacity: 1; - } - } -} - -.product-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--margin-5); -} - -.product-name { - font-size: var(--font-size-xl); - font-weight: var(--font-weight-bold); - margin: 0; - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-3); -} - -.product-status { - display: inline-flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-2) var(--spacing-4); - border-radius: var(--radius-full); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-semibold); -} - -.product-status.online { - background: rgba(16, 185, 129, 0.15); - color: #059669; - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.product-status.offline { - background: rgba(239, 68, 68, 0.15); - color: #dc2626; - border: 1px solid rgba(239, 68, 68, 0.3); -} - -.product-status.error { - background: rgba(245, 158, 11, 0.15); - color: #d97706; - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.product-info { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: var(--spacing-5); - margin-bottom: var(--margin-5); -} - -.product-info-item { - display: flex; - flex-direction: column; - gap: var(--spacing-1); -} - -.product-info-label { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: var(--font-weight-medium); -} - -.product-info-value { - font-size: var(--font-size-base); - color: var(--text-primary); - font-weight: var(--font-weight-semibold); -} - -.product-description { - margin-top: var(--margin-5); - padding-top: var(--margin-5); - border-top: 1px solid var(--border-color); - color: var(--text-secondary); - line-height: var(--line-height-relaxed); -} - -/* 隐藏目标产品字段 */ -#id_target_product { - display: none; -} - -.target-product-display { - background: var(--bg-tertiary); - padding: var(--spacing-4); - border-radius: var(--radius-md); - margin-bottom: var(--margin-4); - border-left: 4px solid var(--info-color); -} - -/* 密码复杂度要求 */ -.password-requirements ul { - margin-bottom: 0; - padding-left: 1.5em; -} - -.password-requirements li { - margin-bottom: var(--margin-2); - font-size: var(--font-size-sm); - color: var(--text-secondary); -} - -/* ==================== 响应式设计 (Responsive Design) ==================== */ -@media (max-width: 768px) { - .row { - margin-left: calc(var(--spacing-4) * -1); - margin-right: calc(var(--spacing-4) * -1); - } - - .col-md-6 { - padding-left: var(--spacing-4); - padding-right: var(--spacing-4); - } - - .confirmation-details { - grid-template-columns: 1fr; - } - - .action-buttons { - flex-direction: column; - width: 100%; - } - - .action-buttons .btn { - width: 100%; - justify-content: center; - } - - .product-info { - grid-template-columns: 1fr; - } - - .user-header { - flex-direction: column; - align-items: flex-start; - gap: var(--margin-3); - } - - .task-header { - flex-direction: column; - align-items: flex-start; - gap: var(--margin-3); - } - - .application-header { - flex-direction: column; - align-items: flex-start; - gap: var(--margin-3); - } -} diff --git a/static/css/profile.css b/static/css/profile.css deleted file mode 100755 index 2b751f9..0000000 --- a/static/css/profile.css +++ /dev/null @@ -1,449 +0,0 @@ -/** - * 2c2a 用户资料页面样式 - 现代毛玻璃风格 - * 特性:优雅简洁、响应式设计、深浅模式支持 - */ - -/* ==================== 用户资料容器 (Profile Container) ==================== */ -.profile-container { - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: var(--radius-xl); - padding: var(--spacing-8); - border: var(--glass-border); - box-shadow: var(--glass-shadow-lg); - margin: var(--margin-8) auto; - max-width: 1000px; - position: relative; - overflow: hidden; - - /* 背景装饰 */ - &::before { - content: ''; - position: absolute; - top: -50%; - right: -50%; - width: 100%; - height: 100%; - background: radial-gradient(circle, rgba(var(--primary-color-rgb), 0.05) 0%, transparent 70%); - pointer-events: none; - } -} - -.profile-content { - display: flex; - gap: var(--spacing-8); - position: relative; - z-index: 1; -} - -/* ==================== 侧边栏 (Sidebar) ==================== */ -.profile-sidebar { - flex: 0 0 280px; - text-align: center; -} - -/* ==================== 主内容区 (Main Content) ==================== */ -.profile-main { - flex: 1; - min-width: 0; /* 防止flex子项溢出 */ -} - -/* ==================== 头像样式 (Avatar) ==================== */ -.profile-avatar { - width: 140px; - height: 140px; - border-radius: 50%; - object-fit: cover; - margin-bottom: var(--margin-5); - border: 4px solid var(--bg-glass-heavy); - backdrop-filter: blur(12px); - box-shadow: - var(--shadow-lg), - 0 0 30px rgba(var(--primary-color-rgb), 0.15), - inset 0 2px 10px rgba(255, 255, 255, 0.3); - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - transform: scale(1.08) rotate(2deg); - box-shadow: - var(--shadow-xl), - 0 0 40px rgba(var(--primary-color-rgb), 0.25), - inset 0 2px 15px rgba(255, 255, 255, 0.4); - } -} - -/* ==================== 用户信息 (User Info) ==================== */ -.profile-info { - text-align: center; - margin-bottom: var(--margin-6); -} - -.profile-name { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-bold); - margin-bottom: var(--margin-2); - color: var(--text-primary); - letter-spacing: -0.02em; - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.profile-email { - color: var(--text-secondary); - font-size: var(--font-size-sm); - margin-bottom: var(--margin-1); - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-2); - - i { - opacity: 0.7; - } -} - -.profile-username { - color: var(--text-tertiary); - font-size: var(--font-size-xs); - font-style: italic; - margin-bottom: var(--margin-4); -} - -/* ==================== 操作按钮 (Action Buttons) ==================== */ -.profile-actions { - display: flex; - gap: var(--spacing-3); - justify-content: center; - flex-wrap: wrap; - - .btn { - padding: var(--spacing-3) var(--spacing-5); - font-size: var(--font-size-sm); - border-radius: var(--radius-full); - background: var(--glass-bg); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: var(--glass-border); - color: var(--text-primary); - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - background: var(--glass-heavy-bg); - transform: translateY(-2px); - box-shadow: var(--shadow-md); - border-color: var(--primary-color-light); - } - - i { - font-size: var(--font-size-base); - } - } -} - -/* ==================== 表单区域 (Form Sections) ==================== */ -.profile-form { - max-width: 100%; -} - -.form-section { - margin-bottom: var(--margin-8); - padding: var(--spacing-6); - background: var(--glass-bg); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-radius: var(--radius-lg); - border: var(--glass-border); - position: relative; - overflow: hidden; - transition: all var(--duration-normal) var(--ease-default); - - /* 左侧装饰条 */ - &::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - background: linear-gradient(180deg, var(--primary-color), var(--secondary-color)); - opacity: 0; - transition: opacity var(--duration-fast) var(--ease-default); - } - - &:hover { - border-color: var(--primary-color-light); - box-shadow: var(--shadow-md); - - &::before { - opacity: 1; - } - } -} - -.form-section-title { - font-size: var(--font-size-xl); - font-weight: var(--font-weight-bold); - margin-bottom: var(--margin-5); - padding-bottom: var(--margin-4); - border-bottom: 2px solid var(--border-color); - color: var(--text-primary); - display: flex; - align-items: center; - gap: var(--spacing-3); - position: relative; - - &::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 60px; - height: 2px; - background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); - } -} - -.form-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-5); - margin-bottom: var(--margin-5); -} - -/* ==================== 密码修改表单 (Password Change Section) ==================== */ -.password-change-section { - margin-top: var(--margin-8); - padding: var(--spacing-6); - background: var(--glass-bg); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border-radius: var(--radius-lg); - border: var(--glass-border); - box-shadow: var(--glass-shadow); - transition: all var(--duration-normal) var(--ease-default); - - &:hover { - border-color: var(--warning-color); - box-shadow: var(--shadow-md); - } -} - -/* ==================== 头像上传 (Avatar Upload) ==================== */ -.avatar-upload { - position: relative; - display: block; - margin: 0 auto var(--margin-5) auto; -} - -.avatar-wrapper { - position: relative; - display: inline-block; - cursor: pointer; - border-radius: 50%; - - &:hover .profile-avatar { - filter: brightness(0.85); - } -} - -.avatar-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - border-radius: 50%; - opacity: 0; - transition: all var(--duration-normal) var(--ease-default); - pointer-events: none; -} - -.avatar-wrapper:hover .avatar-overlay { - opacity: 1; -} - -.avatar-overlay i { - font-size: var(--font-size-3xl); - color: white; - margin-bottom: var(--margin-2); - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); -} - -.avatar-overlay span { - color: white; - font-size: var(--font-size-sm); - text-align: center; - font-weight: var(--font-weight-medium); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.avatar-upload input[type="file"] { - position: absolute; - left: -9999px; -} - -/* ==================== 选项卡样式 (Tabs) ==================== */ -.profile-tabs { - display: flex; - gap: var(--spacing-2); - border-bottom: 2px solid var(--border-color); - margin-bottom: var(--margin-6); - padding-bottom: 0; - overflow-x: auto; - - /* 隐藏滚动条但保持功能 */ - &::-webkit-scrollbar { - display: none; - } -} - -.profile-tab { - background: transparent; - border: none; - color: var(--text-secondary); - padding: var(--spacing-4) var(--spacing-5); - cursor: pointer; - border-bottom: 3px solid transparent; - transition: all var(--duration-fast) var(--ease-default); - font-weight: var(--font-weight-medium); - font-size: var(--font-size-sm); - white-space: nowrap; - position: relative; - - &:hover:not(.active) { - color: var(--text-primary); - background: var(--bg-tertiary); - border-radius: var(--radius-md) var(--radius-md) 0 0; - } - - &.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); - background: linear-gradient(to top, rgba(var(--primary-color-rgb), 0.08), transparent); - } -} - -.tab-content { - display: none; - animation: fadeInUp 0.3s ease-out; -} - -.tab-content.active { - display: block; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ==================== 响应式设计 (Responsive Design) ==================== */ -@media (max-width: 1024px) { - .profile-container { - margin: var(--margin-6); - padding: var(--spacing-6); - } - - .profile-content { - gap: var(--spacing-6); - } - - .profile-sidebar { - flex: 0 0 240px; - } -} - -@media (max-width: 768px) { - .profile-content { - flex-direction: column; - } - - .profile-sidebar { - flex: none; - margin-right: 0; - margin-bottom: var(--margin-8); - order: 2; /* 在移动端显示在下方 */ - } - - .profile-main { - order: 1; /* 在移动端显示在上方 */ - } - - .profile-avatar { - width: 120px; - height: 120px; - margin-bottom: var(--margin-4); - } - - .profile-actions { - justify-content: center; - } - - .form-row { - grid-template-columns: 1fr; - } - - .profile-name { - font-size: var(--font-size-xl); - } - - .profile-tabs { - flex-wrap: nowrap; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } - - .profile-tab { - padding: var(--spacing-3) var(--spacing-4); - font-size: var(--font-size-xs); - } - - .form-section, - .password-change-section { - padding: var(--spacing-5); - } -} - -@media (max-width: 480px) { - .profile-container { - margin: var(--margin-4); - padding: var(--spacing-5); - border-radius: var(--radius-lg); - } - - .profile-avatar { - width: 100px; - height: 100px; - } - - .profile-name { - font-size: var(--font-size-lg); - } - - .profile-actions { - flex-direction: column; - width: 100%; - - .btn { - width: 100%; - justify-content: center; - } - } -} diff --git a/static/css/provider.css b/static/css/provider.css deleted file mode 100644 index 3db152c..0000000 --- a/static/css/provider.css +++ /dev/null @@ -1,4442 +0,0 @@ -/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */ -@layer properties; -@layer theme, base, components, utilities; -@layer theme { - :root, :host { - --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', - monospace; - --color-red-200: oklch(88.5% 0.062 18.334); - --color-red-300: oklch(80.8% 0.114 19.571); - --color-red-400: oklch(70.4% 0.191 22.216); - --color-red-500: oklch(63.7% 0.237 25.331); - --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-900: oklch(39.6% 0.141 25.723); - --color-amber-100: oklch(96.2% 0.059 95.617); - --color-amber-200: oklch(92.4% 0.12 95.746); - --color-amber-300: oklch(87.9% 0.169 91.605); - --color-amber-400: oklch(82.8% 0.189 84.429); - --color-amber-500: oklch(76.9% 0.188 70.08); - --color-amber-600: oklch(66.6% 0.179 58.318); - --color-amber-800: oklch(47.3% 0.137 46.201); - --color-green-400: oklch(79.2% 0.209 151.711); - --color-green-500: oklch(72.3% 0.219 149.579); - --color-emerald-400: oklch(76.5% 0.177 163.223); - --color-emerald-500: oklch(69.6% 0.17 162.48); - --color-emerald-600: oklch(59.6% 0.145 163.225); - --color-cyan-200: oklch(91.7% 0.08 205.041); - --color-cyan-300: oklch(86.5% 0.127 207.078); - --color-cyan-400: oklch(78.9% 0.154 211.53); - --color-cyan-500: oklch(71.5% 0.143 215.221); - --color-cyan-600: oklch(60.9% 0.126 221.723); - --color-blue-50: oklch(97% 0.014 254.604); - --color-blue-400: oklch(70.7% 0.165 254.624); - --color-blue-500: oklch(62.3% 0.214 259.815); - --color-blue-600: oklch(54.6% 0.245 262.881); - --color-purple-400: oklch(71.4% 0.203 305.504); - --color-purple-500: oklch(62.7% 0.265 303.9); - --color-slate-200: oklch(92.9% 0.013 255.508); - --color-slate-300: oklch(86.9% 0.022 252.894); - --color-slate-400: oklch(70.4% 0.04 256.788); - --color-slate-500: oklch(55.4% 0.046 257.417); - --color-slate-600: oklch(44.6% 0.043 257.281); - --color-slate-700: oklch(37.2% 0.044 257.287); - --color-slate-800: oklch(27.9% 0.041 260.031); - --color-slate-900: oklch(20.8% 0.042 265.755); - --color-slate-950: oklch(12.9% 0.042 264.695); - --color-gray-100: oklch(96.7% 0.003 264.542); - --color-gray-500: oklch(55.1% 0.027 264.364); - --color-gray-800: oklch(27.8% 0.033 256.848); - --color-black: #000; - --color-white: #fff; - --spacing: 0.25rem; - --container-xs: 20rem; - --container-md: 28rem; - --container-lg: 32rem; - --container-2xl: 42rem; - --container-3xl: 48rem; - --container-4xl: 56rem; - --container-5xl: 64rem; - --container-6xl: 72rem; - --container-7xl: 80rem; - --text-xs: 0.75rem; - --text-xs--line-height: calc(1 / 0.75); - --text-sm: 0.875rem; - --text-sm--line-height: calc(1.25 / 0.875); - --text-base: 1rem; - --text-base--line-height: calc(1.5 / 1); - --text-lg: 1.125rem; - --text-lg--line-height: calc(1.75 / 1.125); - --text-xl: 1.25rem; - --text-xl--line-height: calc(1.75 / 1.25); - --text-2xl: 1.5rem; - --text-2xl--line-height: calc(2 / 1.5); - --text-3xl: 1.875rem; - --text-3xl--line-height: calc(2.25 / 1.875); - --text-4xl: 2.25rem; - --text-4xl--line-height: calc(2.5 / 2.25); - --text-6xl: 3.75rem; - --text-6xl--line-height: 1; - --font-weight-normal: 400; - --font-weight-medium: 500; - --font-weight-semibold: 600; - --font-weight-bold: 700; - --tracking-wider: 0.05em; - --leading-tight: 1.25; - --leading-normal: 1.5; - --leading-relaxed: 1.625; - --radius-sm: 0.25rem; - --radius-md: 12px; - --radius-lg: 0.5rem; - --radius-xl: 0.75rem; - --radius-3xl: 1.5rem; - --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --ease-in: cubic-bezier(0.4, 0, 1, 1); - --ease-out: cubic-bezier(0, 0, 0.2, 1); - --animate-spin: spin 1s linear infinite; - --blur-sm: 8px; - --blur-md: 12px; - --blur-xl: 24px; - --blur-2xl: 40px; - --blur-3xl: 64px; - --default-transition-duration: 150ms; - --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - --default-font-family: var(--font-sans); - --default-mono-font-family: var(--font-mono); - --color-md-primary: #D0BCFF; - --color-md-on-primary: #381E72; - --color-md-primary-container: #4F378B; - --color-md-on-primary-container: #EADDFF; - --color-md-secondary: #CCC2DC; - --color-md-on-secondary: #332D41; - --color-md-secondary-container: #4A4458; - --color-md-on-secondary-container: #EADDFF; - --color-md-tertiary-container: #633B48; - --color-md-on-tertiary-container: #FFD8E4; - --color-md-surface: #1C1B1F; - --color-md-on-surface: #E6E1E5; - --color-md-on-surface-variant: #CAC4D0; - --color-md-surface-variant: #49454F; - --color-md-surface-container: #211F26; - --color-md-surface-container-low: #1D1B20; - --color-md-surface-container-high: #2B2930; - --color-md-outline: #938F99; - --color-md-outline-variant: #49454F; - --color-md-error: #F2B8B5; - --color-md-on-error: #601410; - --color-md-error-container: #8C1D18; - --color-md-on-error-container: #F9DEDC; - --radius-md-lg: 16px; - --radius-md-xl: 28px; - } -} -@layer base { - *, ::after, ::before, ::backdrop, ::file-selector-button { - box-sizing: border-box; - margin: 0; - padding: 0; - border: 0 solid; - } - html, :host { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - tab-size: 4; - font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); - font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var(--default-font-variation-settings, normal); - -webkit-tap-highlight-color: transparent; - } - hr { - height: 0; - color: inherit; - border-top-width: 1px; - } - abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - } - h1, h2, h3, h4, h5, h6 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; - } - b, strong { - font-weight: bolder; - } - code, kbd, samp, pre { - font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); - font-feature-settings: var(--default-mono-font-feature-settings, normal); - font-variation-settings: var(--default-mono-font-variation-settings, normal); - font-size: 1em; - } - small { - font-size: 80%; - } - sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - sub { - bottom: -0.25em; - } - sup { - top: -0.5em; - } - table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; - } - :-moz-focusring { - outline: auto; - } - progress { - vertical-align: baseline; - } - summary { - display: list-item; - } - ol, ul, menu { - list-style: none; - } - img, svg, video, canvas, audio, iframe, embed, object { - display: block; - vertical-align: middle; - } - img, video { - max-width: 100%; - height: auto; - } - button, input, select, optgroup, textarea, ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - border-radius: 0; - background-color: transparent; - opacity: 1; - } - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - ::file-selector-button { - margin-inline-end: 4px; - } - ::placeholder { - opacity: 1; - } - @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentcolor; - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - textarea { - resize: vertical; - } - ::-webkit-search-decoration { - -webkit-appearance: none; - } - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - ::-webkit-datetime-edit { - display: inline-flex; - } - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - ::-webkit-calendar-picker-indicator { - line-height: 1; - } - :-moz-ui-invalid { - box-shadow: none; - } - button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { - appearance: button; - } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { - height: auto; - } - [hidden]:where(:not([hidden='until-found'])) { - display: none !important; - } -} -@layer utilities { - .pointer-events-none { - pointer-events: none; - } - .collapse { - visibility: collapse; - } - .invisible { - visibility: hidden; - } - .visible { - visibility: visible; - } - .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; - border-width: 0; - } - .absolute { - position: absolute; - } - .fixed { - position: fixed; - } - .relative { - position: relative; - } - .static { - position: static; - } - .sticky { - position: sticky; - } - .inset-0 { - inset: calc(var(--spacing) * 0); - } - .inset-y-0 { - inset-block: calc(var(--spacing) * 0); - } - .start { - inset-inline-start: var(--spacing); - } - .end { - inset-inline-end: var(--spacing); - } - .top-0 { - top: calc(var(--spacing) * 0); - } - .top-1 { - top: calc(var(--spacing) * 1); - } - .top-1\/2 { - top: calc(1 / 2 * 100%); - } - .top-2 { - top: calc(var(--spacing) * 2); - } - .top-5 { - top: calc(var(--spacing) * 5); - } - .top-20 { - top: calc(var(--spacing) * 20); - } - .top-full { - top: 100%; - } - .right-0 { - right: calc(var(--spacing) * 0); - } - .right-2 { - right: calc(var(--spacing) * 2); - } - .right-3 { - right: calc(var(--spacing) * 3); - } - .right-5 { - right: calc(var(--spacing) * 5); - } - .bottom-0 { - bottom: calc(var(--spacing) * 0); - } - .left-0 { - left: calc(var(--spacing) * 0); - } - .left-1 { - left: calc(var(--spacing) * 1); - } - .left-3 { - left: calc(var(--spacing) * 3); - } - .left-4 { - left: calc(var(--spacing) * 4); - } - .left-5 { - left: calc(var(--spacing) * 5); - } - .z-0 { - z-index: 0; - } - .z-10 { - z-index: 10; - } - .z-20 { - z-index: 20; - } - .z-30 { - z-index: 30; - } - .z-40 { - z-index: 40; - } - .z-50 { - z-index: 50; - } - .z-\[3\] { - z-index: 3; - } - .z-\[4\] { - z-index: 4; - } - .z-\[1000\] { - z-index: 1000; - } - .z-\[9999\] { - z-index: 9999; - } - .col-12 { - grid-column: 12; - } - .container { - width: 100%; - @media (width >= 40rem) { - max-width: 40rem; - } - @media (width >= 48rem) { - max-width: 48rem; - } - @media (width >= 64rem) { - max-width: 64rem; - } - @media (width >= 80rem) { - max-width: 80rem; - } - @media (width >= 96rem) { - max-width: 96rem; - } - } - .m-0 { - margin: calc(var(--spacing) * 0); - } - .-mx-3 { - margin-inline: calc(var(--spacing) * -3); - } - .-mx-6 { - margin-inline: calc(var(--spacing) * -6); - } - .mx-1 { - margin-inline: calc(var(--spacing) * 1); - } - .mx-4 { - margin-inline: calc(var(--spacing) * 4); - } - .mx-auto { - margin-inline: auto; - } - .my-1 { - margin-block: calc(var(--spacing) * 1); - } - .my-2 { - margin-block: calc(var(--spacing) * 2); - } - .my-2\.5 { - margin-block: calc(var(--spacing) * 2.5); - } - .my-4 { - margin-block: calc(var(--spacing) * 4); - } - .my-5 { - margin-block: calc(var(--spacing) * 5); - } - .me-2 { - margin-inline-end: calc(var(--spacing) * 2); - } - .mt-0 { - margin-top: calc(var(--spacing) * 0); - } - .mt-0\.5 { - margin-top: calc(var(--spacing) * 0.5); - } - .mt-1 { - margin-top: calc(var(--spacing) * 1); - } - .mt-1\.5 { - margin-top: calc(var(--spacing) * 1.5); - } - .mt-2 { - margin-top: calc(var(--spacing) * 2); - } - .mt-3 { - margin-top: calc(var(--spacing) * 3); - } - .mt-4 { - margin-top: calc(var(--spacing) * 4); - } - .mt-6 { - margin-top: calc(var(--spacing) * 6); - } - .mt-8 { - margin-top: calc(var(--spacing) * 8); - } - .mr-1 { - margin-right: calc(var(--spacing) * 1); - } - .mr-2 { - margin-right: calc(var(--spacing) * 2); - } - .mr-3 { - margin-right: calc(var(--spacing) * 3); - } - .mb-0 { - margin-bottom: calc(var(--spacing) * 0); - } - .mb-1 { - margin-bottom: calc(var(--spacing) * 1); - } - .mb-1\.5 { - margin-bottom: calc(var(--spacing) * 1.5); - } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); - } - .mb-3 { - margin-bottom: calc(var(--spacing) * 3); - } - .mb-4 { - margin-bottom: calc(var(--spacing) * 4); - } - .mb-5 { - margin-bottom: calc(var(--spacing) * 5); - } - .mb-6 { - margin-bottom: calc(var(--spacing) * 6); - } - .mb-8 { - margin-bottom: calc(var(--spacing) * 8); - } - .ml-1 { - margin-left: calc(var(--spacing) * 1); - } - .ml-2 { - margin-left: calc(var(--spacing) * 2); - } - .ml-3 { - margin-left: calc(var(--spacing) * 3); - } - .ml-4 { - margin-left: calc(var(--spacing) * 4); - } - .ml-auto { - margin-left: auto; - } - .box-border { - box-sizing: border-box; - } - .block { - display: block; - } - .contents { - display: contents; - } - .flex { - display: flex; - } - .grid { - display: grid; - } - .hidden { - display: none; - } - .inline { - display: inline; - } - .inline-block { - display: inline-block; - } - .inline-flex { - display: inline-flex; - } - .table { - display: table; - } - .h-1 { - height: calc(var(--spacing) * 1); - } - .h-1\.5 { - height: calc(var(--spacing) * 1.5); - } - .h-2 { - height: calc(var(--spacing) * 2); - } - .h-2\.5 { - height: calc(var(--spacing) * 2.5); - } - .h-4 { - height: calc(var(--spacing) * 4); - } - .h-5 { - height: calc(var(--spacing) * 5); - } - .h-6 { - height: calc(var(--spacing) * 6); - } - .h-7 { - height: calc(var(--spacing) * 7); - } - .h-8 { - height: calc(var(--spacing) * 8); - } - .h-9 { - height: calc(var(--spacing) * 9); - } - .h-10 { - height: calc(var(--spacing) * 10); - } - .h-11 { - height: calc(var(--spacing) * 11); - } - .h-12 { - height: calc(var(--spacing) * 12); - } - .h-14 { - height: calc(var(--spacing) * 14); - } - .h-16 { - height: calc(var(--spacing) * 16); - } - .h-20 { - height: calc(var(--spacing) * 20); - } - .h-\[18px\] { - height: 18px; - } - .h-full { - height: 100%; - } - .h-px { - height: 1px; - } - .h-screen { - height: 100vh; - } - .max-h-64 { - max-height: calc(var(--spacing) * 64); - } - .max-h-\[calc\(100vh-6rem\)\] { - max-height: calc(100vh - 6rem); - } - .min-h-\[40px\] { - min-height: 40px; - } - .min-h-\[44px\] { - min-height: 44px; - } - .min-h-\[56px\] { - min-height: 56px; - } - .min-h-\[60vh\] { - min-height: 60vh; - } - .min-h-\[80px\] { - min-height: 80px; - } - .min-h-\[120px\] { - min-height: 120px; - } - .min-h-\[calc\(100vh-64px\)\] { - min-height: calc(100vh - 64px); - } - .min-h-screen { - min-height: 100vh; - } - .w-0\.5 { - width: calc(var(--spacing) * 0.5); - } - .w-1\.5 { - width: calc(var(--spacing) * 1.5); - } - .w-2\.5 { - width: calc(var(--spacing) * 2.5); - } - .w-4 { - width: calc(var(--spacing) * 4); - } - .w-5 { - width: calc(var(--spacing) * 5); - } - .w-7 { - width: calc(var(--spacing) * 7); - } - .w-8 { - width: calc(var(--spacing) * 8); - } - .w-9 { - width: calc(var(--spacing) * 9); - } - .w-10 { - width: calc(var(--spacing) * 10); - } - .w-11 { - width: calc(var(--spacing) * 11); - } - .w-12 { - width: calc(var(--spacing) * 12); - } - .w-14 { - width: calc(var(--spacing) * 14); - } - .w-16 { - width: calc(var(--spacing) * 16); - } - .w-20 { - width: calc(var(--spacing) * 20); - } - .w-24 { - width: calc(var(--spacing) * 24); - } - .w-32 { - width: calc(var(--spacing) * 32); - } - .w-56 { - width: calc(var(--spacing) * 56); - } - .w-64 { - width: calc(var(--spacing) * 64); - } - .w-\[18px\] { - width: 18px; - } - .w-\[52px\] { - width: 52px; - } - .w-\[60px\] { - width: 60px; - } - .w-\[120px\] { - width: 120px; - } - .w-\[200px\] { - width: 200px; - } - .w-\[280px\] { - width: 280px; - } - .w-\[320px\] { - width: 320px; - } - .w-auto { - width: auto; - } - .w-full { - width: 100%; - } - .w-px { - width: 1px; - } - .max-w-2xl { - max-width: var(--container-2xl); - } - .max-w-3xl { - max-width: var(--container-3xl); - } - .max-w-4xl { - max-width: var(--container-4xl); - } - .max-w-5xl { - max-width: var(--container-5xl); - } - .max-w-6xl { - max-width: var(--container-6xl); - } - .max-w-7xl { - max-width: var(--container-7xl); - } - .max-w-\[90vw\] { - max-width: 90vw; - } - .max-w-\[160px\] { - max-width: 160px; - } - .max-w-\[200px\] { - max-width: 200px; - } - .max-w-\[420px\] { - max-width: 420px; - } - .max-w-\[450px\] { - max-width: 450px; - } - .max-w-\[600px\] { - max-width: 600px; - } - .max-w-\[800px\] { - max-width: 800px; - } - .max-w-\[1000px\] { - max-width: 1000px; - } - .max-w-\[1200px\] { - max-width: 1200px; - } - .max-w-\[1400px\] { - max-width: 1400px; - } - .max-w-full { - max-width: 100%; - } - .max-w-lg { - max-width: var(--container-lg); - } - .max-w-md { - max-width: var(--container-md); - } - .max-w-xs { - max-width: var(--container-xs); - } - .min-w-0 { - min-width: calc(var(--spacing) * 0); - } - .min-w-\[20px\] { - min-width: 20px; - } - .min-w-\[40px\] { - min-width: 40px; - } - .min-w-\[80px\] { - min-width: 80px; - } - .min-w-\[120px\] { - min-width: 120px; - } - .min-w-\[180px\] { - min-width: 180px; - } - .min-w-\[200px\] { - min-width: 200px; - } - .min-w-\[250px\] { - min-width: 250px; - } - .min-w-\[600px\] { - min-width: 600px; - } - .flex-1 { - flex: 1; - } - .flex-\[2\] { - flex: 2; - } - .flex-shrink-0 { - flex-shrink: 0; - } - .shrink-0 { - flex-shrink: 0; - } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-8 { - --tw-translate-x: calc(var(--spacing) * -8); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .-translate-x-full { - --tw-translate-x: -100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .translate-x-0 { - --tw-translate-x: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .translate-x-8 { - --tw-translate-x: calc(var(--spacing) * 8); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .-translate-y-1\/2 { - --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .translate-y-0 { - --tw-translate-y: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .translate-y-2 { - --tw-translate-y: calc(var(--spacing) * 2); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - .scale-95 { - --tw-scale-x: 95%; - --tw-scale-y: 95%; - --tw-scale-z: 95%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } - .scale-100 { - --tw-scale-x: 100%; - --tw-scale-y: 100%; - --tw-scale-z: 100%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } - .rotate-180 { - rotate: 180deg; - } - .transform { - transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); - } - .animate-\[bounce_0\.6s_ease-out\] { - animation: bounce 0.6s ease-out; - } - .animate-\[fadeIn_0\.5s_ease-out\] { - animation: fadeIn 0.5s ease-out; - } - .animate-\[pulse_2s_ease-in-out_infinite\] { - animation: pulse 2s ease-in-out infinite; - } - .animate-spin { - animation: var(--animate-spin); - } - .cursor-not-allowed { - cursor: not-allowed; - } - .cursor-pointer { - cursor: pointer; - } - .resize { - resize: both; - } - .resize-none { - resize: none; - } - .resize-y { - resize: vertical; - } - .list-none { - list-style-type: none; - } - .appearance-none { - appearance: none; - } - .grid-cols-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } - .grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .grid-cols-\[repeat\(auto-fit\,minmax\(280px\,1fr\)\)\] { - grid-template-columns: repeat(auto-fit,minmax(280px,1fr)); - } - .flex-col { - flex-direction: column; - } - .flex-wrap { - flex-wrap: wrap; - } - .content-start { - align-content: flex-start; - } - .items-center { - align-items: center; - } - .items-end { - align-items: flex-end; - } - .items-start { - align-items: flex-start; - } - .items-stretch { - align-items: stretch; - } - .justify-between { - justify-content: space-between; - } - .justify-center { - justify-content: center; - } - .justify-end { - justify-content: flex-end; - } - .gap-1 { - gap: calc(var(--spacing) * 1); - } - .gap-1\.5 { - gap: calc(var(--spacing) * 1.5); - } - .gap-2 { - gap: calc(var(--spacing) * 2); - } - .gap-3 { - gap: calc(var(--spacing) * 3); - } - .gap-4 { - gap: calc(var(--spacing) * 4); - } - .gap-5 { - gap: calc(var(--spacing) * 5); - } - .gap-6 { - gap: calc(var(--spacing) * 6); - } - .gap-8 { - gap: calc(var(--spacing) * 8); - } - .space-y-0 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-0\.5 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 0.5) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 0.5) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-1 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-2 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-3 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-4 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-5 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-6 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-8 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); - } - } - .gap-x-4 { - column-gap: calc(var(--spacing) * 4); - } - .gap-x-6 { - column-gap: calc(var(--spacing) * 6); - } - .gap-x-8 { - column-gap: calc(var(--spacing) * 8); - } - .gap-y-1 { - row-gap: calc(var(--spacing) * 1); - } - .gap-y-2 { - row-gap: calc(var(--spacing) * 2); - } - .gap-y-4 { - row-gap: calc(var(--spacing) * 4); - } - .divide-y { - :where(& > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } - } - .divide-md-outline-variant\/30 { - :where(& > :not(:last-child)) { - border-color: color-mix(in srgb, #49454F 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-md-outline-variant) 30%, transparent); - } - } - } - .divide-white\/5 { - :where(& > :not(:last-child)) { - border-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - } - .truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .overflow-hidden { - overflow: hidden; - } - .overflow-x-auto { - overflow-x: auto; - } - .overflow-y-auto { - overflow-y: auto; - } - .rounded { - border-radius: 0.25rem; - } - .rounded-3xl { - border-radius: var(--radius-3xl); - } - .rounded-full { - border-radius: calc(infinity * 1px); - } - .rounded-lg { - border-radius: var(--radius-lg); - } - .rounded-md { - border-radius: var(--radius-md); - } - .rounded-md-lg { - border-radius: var(--radius-md-lg); - } - .rounded-md-xl { - border-radius: var(--radius-md-xl); - } - .border { - border-style: var(--tw-border-style); - border-width: 1px; - } - .border-0 { - border-style: var(--tw-border-style); - border-width: 0px; - } - .border-2 { - border-style: var(--tw-border-style); - border-width: 2px; - } - .border-t { - border-top-style: var(--tw-border-style); - border-top-width: 1px; - } - .border-r { - border-right-style: var(--tw-border-style); - border-right-width: 1px; - } - .border-b { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 1px; - } - .border-b-2 { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 2px; - } - .border-l-4 { - border-left-style: var(--tw-border-style); - border-left-width: 4px; - } - .border-l-\[3px\] { - border-left-style: var(--tw-border-style); - border-left-width: 3px; - } - .border-none { - --tw-border-style: none; - border-style: none; - } - .\!border-cyan-500\/30 { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent) !important; - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent) !important; - } - } - .border-amber-500\/20 { - border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-amber-500) 20%, transparent); - } - } - .border-amber-500\/30 { - border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-amber-500) 30%, transparent); - } - } - .border-blue-500\/30 { - border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-blue-500) 30%, transparent); - } - } - .border-blue-600 { - border-color: var(--color-blue-600); - } - .border-cyan-400 { - border-color: var(--color-cyan-400); - } - .border-cyan-500\/20 { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 20%, transparent); - } - } - .border-cyan-500\/30 { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent); - } - } - .border-cyan-500\/50 { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 50%, transparent); - } - } - .border-cyan-600 { - border-color: var(--color-cyan-600); - } - .border-emerald-500\/30 { - border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-emerald-500) 30%, transparent); - } - } - .border-emerald-600 { - border-color: var(--color-emerald-600); - } - .border-green-500\/20 { - border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); - } - } - .border-green-500\/30 { - border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-green-500) 30%, transparent); - } - } - .border-md-error { - border-color: var(--color-md-error); - } - .border-md-outline { - border-color: var(--color-md-outline); - } - .border-md-outline-variant { - border-color: var(--color-md-outline-variant); - } - .border-md-outline-variant\/30 { - border-color: color-mix(in srgb, #49454F 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-md-outline-variant) 30%, transparent); - } - } - .border-md-outline-variant\/50 { - border-color: color-mix(in srgb, #49454F 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-md-outline-variant) 50%, transparent); - } - } - .border-md-outline\/50 { - border-color: color-mix(in srgb, #938F99 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-md-outline) 50%, transparent); - } - } - .border-md-primary { - border-color: var(--color-md-primary); - } - .border-red-500 { - border-color: var(--color-red-500); - } - .border-red-500\/20 { - border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-red-500) 20%, transparent); - } - } - .border-red-500\/30 { - border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-red-500) 30%, transparent); - } - } - .border-red-500\/50 { - border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); - } - } - .border-red-600 { - border-color: var(--color-red-600); - } - .border-slate-500\/30 { - border-color: color-mix(in srgb, oklch(55.4% 0.046 257.417) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-500) 30%, transparent); - } - } - .border-slate-600 { - border-color: var(--color-slate-600); - } - .border-slate-600\/20 { - border-color: color-mix(in srgb, oklch(44.6% 0.043 257.281) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-600) 20%, transparent); - } - } - .border-slate-600\/30 { - border-color: color-mix(in srgb, oklch(44.6% 0.043 257.281) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-600) 30%, transparent); - } - } - .border-slate-700\/20 { - border-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-700) 20%, transparent); - } - } - .border-slate-700\/30 { - border-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-700) 30%, transparent); - } - } - .border-slate-700\/50 { - border-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-slate-700) 50%, transparent); - } - } - .border-slate-800 { - border-color: var(--color-slate-800); - } - .border-white\/5 { - border-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - .border-white\/10 { - border-color: color-mix(in srgb, #fff 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 10%, transparent); - } - } - .border-white\/20 { - border-color: color-mix(in srgb, #fff 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 20%, transparent); - } - } - .border-l-md-error { - border-left-color: var(--color-md-error); - } - .border-l-md-primary { - border-left-color: var(--color-md-primary); - } - .border-l-transparent { - border-left-color: transparent; - } - .\!bg-md-surface-variant { - background-color: var(--color-md-surface-variant) !important; - } - .\!bg-red-500 { - background-color: var(--color-red-500) !important; - } - .bg-amber-100 { - background-color: var(--color-amber-100); - } - .bg-amber-400 { - background-color: var(--color-amber-400); - } - .bg-amber-500\/10 { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); - } - } - .bg-amber-500\/15 { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent); - } - } - .bg-amber-500\/20 { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 20%, transparent); - } - } - .bg-amber-600\/20 { - background-color: color-mix(in srgb, oklch(66.6% 0.179 58.318) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-600) 20%, transparent); - } - } - .bg-black\/50 { - background-color: color-mix(in srgb, #000 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 50%, transparent); - } - } - .bg-black\/60 { - background-color: color-mix(in srgb, #000 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 60%, transparent); - } - } - .bg-black\/90 { - background-color: color-mix(in srgb, #000 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 90%, transparent); - } - } - .bg-blue-50 { - background-color: var(--color-blue-50); - } - .bg-blue-500\/15 { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 15%, transparent); - } - } - .bg-blue-500\/20 { - background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); - } - } - .bg-blue-600 { - background-color: var(--color-blue-600); - } - .bg-cyan-500\/5 { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 5%, transparent); - } - } - .bg-cyan-500\/10 { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 10%, transparent); - } - } - .bg-cyan-500\/20 { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 20%, transparent); - } - } - .bg-cyan-500\/30 { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent); - } - } - .bg-cyan-600 { - background-color: var(--color-cyan-600); - } - .bg-emerald-500\/10 { - background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-500) 10%, transparent); - } - } - .bg-emerald-500\/20 { - background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); - } - } - .bg-emerald-600 { - background-color: var(--color-emerald-600); - } - .bg-emerald-600\/20 { - background-color: color-mix(in srgb, oklch(59.6% 0.145 163.225) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-600) 20%, transparent); - } - } - .bg-gray-100 { - background-color: var(--color-gray-100); - } - .bg-green-400 { - background-color: var(--color-green-400); - } - .bg-green-500\/10 { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 10%, transparent); - } - } - .bg-green-500\/15 { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 15%, transparent); - } - } - .bg-green-500\/20 { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); - } - } - .bg-md-error { - background-color: var(--color-md-error); - } - .bg-md-error-container { - background-color: var(--color-md-error-container); - } - .bg-md-error-container\/30 { - background-color: color-mix(in srgb, #8C1D18 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error-container) 30%, transparent); - } - } - .bg-md-error-container\/50 { - background-color: color-mix(in srgb, #8C1D18 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error-container) 50%, transparent); - } - } - .bg-md-error-container\/90 { - background-color: color-mix(in srgb, #8C1D18 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error-container) 90%, transparent); - } - } - .bg-md-error\/10 { - background-color: color-mix(in srgb, #F2B8B5 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error) 10%, transparent); - } - } - .bg-md-outline-variant { - background-color: var(--color-md-outline-variant); - } - .bg-md-primary { - background-color: var(--color-md-primary); - } - .bg-md-primary-container { - background-color: var(--color-md-primary-container); - } - .bg-md-primary-container\/90 { - background-color: color-mix(in srgb, #4F378B 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary-container) 90%, transparent); - } - } - .bg-md-primary\/30 { - background-color: color-mix(in srgb, #D0BCFF 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 30%, transparent); - } - } - .bg-md-secondary { - background-color: var(--color-md-secondary); - } - .bg-md-secondary-container { - background-color: var(--color-md-secondary-container); - } - .bg-md-secondary-container\/80 { - background-color: color-mix(in srgb, #4A4458 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-secondary-container) 80%, transparent); - } - } - .bg-md-surface { - background-color: var(--color-md-surface); - } - .bg-md-surface-container-high { - background-color: var(--color-md-surface-container-high); - } - .bg-md-surface-container-high\/50 { - background-color: color-mix(in srgb, #2B2930 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface-container-high) 50%, transparent); - } - } - .bg-md-surface-container-low { - background-color: var(--color-md-surface-container-low); - } - .bg-md-surface-container\/70 { - background-color: color-mix(in srgb, #211F26 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface-container) 70%, transparent); - } - } - .bg-md-surface-container\/80 { - background-color: color-mix(in srgb, #211F26 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface-container) 80%, transparent); - } - } - .bg-md-surface-variant { - background-color: var(--color-md-surface-variant); - } - .bg-md-surface\/50 { - background-color: color-mix(in srgb, #1C1B1F 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface) 50%, transparent); - } - } - .bg-md-surface\/80 { - background-color: color-mix(in srgb, #1C1B1F 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface) 80%, transparent); - } - } - .bg-md-tertiary-container { - background-color: var(--color-md-tertiary-container); - } - .bg-md-tertiary-container\/90 { - background-color: color-mix(in srgb, #633B48 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-tertiary-container) 90%, transparent); - } - } - .bg-purple-500\/15 { - background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-purple-500) 15%, transparent); - } - } - .bg-purple-500\/20 { - background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-purple-500) 20%, transparent); - } - } - .bg-red-500 { - background-color: var(--color-red-500); - } - .bg-red-500\/10 { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent); - } - } - .bg-red-500\/15 { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 15%, transparent); - } - } - .bg-red-500\/20 { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 20%, transparent); - } - } - .bg-red-500\/\[0\.03\] { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 3%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 3%, transparent); - } - } - .bg-red-600 { - background-color: var(--color-red-600); - } - .bg-red-600\/15 { - background-color: color-mix(in srgb, oklch(57.7% 0.245 27.325) 15%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-600) 15%, transparent); - } - } - .bg-red-900\/20 { - background-color: color-mix(in srgb, oklch(39.6% 0.141 25.723) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-900) 20%, transparent); - } - } - .bg-slate-500\/20 { - background-color: color-mix(in srgb, oklch(55.4% 0.046 257.417) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-500) 20%, transparent); - } - } - .bg-slate-600 { - background-color: var(--color-slate-600); - } - .bg-slate-600\/20 { - background-color: color-mix(in srgb, oklch(44.6% 0.043 257.281) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-600) 20%, transparent); - } - } - .bg-slate-600\/30 { - background-color: color-mix(in srgb, oklch(44.6% 0.043 257.281) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-600) 30%, transparent); - } - } - .bg-slate-600\/50 { - background-color: color-mix(in srgb, oklch(44.6% 0.043 257.281) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-600) 50%, transparent); - } - } - .bg-slate-700 { - background-color: var(--color-slate-700); - } - .bg-slate-700\/30 { - background-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-700) 30%, transparent); - } - } - .bg-slate-800 { - background-color: var(--color-slate-800); - } - .bg-slate-800\/50 { - background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-800) 50%, transparent); - } - } - .bg-slate-900\/30 { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 30%, transparent); - } - } - .bg-slate-900\/40 { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 40%, transparent); - } - } - .bg-slate-900\/50 { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 50%, transparent); - } - } - .bg-slate-900\/80 { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 80%, transparent); - } - } - .bg-slate-950 { - background-color: var(--color-slate-950); - } - .bg-slate-950\/30 { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 30%, transparent); - } - } - .bg-slate-950\/50 { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 50%, transparent); - } - } - .bg-slate-950\/70 { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 70%, transparent); - } - } - .bg-slate-950\/80 { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 80%, transparent); - } - } - .bg-slate-950\/95 { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 95%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 95%, transparent); - } - } - .bg-transparent { - background-color: transparent; - } - .bg-white { - background-color: var(--color-white); - } - .bg-white\/5 { - background-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - .bg-white\/10 { - background-color: color-mix(in srgb, #fff 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 10%, transparent); - } - } - .bg-white\/20 { - background-color: color-mix(in srgb, #fff 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 20%, transparent); - } - } - .bg-white\/\[0\.02\] { - background-color: color-mix(in srgb, #fff 2%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 2%, transparent); - } - } - .bg-gradient-to-b { - --tw-gradient-position: to bottom in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - .bg-gradient-to-br { - --tw-gradient-position: to bottom right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - .bg-gradient-to-r { - --tw-gradient-position: to right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } - .bg-\[url\(\'\/static\/img\/bgimg\.jpg\'\)\] { - background-image: url('/static/img/bgimg.jpg'); - } - .from-\[\#1e3a5f\] { - --tw-gradient-from: #1e3a5f; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-cyan-600 { - --tw-gradient-from: var(--color-cyan-600); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-md-surface { - --tw-gradient-from: var(--color-md-surface); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-md-surface\/60 { - --tw-gradient-from: color-mix(in srgb, #1C1B1F 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-from: color-mix(in oklab, var(--color-md-surface) 60%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-slate-950\/70 { - --tw-gradient-from: color-mix(in srgb, oklch(12.9% 0.042 264.695) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-from: color-mix(in oklab, var(--color-slate-950) 70%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-slate-950\/80 { - --tw-gradient-from: color-mix(in srgb, oklch(12.9% 0.042 264.695) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-from: color-mix(in oklab, var(--color-slate-950) 80%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .via-md-surface\/40 { - --tw-gradient-via: color-mix(in srgb, #1C1B1F 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-via: color-mix(in oklab, var(--color-md-surface) 40%, transparent); - } - --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-via-stops); - } - .via-slate-900\/50 { - --tw-gradient-via: color-mix(in srgb, oklch(20.8% 0.042 265.755) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-via: color-mix(in oklab, var(--color-slate-900) 50%, transparent); - } - --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-via-stops); - } - .via-slate-900\/70 { - --tw-gradient-via: color-mix(in srgb, oklch(20.8% 0.042 265.755) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-via: color-mix(in oklab, var(--color-slate-900) 70%, transparent); - } - --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-via-stops); - } - .to-\[\#10b981\] { - --tw-gradient-to: #10b981; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-cyan-400 { - --tw-gradient-to: var(--color-cyan-400); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-md-surface-container { - --tw-gradient-to: var(--color-md-surface-container); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-md-surface\/70 { - --tw-gradient-to: color-mix(in srgb, #1C1B1F 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-to: color-mix(in oklab, var(--color-md-surface) 70%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-slate-950\/80 { - --tw-gradient-to: color-mix(in srgb, oklch(12.9% 0.042 264.695) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-to: color-mix(in oklab, var(--color-slate-950) 80%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .bg-cover { - background-size: cover; - } - .bg-fixed { - background-attachment: fixed; - } - .bg-center { - background-position: center; - } - .object-cover { - object-fit: cover; - } - .p-0 { - padding: calc(var(--spacing) * 0); - } - .p-1 { - padding: calc(var(--spacing) * 1); - } - .p-1\.5 { - padding: calc(var(--spacing) * 1.5); - } - .p-2 { - padding: calc(var(--spacing) * 2); - } - .p-3 { - padding: calc(var(--spacing) * 3); - } - .p-4 { - padding: calc(var(--spacing) * 4); - } - .p-5 { - padding: calc(var(--spacing) * 5); - } - .p-6 { - padding: calc(var(--spacing) * 6); - } - .p-8 { - padding: calc(var(--spacing) * 8); - } - .p-10 { - padding: calc(var(--spacing) * 10); - } - .px-1\.5 { - padding-inline: calc(var(--spacing) * 1.5); - } - .px-2 { - padding-inline: calc(var(--spacing) * 2); - } - .px-2\.5 { - padding-inline: calc(var(--spacing) * 2.5); - } - .px-3 { - padding-inline: calc(var(--spacing) * 3); - } - .px-4 { - padding-inline: calc(var(--spacing) * 4); - } - .px-5 { - padding-inline: calc(var(--spacing) * 5); - } - .px-6 { - padding-inline: calc(var(--spacing) * 6); - } - .px-8 { - padding-inline: calc(var(--spacing) * 8); - } - .py-0\.5 { - padding-block: calc(var(--spacing) * 0.5); - } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } - .py-1\.5 { - padding-block: calc(var(--spacing) * 1.5); - } - .py-2 { - padding-block: calc(var(--spacing) * 2); - } - .py-2\.5 { - padding-block: calc(var(--spacing) * 2.5); - } - .py-3 { - padding-block: calc(var(--spacing) * 3); - } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .py-5 { - padding-block: calc(var(--spacing) * 5); - } - .py-6 { - padding-block: calc(var(--spacing) * 6); - } - .py-8 { - padding-block: calc(var(--spacing) * 8); - } - .py-12 { - padding-block: calc(var(--spacing) * 12); - } - .py-16 { - padding-block: calc(var(--spacing) * 16); - } - .pt-1 { - padding-top: calc(var(--spacing) * 1); - } - .pt-3 { - padding-top: calc(var(--spacing) * 3); - } - .pt-4 { - padding-top: calc(var(--spacing) * 4); - } - .pt-5 { - padding-top: calc(var(--spacing) * 5); - } - .pt-6 { - padding-top: calc(var(--spacing) * 6); - } - .pt-7 { - padding-top: calc(var(--spacing) * 7); - } - .pr-2 { - padding-right: calc(var(--spacing) * 2); - } - .pr-4 { - padding-right: calc(var(--spacing) * 4); - } - .pr-10 { - padding-right: calc(var(--spacing) * 10); - } - .pr-12 { - padding-right: calc(var(--spacing) * 12); - } - .pr-28 { - padding-right: calc(var(--spacing) * 28); - } - .pb-2 { - padding-bottom: calc(var(--spacing) * 2); - } - .pb-3 { - padding-bottom: calc(var(--spacing) * 3); - } - .pb-4 { - padding-bottom: calc(var(--spacing) * 4); - } - .pb-6 { - padding-bottom: calc(var(--spacing) * 6); - } - .pl-2 { - padding-left: calc(var(--spacing) * 2); - } - .pl-4 { - padding-left: calc(var(--spacing) * 4); - } - .pl-9 { - padding-left: calc(var(--spacing) * 9); - } - .pl-10 { - padding-left: calc(var(--spacing) * 10); - } - .text-center { - text-align: center; - } - .text-left { - text-align: left; - } - .text-right { - text-align: right; - } - .align-middle { - vertical-align: middle; - } - .font-\[inherit\] { - font-family: inherit; - } - .font-mono { - font-family: var(--font-mono); - } - .font-sans { - font-family: var(--font-sans); - } - .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - } - .text-3xl { - font-size: var(--text-3xl); - line-height: var(--tw-leading, var(--text-3xl--line-height)); - } - .text-4xl { - font-size: var(--text-4xl); - line-height: var(--tw-leading, var(--text-4xl--line-height)); - } - .text-6xl { - font-size: var(--text-6xl); - line-height: var(--tw-leading, var(--text-6xl--line-height)); - } - .text-base { - font-size: var(--text-base); - line-height: var(--tw-leading, var(--text-base--line-height)); - } - .text-lg { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - } - .text-sm { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } - .text-xs { - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - } - .text-\[6rem\] { - font-size: 6rem; - } - .text-\[16px\] { - font-size: 16px; - } - .text-\[18px\] { - font-size: 18px; - } - .text-\[20px\] { - font-size: 20px; - } - .text-\[40px\] { - font-size: 40px; - } - .text-\[48px\] { - font-size: 48px; - } - .text-\[64px\] { - font-size: 64px; - } - .leading-none { - --tw-leading: 1; - line-height: 1; - } - .leading-normal { - --tw-leading: var(--leading-normal); - line-height: var(--leading-normal); - } - .leading-relaxed { - --tw-leading: var(--leading-relaxed); - line-height: var(--leading-relaxed); - } - .leading-tight { - --tw-leading: var(--leading-tight); - line-height: var(--leading-tight); - } - .font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } - .font-medium { - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - } - .font-semibold { - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - } - .tracking-\[0\.1em\] { - --tw-tracking: 0.1em; - letter-spacing: 0.1em; - } - .tracking-\[0\.3em\] { - --tw-tracking: 0.3em; - letter-spacing: 0.3em; - } - .tracking-wider { - --tw-tracking: var(--tracking-wider); - letter-spacing: var(--tracking-wider); - } - .break-words { - overflow-wrap: break-word; - } - .break-all { - word-break: break-all; - } - .whitespace-nowrap { - white-space: nowrap; - } - .whitespace-pre-wrap { - white-space: pre-wrap; - } - .\!text-md-error { - color: var(--color-md-error) !important; - } - .\!text-md-on-surface-variant { - color: var(--color-md-on-surface-variant) !important; - } - .\!text-md-outline { - color: var(--color-md-outline) !important; - } - .text-amber-200 { - color: var(--color-amber-200); - } - .text-amber-200\/70 { - color: color-mix(in srgb, oklch(92.4% 0.12 95.746) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-200) 70%, transparent); - } - } - .text-amber-200\/80 { - color: color-mix(in srgb, oklch(92.4% 0.12 95.746) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-200) 80%, transparent); - } - } - .text-amber-300\/70 { - color: color-mix(in srgb, oklch(87.9% 0.169 91.605) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-300) 70%, transparent); - } - } - .text-amber-400 { - color: var(--color-amber-400); - } - .text-amber-400\/60 { - color: color-mix(in srgb, oklch(82.8% 0.189 84.429) 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-400) 60%, transparent); - } - } - .text-amber-400\/70 { - color: color-mix(in srgb, oklch(82.8% 0.189 84.429) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-400) 70%, transparent); - } - } - .text-amber-400\/80 { - color: color-mix(in srgb, oklch(82.8% 0.189 84.429) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-amber-400) 80%, transparent); - } - } - .text-amber-800 { - color: var(--color-amber-800); - } - .text-blue-400 { - color: var(--color-blue-400); - } - .text-blue-400\/60 { - color: color-mix(in srgb, oklch(70.7% 0.165 254.624) 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-blue-400) 60%, transparent); - } - } - .text-blue-400\/80 { - color: color-mix(in srgb, oklch(70.7% 0.165 254.624) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-blue-400) 80%, transparent); - } - } - .text-blue-600 { - color: var(--color-blue-600); - } - .text-cyan-200 { - color: var(--color-cyan-200); - } - .text-cyan-400 { - color: var(--color-cyan-400); - } - .text-cyan-400\/60 { - color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-cyan-400) 60%, transparent); - } - } - .text-cyan-400\/80 { - color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-cyan-400) 80%, transparent); - } - } - .text-cyan-500 { - color: var(--color-cyan-500); - } - .text-emerald-400 { - color: var(--color-emerald-400); - } - .text-gray-500 { - color: var(--color-gray-500); - } - .text-gray-800 { - color: var(--color-gray-800); - } - .text-green-400 { - color: var(--color-green-400); - } - .text-green-400\/60 { - color: color-mix(in srgb, oklch(79.2% 0.209 151.711) 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-green-400) 60%, transparent); - } - } - .text-green-400\/80 { - color: color-mix(in srgb, oklch(79.2% 0.209 151.711) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-green-400) 80%, transparent); - } - } - .text-md-error { - color: var(--color-md-error); - } - .text-md-on-error { - color: var(--color-md-on-error); - } - .text-md-on-error-container { - color: var(--color-md-on-error-container); - } - .text-md-on-primary { - color: var(--color-md-on-primary); - } - .text-md-on-primary-container { - color: var(--color-md-on-primary-container); - } - .text-md-on-secondary { - color: var(--color-md-on-secondary); - } - .text-md-on-secondary-container { - color: var(--color-md-on-secondary-container); - } - .text-md-on-surface { - color: var(--color-md-on-surface); - } - .text-md-on-surface-variant { - color: var(--color-md-on-surface-variant); - } - .text-md-on-tertiary-container { - color: var(--color-md-on-tertiary-container); - } - .text-md-outline { - color: var(--color-md-outline); - } - .text-md-outline\/60 { - color: color-mix(in srgb, #938F99 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-md-outline) 60%, transparent); - } - } - .text-md-primary { - color: var(--color-md-primary); - } - .text-purple-400 { - color: var(--color-purple-400); - } - .text-red-200 { - color: var(--color-red-200); - } - .text-red-400 { - color: var(--color-red-400); - } - .text-red-400\/60 { - color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-red-400) 60%, transparent); - } - } - .text-red-400\/70 { - color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-red-400) 70%, transparent); - } - } - .text-red-400\/80 { - color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-red-400) 80%, transparent); - } - } - .text-slate-200 { - color: var(--color-slate-200); - } - .text-slate-300 { - color: var(--color-slate-300); - } - .text-slate-400 { - color: var(--color-slate-400); - } - .text-slate-500 { - color: var(--color-slate-500); - } - .text-white { - color: var(--color-white); - } - .text-white\/20 { - color: color-mix(in srgb, #fff 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 20%, transparent); - } - } - .text-white\/30 { - color: color-mix(in srgb, #fff 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 30%, transparent); - } - } - .text-white\/40 { - color: color-mix(in srgb, #fff 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 40%, transparent); - } - } - .text-white\/50 { - color: color-mix(in srgb, #fff 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 50%, transparent); - } - } - .text-white\/60 { - color: color-mix(in srgb, #fff 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 60%, transparent); - } - } - .text-white\/70 { - color: color-mix(in srgb, #fff 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 70%, transparent); - } - } - .text-white\/80 { - color: color-mix(in srgb, #fff 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 80%, transparent); - } - } - .text-white\/90 { - color: color-mix(in srgb, #fff 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 90%, transparent); - } - } - .lowercase { - text-transform: lowercase; - } - .uppercase { - text-transform: uppercase; - } - .italic { - font-style: italic; - } - .no-underline { - text-decoration-line: none; - } - .underline { - text-decoration-line: underline; - } - .placeholder-md-on-surface-variant\/50 { - &::placeholder { - color: color-mix(in srgb, #CAC4D0 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-md-on-surface-variant) 50%, transparent); - } - } - } - .placeholder-md-outline { - &::placeholder { - color: var(--color-md-outline); - } - } - .placeholder-slate-500 { - &::placeholder { - color: var(--color-slate-500); - } - } - .accent-\[\#1e3a5f\] { - accent-color: #1e3a5f; - } - .accent-md-primary { - accent-color: var(--color-md-primary); - } - .opacity-0 { - opacity: 0%; - } - .opacity-30 { - opacity: 30%; - } - .opacity-60 { - opacity: 60%; - } - .opacity-80 { - opacity: 80%; - } - .opacity-100 { - opacity: 100%; - } - .shadow-2xl { - --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-\[0_0_15px_-3px_rgba\(34\,211\,238\,0\.3\)\] { - --tw-shadow: 0 0 15px -3px var(--tw-shadow-color, rgba(34,211,238,0.3)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-\[0_0_20px_-3px_rgba\(34\,211\,238\,0\.2\)\] { - --tw-shadow: 0 0 20px -3px var(--tw-shadow-color, rgba(34,211,238,0.2)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-lg { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-md { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-none { - --tw-shadow: 0 0 #0000; - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-xl { - --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .ring-2 { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .ring-md-error { - --tw-ring-color: var(--color-md-error); - } - .blur { - --tw-blur: blur(8px); - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - .backdrop-blur-2xl { - --tw-backdrop-blur: blur(var(--blur-2xl)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .backdrop-blur-3xl { - --tw-backdrop-blur: blur(var(--blur-3xl)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .backdrop-blur-md { - --tw-backdrop-blur: blur(var(--blur-md)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .backdrop-blur-sm { - --tw-backdrop-blur: blur(var(--blur-sm)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .backdrop-blur-xl { - --tw-backdrop-blur: blur(var(--blur-xl)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .backdrop-filter { - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-\[background\] { - transition-property: background; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-all { - transition-property: all; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-opacity { - transition-property: opacity; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-transform { - transition-property: transform, translate, scale, rotate; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .duration-150 { - --tw-duration: 150ms; - transition-duration: 150ms; - } - .duration-200 { - --tw-duration: 200ms; - transition-duration: 200ms; - } - .duration-300 { - --tw-duration: 300ms; - transition-duration: 300ms; - } - .duration-500 { - --tw-duration: 500ms; - transition-duration: 500ms; - } - .ease-in { - --tw-ease: var(--ease-in); - transition-timing-function: var(--ease-in); - } - .ease-out { - --tw-ease: var(--ease-out); - transition-timing-function: var(--ease-out); - } - .outline-none { - --tw-outline-style: none; - outline-style: none; - } - .select-all { - -webkit-user-select: all; - user-select: all; - } - .\[group\:2c2a\] { - group: 2c2a; - } - .\[program\:celery-beat\] { - program: celery-beat; - } - .\[program\:celery-worker\] { - program: celery-worker; - } - .\[program\:gunicorn\] { - program: gunicorn; - } - .group-hover\:bg-cyan-500\/30 { - &:is(:where(.group):hover *) { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent); - } - } - } - } - .group-hover\:text-cyan-400 { - &:is(:where(.group):hover *) { - @media (hover: hover) { - color: var(--color-cyan-400); - } - } - } - .peer-checked\:translate-x-5 { - &:is(:where(.peer):checked ~ *) { - --tw-translate-x: calc(var(--spacing) * 5); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - .peer-checked\:bg-cyan-500 { - &:is(:where(.peer):checked ~ *) { - background-color: var(--color-cyan-500); - } - } - .peer-checked\:bg-cyan-600 { - &:is(:where(.peer):checked ~ *) { - background-color: var(--color-cyan-600); - } - } - .peer-checked\:bg-md-primary { - &:is(:where(.peer):checked ~ *) { - background-color: var(--color-md-primary); - } - } - .peer-focus\:ring-1 { - &:is(:where(.peer):focus ~ *) { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .peer-focus\:ring-cyan-500\/50 { - &:is(:where(.peer):focus ~ *) { - --tw-ring-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-cyan-500) 50%, transparent); - } - } - } - .peer-focus\:outline-none { - &:is(:where(.peer):focus ~ *) { - --tw-outline-style: none; - outline-style: none; - } - } - .file\:mr-4 { - &::file-selector-button { - margin-right: calc(var(--spacing) * 4); - } - } - .file\:rounded-md { - &::file-selector-button { - border-radius: var(--radius-md); - } - } - .file\:border-0 { - &::file-selector-button { - border-style: var(--tw-border-style); - border-width: 0px; - } - } - .file\:bg-md-primary\/20 { - &::file-selector-button { - background-color: color-mix(in srgb, #D0BCFF 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 20%, transparent); - } - } - } - .file\:px-4 { - &::file-selector-button { - padding-inline: calc(var(--spacing) * 4); - } - } - .file\:py-1 { - &::file-selector-button { - padding-block: calc(var(--spacing) * 1); - } - } - .file\:text-sm { - &::file-selector-button { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - } - .file\:font-medium { - &::file-selector-button { - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - } - } - .file\:text-md-primary { - &::file-selector-button { - color: var(--color-md-primary); - } - } - .before\:absolute { - &::before { - content: var(--tw-content); - position: absolute; - } - } - .before\:bottom-1 { - &::before { - content: var(--tw-content); - bottom: calc(var(--spacing) * 1); - } - } - .before\:left-1 { - &::before { - content: var(--tw-content); - left: calc(var(--spacing) * 1); - } - } - .before\:h-6 { - &::before { - content: var(--tw-content); - height: calc(var(--spacing) * 6); - } - } - .before\:w-6 { - &::before { - content: var(--tw-content); - width: calc(var(--spacing) * 6); - } - } - .before\:rounded-full { - &::before { - content: var(--tw-content); - border-radius: calc(infinity * 1px); - } - } - .before\:bg-md-outline { - &::before { - content: var(--tw-content); - background-color: var(--color-md-outline); - } - } - .before\:transition { - &::before { - content: var(--tw-content); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - } - .before\:content-\[\'\'\] { - &::before { - --tw-content: ''; - content: var(--tw-content); - } - } - .peer-checked\:before\:translate-x-5 { - &:is(:where(.peer):checked ~ *) { - &::before { - content: var(--tw-content); - --tw-translate-x: calc(var(--spacing) * 5); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - } - .peer-checked\:before\:bg-md-on-primary { - &:is(:where(.peer):checked ~ *) { - &::before { - content: var(--tw-content); - background-color: var(--color-md-on-primary); - } - } - } - .after\:absolute { - &::after { - content: var(--tw-content); - position: absolute; - } - } - .after\:start-\[2px\] { - &::after { - content: var(--tw-content); - inset-inline-start: 2px; - } - } - .after\:top-\[2px\] { - &::after { - content: var(--tw-content); - top: 2px; - } - } - .after\:h-4 { - &::after { - content: var(--tw-content); - height: calc(var(--spacing) * 4); - } - } - .after\:w-4 { - &::after { - content: var(--tw-content); - width: calc(var(--spacing) * 4); - } - } - .after\:rounded-full { - &::after { - content: var(--tw-content); - border-radius: calc(infinity * 1px); - } - } - .after\:bg-white { - &::after { - content: var(--tw-content); - background-color: var(--color-white); - } - } - .after\:transition-all { - &::after { - content: var(--tw-content); - transition-property: all; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - } - .after\:content-\[\'\'\] { - &::after { - --tw-content: ''; - content: var(--tw-content); - } - } - .peer-checked\:after\:translate-x-full { - &:is(:where(.peer):checked ~ *) { - &::after { - content: var(--tw-content); - --tw-translate-x: 100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - } - .peer-checked\:after\:border-white { - &:is(:where(.peer):checked ~ *) { - &::after { - content: var(--tw-content); - border-color: var(--color-white); - } - } - } - .last\:mb-0 { - &:last-child { - margin-bottom: calc(var(--spacing) * 0); - } - } - .last\:border-0 { - &:last-child { - border-style: var(--tw-border-style); - border-width: 0px; - } - } - .last\:pb-0 { - &:last-child { - padding-bottom: calc(var(--spacing) * 0); - } - } - .hover\:-translate-y-0\.5 { - &:hover { - @media (hover: hover) { - --tw-translate-y: calc(var(--spacing) * -0.5); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - } - .hover\:scale-105 { - &:hover { - @media (hover: hover) { - --tw-scale-x: 105%; - --tw-scale-y: 105%; - --tw-scale-z: 105%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } - } - } - .hover\:scale-110 { - &:hover { - @media (hover: hover) { - --tw-scale-x: 110%; - --tw-scale-y: 110%; - --tw-scale-z: 110%; - scale: var(--tw-scale-x) var(--tw-scale-y); - } - } - } - .hover\:border-cyan-500\/30 { - &:hover { - @media (hover: hover) { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent); - } - } - } - } - .hover\:border-cyan-500\/40 { - &:hover { - @media (hover: hover) { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 40%, transparent); - } - } - } - } - .hover\:border-cyan-500\/50 { - &:hover { - @media (hover: hover) { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 50%, transparent); - } - } - } - } - .hover\:border-md-primary { - &:hover { - @media (hover: hover) { - border-color: var(--color-md-primary); - } - } - } - .hover\:\!bg-red-600 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-600) !important; - } - } - } - .hover\:bg-amber-500\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); - } - } - } - } - .hover\:bg-amber-500\/20 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 20%, transparent); - } - } - } - } - .hover\:bg-amber-500\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-500) 30%, transparent); - } - } - } - } - .hover\:bg-amber-600\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(66.6% 0.179 58.318) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-amber-600) 30%, transparent); - } - } - } - } - .hover\:bg-cyan-500 { - &:hover { - @media (hover: hover) { - background-color: var(--color-cyan-500); - } - } - } - .hover\:bg-cyan-500\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 10%, transparent); - } - } - } - } - .hover\:bg-cyan-500\/20 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 20%, transparent); - } - } - } - } - .hover\:bg-cyan-500\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-500) 30%, transparent); - } - } - } - } - .hover\:bg-cyan-600\/5 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(60.9% 0.126 221.723) 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-600) 5%, transparent); - } - } - } - } - .hover\:bg-cyan-600\/25 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(60.9% 0.126 221.723) 25%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-600) 25%, transparent); - } - } - } - } - .hover\:bg-cyan-600\/90 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(60.9% 0.126 221.723) 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-cyan-600) 90%, transparent); - } - } - } - } - .hover\:bg-emerald-500\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-500) 30%, transparent); - } - } - } - } - .hover\:bg-emerald-600\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(59.6% 0.145 163.225) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-600) 30%, transparent); - } - } - } - } - .hover\:bg-green-500\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 10%, transparent); - } - } - } - } - .hover\:bg-green-500\/20 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); - } - } - } - } - .hover\:bg-green-500\/25 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 25%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-500) 25%, transparent); - } - } - } - } - .hover\:bg-md-error\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #F2B8B5 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error) 10%, transparent); - } - } - } - } - .hover\:bg-md-error\/20 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #F2B8B5 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error) 20%, transparent); - } - } - } - } - .hover\:bg-md-error\/90 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #F2B8B5 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-error) 90%, transparent); - } - } - } - } - .hover\:bg-md-primary\/8 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #D0BCFF 8%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 8%, transparent); - } - } - } - } - .hover\:bg-md-primary\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #D0BCFF 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 10%, transparent); - } - } - } - } - .hover\:bg-md-primary\/90 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #D0BCFF 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 90%, transparent); - } - } - } - } - .hover\:bg-md-secondary-container { - &:hover { - @media (hover: hover) { - background-color: var(--color-md-secondary-container); - } - } - } - .hover\:bg-md-secondary\/90 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #CCC2DC 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-secondary) 90%, transparent); - } - } - } - } - .hover\:bg-md-surface-container { - &:hover { - @media (hover: hover) { - background-color: var(--color-md-surface-container); - } - } - } - .hover\:bg-md-surface-container-high { - &:hover { - @media (hover: hover) { - background-color: var(--color-md-surface-container-high); - } - } - } - .hover\:bg-md-surface-container\/95 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #211F26 95%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-surface-container) 95%, transparent); - } - } - } - } - .hover\:bg-md-surface-variant { - &:hover { - @media (hover: hover) { - background-color: var(--color-md-surface-variant); - } - } - } - .hover\:bg-red-500\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent); - } - } - } - } - .hover\:bg-red-500\/20 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 20%, transparent); - } - } - } - } - .hover\:bg-red-500\/25 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 25%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 25%, transparent); - } - } - } - } - .hover\:bg-red-500\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-500) 30%, transparent); - } - } - } - } - .hover\:bg-red-600 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-600); - } - } - } - .hover\:bg-slate-700\/50 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-700) 50%, transparent); - } - } - } - } - .hover\:bg-slate-800 { - &:hover { - @media (hover: hover) { - background-color: var(--color-slate-800); - } - } - } - .hover\:bg-slate-800\/50 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-800) 50%, transparent); - } - } - } - } - .hover\:bg-slate-900\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 30%, transparent); - } - } - } - } - .hover\:bg-slate-950\/80 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-950) 80%, transparent); - } - } - } - } - .hover\:bg-white\/5 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - } - } - .hover\:bg-white\/10 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 10%, transparent); - } - } - } - } - .hover\:bg-white\/30 { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 30%, transparent); - } - } - } - } - .hover\:bg-white\/\[0\.02\] { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 2%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 2%, transparent); - } - } - } - } - .hover\:bg-white\/\[0\.05\] { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 5%, transparent); - } - } - } - } - .hover\:text-amber-400 { - &:hover { - @media (hover: hover) { - color: var(--color-amber-400); - } - } - } - .hover\:text-blue-400 { - &:hover { - @media (hover: hover) { - color: var(--color-blue-400); - } - } - } - .hover\:text-cyan-300 { - &:hover { - @media (hover: hover) { - color: var(--color-cyan-300); - } - } - } - .hover\:text-cyan-400 { - &:hover { - @media (hover: hover) { - color: var(--color-cyan-400); - } - } - } - .hover\:text-green-400 { - &:hover { - @media (hover: hover) { - color: var(--color-green-400); - } - } - } - .hover\:text-md-on-surface { - &:hover { - @media (hover: hover) { - color: var(--color-md-on-surface); - } - } - } - .hover\:text-md-primary { - &:hover { - @media (hover: hover) { - color: var(--color-md-primary); - } - } - } - .hover\:text-red-300 { - &:hover { - @media (hover: hover) { - color: var(--color-red-300); - } - } - } - .hover\:text-red-400 { - &:hover { - @media (hover: hover) { - color: var(--color-red-400); - } - } - } - .hover\:text-slate-200 { - &:hover { - @media (hover: hover) { - color: var(--color-slate-200); - } - } - } - .hover\:text-white { - &:hover { - @media (hover: hover) { - color: var(--color-white); - } - } - } - .hover\:text-white\/70 { - &:hover { - @media (hover: hover) { - color: color-mix(in srgb, #fff 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 70%, transparent); - } - } - } - } - .hover\:underline { - &:hover { - @media (hover: hover) { - text-decoration-line: underline; - } - } - } - .hover\:opacity-80 { - &:hover { - @media (hover: hover) { - opacity: 80%; - } - } - } - .hover\:opacity-90 { - &:hover { - @media (hover: hover) { - opacity: 90%; - } - } - } - .hover\:shadow-2xl { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:shadow-lg { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:shadow-md { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:shadow-xl { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .hover\:file\:bg-md-primary\/30 { - &:hover { - @media (hover: hover) { - &::file-selector-button { - background-color: color-mix(in srgb, #D0BCFF 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-md-primary) 30%, transparent); - } - } - } - } - } - .focus\:border-cyan-500 { - &:focus { - border-color: var(--color-cyan-500); - } - } - .focus\:border-cyan-500\/50 { - &:focus { - border-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-cyan-500) 50%, transparent); - } - } - } - .focus\:border-transparent { - &:focus { - border-color: transparent; - } - } - .focus\:ring-1 { - &:focus { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .focus\:ring-2 { - &:focus { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .focus\:ring-cyan-500 { - &:focus { - --tw-ring-color: var(--color-cyan-500); - } - } - .focus\:ring-cyan-500\/50 { - &:focus { - --tw-ring-color: color-mix(in srgb, oklch(71.5% 0.143 215.221) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-cyan-500) 50%, transparent); - } - } - } - .focus\:ring-md-error { - &:focus { - --tw-ring-color: var(--color-md-error); - } - } - .focus\:ring-md-primary { - &:focus { - --tw-ring-color: var(--color-md-primary); - } - } - .focus\:ring-md-secondary { - &:focus { - --tw-ring-color: var(--color-md-secondary); - } - } - .focus\:outline-none { - &:focus { - --tw-outline-style: none; - outline-style: none; - } - } - .disabled\:cursor-not-allowed { - &:disabled { - cursor: not-allowed; - } - } - .disabled\:bg-md-surface-variant { - &:disabled { - background-color: var(--color-md-surface-variant); - } - } - .disabled\:text-md-on-surface-variant { - &:disabled { - color: var(--color-md-on-surface-variant); - } - } - .disabled\:opacity-50 { - &:disabled { - opacity: 50%; - } - } - .disabled\:opacity-60 { - &:disabled { - opacity: 60%; - } - } - .max-md\:absolute { - @media (width < 48rem) { - position: absolute; - } - } - .max-md\:top-16 { - @media (width < 48rem) { - top: calc(var(--spacing) * 16); - } - } - .max-md\:right-0 { - @media (width < 48rem) { - right: calc(var(--spacing) * 0); - } - } - .max-md\:left-0 { - @media (width < 48rem) { - left: calc(var(--spacing) * 0); - } - } - .max-md\:z-40 { - @media (width < 48rem) { - z-index: 40; - } - } - .max-md\:block { - @media (width < 48rem) { - display: block; - } - } - .max-md\:flex { - @media (width < 48rem) { - display: flex; - } - } - .max-md\:hidden { - @media (width < 48rem) { - display: none; - } - } - .max-md\:w-full { - @media (width < 48rem) { - width: 100%; - } - } - .max-md\:flex-col { - @media (width < 48rem) { - flex-direction: column; - } - } - .max-md\:justify-start { - @media (width < 48rem) { - justify-content: flex-start; - } - } - .max-md\:gap-6 { - @media (width < 48rem) { - gap: calc(var(--spacing) * 6); - } - } - .max-md\:rounded-md { - @media (width < 48rem) { - border-radius: var(--radius-md); - } - } - .max-md\:bg-md-surface { - @media (width < 48rem) { - background-color: var(--color-md-surface); - } - } - .max-md\:p-4 { - @media (width < 48rem) { - padding: calc(var(--spacing) * 4); - } - } - .max-md\:px-4 { - @media (width < 48rem) { - padding-inline: calc(var(--spacing) * 4); - } - } - .max-md\:py-3 { - @media (width < 48rem) { - padding-block: calc(var(--spacing) * 3); - } - } - .max-md\:shadow-2xl { - @media (width < 48rem) { - --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .max-sm\:flex-col { - @media (width < 40rem) { - flex-direction: column; - } - } - .max-sm\:items-start { - @media (width < 40rem) { - align-items: flex-start; - } - } - .max-sm\:gap-2 { - @media (width < 40rem) { - gap: calc(var(--spacing) * 2); - } - } - .max-sm\:gap-3 { - @media (width < 40rem) { - gap: calc(var(--spacing) * 3); - } - } - .sm\:col-span-2 { - @media (width >= 40rem) { - grid-column: span 2 / span 2; - } - } - .sm\:mb-0 { - @media (width >= 40rem) { - margin-bottom: calc(var(--spacing) * 0); - } - } - .sm\:block { - @media (width >= 40rem) { - display: block; - } - } - .sm\:flex { - @media (width >= 40rem) { - display: flex; - } - } - .sm\:w-1\/2 { - @media (width >= 40rem) { - width: calc(1 / 2 * 100%); - } - } - .sm\:w-1\/4 { - @media (width >= 40rem) { - width: calc(1 / 4 * 100%); - } - } - .sm\:w-3\/4 { - @media (width >= 40rem) { - width: calc(3 / 4 * 100%); - } - } - .sm\:w-36 { - @media (width >= 40rem) { - width: calc(var(--spacing) * 36); - } - } - .sm\:w-40 { - @media (width >= 40rem) { - width: calc(var(--spacing) * 40); - } - } - .sm\:w-44 { - @media (width >= 40rem) { - width: calc(var(--spacing) * 44); - } - } - .sm\:w-48 { - @media (width >= 40rem) { - width: calc(var(--spacing) * 48); - } - } - .sm\:min-w-\[150px\] { - @media (width >= 40rem) { - min-width: 150px; - } - } - .sm\:min-w-\[200px\] { - @media (width >= 40rem) { - min-width: 200px; - } - } - .sm\:grid-cols-2 { - @media (width >= 40rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .sm\:grid-cols-3 { - @media (width >= 40rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - .sm\:grid-cols-\[repeat\(auto-fill\,minmax\(350px\,1fr\)\)\] { - @media (width >= 40rem) { - grid-template-columns: repeat(auto-fill,minmax(350px,1fr)); - } - } - .sm\:grid-cols-\[repeat\(auto-fit\,minmax\(250px\,1fr\)\)\] { - @media (width >= 40rem) { - grid-template-columns: repeat(auto-fit,minmax(250px,1fr)); - } - } - .sm\:grid-cols-\[repeat\(auto-fit\,minmax\(280px\,1fr\)\)\] { - @media (width >= 40rem) { - grid-template-columns: repeat(auto-fit,minmax(280px,1fr)); - } - } - .sm\:grid-cols-\[repeat\(auto-fit\,minmax\(300px\,1fr\)\)\] { - @media (width >= 40rem) { - grid-template-columns: repeat(auto-fit,minmax(300px,1fr)); - } - } - .sm\:flex-row { - @media (width >= 40rem) { - flex-direction: row; - } - } - .sm\:flex-wrap { - @media (width >= 40rem) { - flex-wrap: wrap; - } - } - .sm\:items-center { - @media (width >= 40rem) { - align-items: center; - } - } - .sm\:items-end { - @media (width >= 40rem) { - align-items: flex-end; - } - } - .sm\:items-start { - @media (width >= 40rem) { - align-items: flex-start; - } - } - .sm\:justify-between { - @media (width >= 40rem) { - justify-content: space-between; - } - } - .sm\:gap-4 { - @media (width >= 40rem) { - gap: calc(var(--spacing) * 4); - } - } - .sm\:gap-6 { - @media (width >= 40rem) { - gap: calc(var(--spacing) * 6); - } - } - .sm\:p-4 { - @media (width >= 40rem) { - padding: calc(var(--spacing) * 4); - } - } - .sm\:p-6 { - @media (width >= 40rem) { - padding: calc(var(--spacing) * 6); - } - } - .sm\:p-8 { - @media (width >= 40rem) { - padding: calc(var(--spacing) * 8); - } - } - .sm\:px-4 { - @media (width >= 40rem) { - padding-inline: calc(var(--spacing) * 4); - } - } - .sm\:px-8 { - @media (width >= 40rem) { - padding-inline: calc(var(--spacing) * 8); - } - } - .sm\:py-3 { - @media (width >= 40rem) { - padding-block: calc(var(--spacing) * 3); - } - } - .sm\:py-4 { - @media (width >= 40rem) { - padding-block: calc(var(--spacing) * 4); - } - } - .md\:hidden { - @media (width >= 48rem) { - display: none; - } - } - .md\:grid-cols-2 { - @media (width >= 48rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .lg\:col-span-2 { - @media (width >= 64rem) { - grid-column: span 2 / span 2; - } - } - .lg\:block { - @media (width >= 64rem) { - display: block; - } - } - .lg\:flex { - @media (width >= 64rem) { - display: flex; - } - } - .lg\:hidden { - @media (width >= 64rem) { - display: none; - } - } - .lg\:w-1\/2 { - @media (width >= 64rem) { - width: calc(1 / 2 * 100%); - } - } - .lg\:w-1\/3 { - @media (width >= 64rem) { - width: calc(1 / 3 * 100%); - } - } - .lg\:w-2\/3 { - @media (width >= 64rem) { - width: calc(2 / 3 * 100%); - } - } - .lg\:grid-cols-2 { - @media (width >= 64rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .lg\:grid-cols-3 { - @media (width >= 64rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - .lg\:grid-cols-4 { - @media (width >= 64rem) { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - } - .lg\:grid-cols-5 { - @media (width >= 64rem) { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - } - .lg\:grid-cols-6 { - @media (width >= 64rem) { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - } - .lg\:p-6 { - @media (width >= 64rem) { - padding: calc(var(--spacing) * 6); - } - } - .lg\:px-6 { - @media (width >= 64rem) { - padding-inline: calc(var(--spacing) * 6); - } - } - .xl\:block { - @media (width >= 80rem) { - display: block; - } - } - .rtl\:peer-checked\:after\:-translate-x-full { - &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) { - &:is(:where(.peer):checked ~ *) { - &::after { - content: var(--tw-content); - --tw-translate-x: -100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - } - } - .dark\:border { - @media (prefers-color-scheme: dark) { - border-style: var(--tw-border-style); - border-width: 1px; - } - } - .dark\:border-\[\#10b981\]\/30 { - @media (prefers-color-scheme: dark) { - border-color: color-mix(in oklab, #10b981 30%, transparent); - } - } - .dark\:bg-\[\#10b981\]\/20 { - @media (prefers-color-scheme: dark) { - background-color: color-mix(in oklab, #10b981 20%, transparent); - } - } - .dark\:accent-\[\#10b981\] { - @media (prefers-color-scheme: dark) { - accent-color: #10b981; - } - } - .dark\:hover\:bg-\[\#10b981\]\/30 { - @media (prefers-color-scheme: dark) { - &:hover { - @media (hover: hover) { - background-color: color-mix(in oklab, #10b981 30%, transparent); - } - } - } - } -} -:root { - color-scheme: dark; -} -select option { - background-color: #1e293b; - color: #ffffff; -} -select option:hover, select option:checked { - background-color: #334155; - color: #ffffff; -} -@property --tw-translate-x { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-y { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-z { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-scale-x { - syntax: "*"; - inherits: false; - initial-value: 1; -} -@property --tw-scale-y { - syntax: "*"; - inherits: false; - initial-value: 1; -} -@property --tw-scale-z { - syntax: "*"; - inherits: false; - initial-value: 1; -} -@property --tw-rotate-x { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-y { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-z { - syntax: "*"; - inherits: false; -} -@property --tw-skew-x { - syntax: "*"; - inherits: false; -} -@property --tw-skew-y { - syntax: "*"; - inherits: false; -} -@property --tw-space-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-gradient-position { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-via { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-to { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-via-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from-position { - syntax: ""; - inherits: false; - initial-value: 0%; -} -@property --tw-gradient-via-position { - syntax: ""; - inherits: false; - initial-value: 50%; -} -@property --tw-gradient-to-position { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-leading { - syntax: "*"; - inherits: false; -} -@property --tw-font-weight { - syntax: "*"; - inherits: false; -} -@property --tw-tracking { - syntax: "*"; - inherits: false; -} -@property --tw-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-inset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-ring-inset { - syntax: "*"; - inherits: false; -} -@property --tw-ring-offset-width { - syntax: ""; - inherits: false; - initial-value: 0px; -} -@property --tw-ring-offset-color { - syntax: "*"; - inherits: false; - initial-value: #fff; -} -@property --tw-ring-offset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-blur { - syntax: "*"; - inherits: false; -} -@property --tw-brightness { - syntax: "*"; - inherits: false; -} -@property --tw-contrast { - syntax: "*"; - inherits: false; -} -@property --tw-grayscale { - syntax: "*"; - inherits: false; -} -@property --tw-hue-rotate { - syntax: "*"; - inherits: false; -} -@property --tw-invert { - syntax: "*"; - inherits: false; -} -@property --tw-opacity { - syntax: "*"; - inherits: false; -} -@property --tw-saturate { - syntax: "*"; - inherits: false; -} -@property --tw-sepia { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-drop-shadow-size { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-blur { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-brightness { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-contrast { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-grayscale { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-hue-rotate { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-invert { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-opacity { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-saturate { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-sepia { - syntax: "*"; - inherits: false; -} -@property --tw-duration { - syntax: "*"; - inherits: false; -} -@property --tw-ease { - syntax: "*"; - inherits: false; -} -@property --tw-content { - syntax: "*"; - initial-value: ""; - inherits: false; -} -@keyframes spin { - to { - transform: rotate(360deg); - } -} -@keyframes pulse { - 50% { - opacity: 0.5; - } -} -@keyframes bounce { - 0%, 100% { - transform: translateY(-25%); - animation-timing-function: cubic-bezier(0.8, 0, 1, 1); - } - 50% { - transform: none; - animation-timing-function: cubic-bezier(0, 0, 0.2, 1); - } -} -@layer properties { - @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { - *, ::before, ::after, ::backdrop { - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-translate-z: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-scale-z: 1; - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; - --tw-space-y-reverse: 0; - --tw-divide-y-reverse: 0; - --tw-border-style: solid; - --tw-gradient-position: initial; - --tw-gradient-from: #0000; - --tw-gradient-via: #0000; - --tw-gradient-to: #0000; - --tw-gradient-stops: initial; - --tw-gradient-via-stops: initial; - --tw-gradient-from-position: 0%; - --tw-gradient-via-position: 50%; - --tw-gradient-to-position: 100%; - --tw-leading: initial; - --tw-font-weight: initial; - --tw-tracking: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; - --tw-blur: initial; - --tw-brightness: initial; - --tw-contrast: initial; - --tw-grayscale: initial; - --tw-hue-rotate: initial; - --tw-invert: initial; - --tw-opacity: initial; - --tw-saturate: initial; - --tw-sepia: initial; - --tw-drop-shadow: initial; - --tw-drop-shadow-color: initial; - --tw-drop-shadow-alpha: 100%; - --tw-drop-shadow-size: initial; - --tw-backdrop-blur: initial; - --tw-backdrop-brightness: initial; - --tw-backdrop-contrast: initial; - --tw-backdrop-grayscale: initial; - --tw-backdrop-hue-rotate: initial; - --tw-backdrop-invert: initial; - --tw-backdrop-opacity: initial; - --tw-backdrop-saturate: initial; - --tw-backdrop-sepia: initial; - --tw-duration: initial; - --tw-ease: initial; - --tw-content: ""; - } - } -} diff --git a/static/css/theme.css b/static/css/theme.css deleted file mode 100644 index e2933a2..0000000 --- a/static/css/theme.css +++ /dev/null @@ -1,467 +0,0 @@ -/* - * 2c2a 设计系统 - 主题变量 - * 配色方案:浅色模式(深蓝强调色) / 深色模式(绿色主色调) - * 特性:毛玻璃效果(Glassmorphism)、现代化设计 - */ - -:root { - /* ==================== 颜色系统 - 浅色模式 ==================== */ - - /* 主要颜色 (Primary) - 深蓝色系 */ - --primary-color: #1e3a5f; - --primary-color-light: #2d5a8f; - --primary-color-dark: #0f1f33; - --primary-color-rgb: 30, 58, 95; - - /* 次要颜色 (Secondary) - 绿色系 */ - --secondary-color: #10b981; - --secondary-color-light: #34d399; - --secondary-color-dark: #059669; - --secondary-color-rgb: 16, 185, 129; - - /* 强调色 (Accent) */ - --accent-color: #06b6d4; - --accent-color-light: #22d3ee; - --accent-color-dark: #0891b2; - - /* 功能颜色 */ - --success-color: #10b981; - --success-color-rgb: 16, 185, 129; - --warning-color: #f59e0b; - --warning-color-rgb: 245, 158, 11; - --danger-color: #ef4444; - --danger-color-rgb: 239, 68, 68; - --info-color: #3b82f6; - --info-color-rgb: 59, 130, 246; - - /* 背景色 (Background) - 浅色模式 */ - --bg-primary: #ffffff; - --bg-secondary: #f8fafc; - --bg-tertiary: #f1f5f9; - --bg-glass: rgba(255, 255, 255, 0.7); - --bg-glass-heavy: rgba(255, 255, 255, 0.85); - - /* 文字颜色 (Text) - 浅色模式 */ - --text-primary: #0f172a; - --text-secondary: #475569; - --text-tertiary: #94a3b8; - --text-inverse: #ffffff; - - /* 边框颜色 (Border) */ - --border-color: rgba(0, 0, 0, 0.08); - --border-color-light: rgba(0, 0, 0, 0.05); - --border-glass: rgba(255, 255, 255, 0.18); - - /* 表面色 (Surface) */ - --surface-color: #ffffff; - --surface-color-elevated: #ffffff; - --surface-color-overlay: rgba(255, 255, 255, 0.9); - - /* ==================== 形状系统 ==================== */ - - /* 圆角 (Shape) */ - --radius-none: 0px; - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-xl: 24px; - --radius-full: 9999px; - - /* ==================== 排版系统 ==================== */ - - /* 字体家族 */ - --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; - - /* 字体大小 (Typography - Size) */ - --font-size-xs: 0.75rem; /* 12px */ - --font-size-sm: 0.875rem; /* 14px */ - --font-size-base: 1rem; /* 16px */ - --font-size-lg: 1.125rem; /* 18px */ - --font-size-xl: 1.25rem; /* 20px */ - --font-size-2xl: 1.5rem; /* 24px */ - --font-size-3xl: 1.875rem; /* 30px */ - --font-size-4xl: 2.25rem; /* 36px */ - - /* 字体粗细 (Typography - Weight) */ - --font-weight-normal: 400; - --font-weight-medium: 500; - --font-weight-semibold: 600; - --font-weight-bold: 700; - - /* 行高 (Line Height) */ - --line-height-tight: 1.25; - --line-height-normal: 1.5; - --line-height-relaxed: 1.75; - - /* ==================== 间距系统 ==================== */ - - /* 内边距 (Padding) */ - --spacing-0: 0px; - --spacing-1: 4px; - --spacing-2: 8px; - --spacing-3: 12px; - --spacing-4: 16px; - --spacing-5: 20px; - --spacing-6: 24px; - --spacing-8: 32px; - --spacing-10: 40px; - --spacing-12: 48px; - - /* 外边距 (Margin) - 与内边距相同 */ - --margin-0: 0px; - --margin-1: 4px; - --margin-2: 8px; - --margin-3: 12px; - --margin-4: 16px; - --margin-5: 20px; - --margin-6: 24px; - --margin-8: 32px; - - /* ==================== 阴影系统 ==================== */ - - /* 基础阴影 */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - - /* 毛玻璃阴影 */ - --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); - --glass-shadow-lg: 0 8px 32px 0 rgba(31, 38, 135, 0.25); - - /* 彩色阴影 */ - --shadow-primary: 0 4px 14px 0 rgba(var(--primary-color-rgb), 0.25); - --shadow-secondary: 0 4px 14px 0 rgba(var(--secondary-color-rgb), 0.25); - - /* ==================== 过渡动画 ==================== */ - - /* 动画持续时间 */ - --duration-fast: 150ms; - --duration-normal: 250ms; - --duration-slow: 350ms; - --duration-slower: 500ms; - - /* 缓动函数 */ - --ease-default: cubic-bezier(0.4, 0, 0.2, 1); - --ease-in: cubic-bezier(0.4, 0, 1, 1); - --ease-out: cubic-bezier(0, 0, 0.2, 1); - --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); - - /* ==================== 毛玻璃效果 (Glassmorphism) ==================== */ - - /* 标准毛玻璃 */ - --glass-bg: rgba(255, 255, 255, 0.7); - --glass-blur: blur(12px); - --glass-border: 1px solid rgba(255, 255, 255, 0.18); - --glass-border-radius: var(--radius-lg); - - /* 强毛玻璃效果 */ - --glass-heavy-bg: rgba(255, 255, 255, 0.85); - --glass-heavy-blur: blur(16px); - - /* 轻量毛玻璃效果 */ - --glass-light-bg: rgba(255, 255, 255, 0.5); - --glass-light-blur: blur(8px); - - /* ==================== 断点系统 ==================== */ - - /* 响应式断点 */ - --breakpoint-sm: 640px; - --breakpoint-md: 768px; - --breakpoint-lg: 1024px; - --breakpoint-xl: 1280px; - --breakpoint-2xl: 1536px; - - /* ==================== 组件特定变量 ==================== */ - - /* 导航栏高度 */ - --navbar-height: 64px; - --navbar-height-mobile: 56px; - - /* 卡片样式 */ - --card-padding: var(--spacing-6); - --card-radius: var(--radius-lg); - --card-shadow: var(--shadow-md); - - /* 按钮尺寸 */ - --button-height: 40px; - --button-padding-x: var(--spacing-6); - --button-padding-y: var(--spacing-2); - --button-radius: var(--radius-md); - - /* 输入框尺寸 */ - --input-height: 48px; - --input-padding-x: var(--spacing-4); - --input-padding-y: var(--spacing-3); - --input-radius: var(--radius-md); - - /* 容器最大宽度 */ - --container-max-width: 1200px; -} - -/* ==================== 深色模式变量 ==================== */ -[data-theme="dark"] { - /* 主要颜色 - 保持深蓝色但在深色模式下调整亮度 */ - --primary-color: #60a5fa; - --primary-color-light: #93c5fd; - --primary-color-dark: #3b82f6; - --primary-color-rgb: 96, 165, 250; - - /* 次要颜色 - 绿色在深色模式下更亮 */ - --secondary-color: #10b981; - --secondary-color-light: #34d399; - --secondary-color-dark: #059669; - --secondary-color-rgb: 16, 185, 129; - - /* 强调色 */ - --accent-color: #22d3ee; - --accent-color-light: #67e8f9; - --accent-color-dark: #06b6d4; - - /* 背景色 - 深色模式 */ - --bg-primary: #0f172a; - --bg-secondary: #1e293b; - --bg-tertiary: #334155; - --bg-glass: rgba(15, 23, 42, 0.7); - --bg-glass-heavy: rgba(15, 23, 42, 0.85); - - /* 文字颜色 - 深色模式 */ - --text-primary: #f1f5f9; - --text-secondary: #cbd5e1; - --text-tertiary: #94a3b8; - --text-inverse: #ffffff; - - /* 边框颜色 - 深色模式 */ - --border-color: rgba(255, 255, 255, 0.1); - --border-color-light: rgba(255, 255, 255, 0.05); - --border-glass: rgba(255, 255, 255, 0.18); - - /* 表面色 - 深色模式 */ - --surface-color: #1e293b; - --surface-color-elevated: #334155; - --surface-color-overlay: rgba(30, 41, 59, 0.9); - - /* 阴影 - 深色模式使用更柔和的阴影 */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); - - /* 毛玻璃效果 - 深色模式 */ - --glass-bg: rgba(30, 41, 59, 0.7); - --glass-heavy-bg: rgba(30, 41, 59, 0.85); - --glass-light-bg: rgba(30, 41, 59, 0.5); - - /* 彩色阴影 - 深色模式增强发光效果 */ - --shadow-primary: 0 4px 14px 0 rgba(var(--primary-color-rgb), 0.35); - --shadow-secondary: 0 4px 14px 0 rgba(var(--secondary-color-rgb), 0.35); - --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); - --glass-shadow-lg: 0 8px 32px 0 rgba(0, 0, 0, 0.45); -} - -/* ==================== 高对比度模式 ==================== */ -@media (prefers-contrast: high) { - :root { - --border-color: rgba(0, 0, 0, 0.3); - --text-secondary: #334155; - } - - [data-theme="dark"] { - --border-color: rgba(255, 255, 255, 0.3); - --text-secondary: #e2e8f0; - } -} - -/* ==================== 减少动画偏好 ==================== */ -@media (prefers-reduced-motion: reduce) { - :root, - [data-theme="dark"] { - --duration-fast: 0ms; - --duration-normal: 0ms; - --duration-slow: 0ms; - --duration-slower: 0ms; - } -} - -/* ==================== Material Design 3 (MD3) 变量映射 ==================== */ -/* 这些变量用于兼容 base.html 中使用的 MD3 风格变量名 */ - -:root { - /* MD3 颜色系统 */ - --md-sys-color-primary: var(--primary-color); - --md-sys-color-on-primary: #ffffff; - --md-sys-color-primary-container: rgba(var(--primary-color-rgb), 0.12); - --md-sys-color-on-primary-container: var(--primary-color); - - --md-sys-color-secondary: var(--secondary-color); - --md-sys-color-on-secondary: #ffffff; - --md-sys-color-secondary-container: rgba(var(--secondary-color-rgb), 0.12); - --md-sys-color-on-secondary-container: var(--secondary-color-dark); - - --md-sys-color-tertiary: var(--accent-color); - --md-sys-color-on-tertiary: #ffffff; - - --md-sys-color-error: var(--danger-color); - --md-sys-color-on-error: #ffffff; - --md-sys-color-error-container: rgba(var(--danger-color-rgb), 0.12); - --md-sys-color-on-error-container: var(--danger-color); - - /* MD3 表面色 */ - --md-sys-color-surface: var(--surface-color); - --md-sys-color-on-surface: var(--text-primary); - --md-sys-color-surface-variant: var(--bg-tertiary); - --md-sys-color-on-surface-variant: var(--text-secondary); - --md-sys-color-surface-container: var(--bg-secondary); - --md-sys-color-surface-container-high: var(--bg-tertiary); - - --md-sys-color-outline: var(--border-color); - --md-sys-color-outline-variant: rgba(0, 0, 0, 0.12); - - /* MD3 警告色 */ - --md-sys-color-warning-container: rgba(var(--warning-color-rgb), 0.15); - --md-sys-color-on-warning-container: #92400e; - - /* MD3 信息色 */ - --md-sys-color-info-container: rgba(var(--info-color-rgb), 0.15); - --md-sys-color-on-info-container: #1e40af; - - /* MD3 成功色 */ - --md-sys-color-success: var(--success-color); - --md-sys-color-success-container: rgba(var(--success-color-rgb), 0.15); - --md-sys-color-on-success-container: #065f46; - - /* MD3 阴影 */ - --md-sys-elevation-level1: var(--shadow-sm); - --md-sys-elevation-level2: var(--shadow-md); - --md-sys-elevation-level3: var(--shadow-lg); - --md-sys-elevation-level4: var(--shadow-xl); - - /* MD3 形状 */ - --md-sys-shape-corner-none: var(--radius-none); - --md-sys-shape-corner-extra-small: var(--radius-sm); - --md-sys-shape-corner-small: var(--radius-sm); - --md-sys-shape-corner-medium: var(--radius-md); - --md-sys-shape-corner-large: var(--radius-lg); - --md-sys-shape-corner-extra-large: var(--radius-xl); - --md-sys-shape-corner-full: var(--radius-full); - - /* MD3 间距 */ - --md-sys-spacing-padding-xs: var(--spacing-1); - --md-sys-spacing-padding-sm: var(--spacing-2); - --md-sys-spacing-padding-md: var(--spacing-3); - --md-sys-spacing-padding-lg: var(--spacing-4); - --md-sys-spacing-padding-xl: var(--spacing-6); - --md-sys-spacing-padding-xxl: var(--spacing-10); - - --md-sys-spacing-margin-xs: var(--margin-1); - --md-sys-spacing-margin-sm: var(--margin-2); - --md-sys-spacing-margin-md: var(--margin-3); - --md-sys-spacing-margin-lg: var(--margin-4); - --md-sys-spacing-margin-xl: var(--margin-6); - --md-sys-spacing-margin-xxl: var(--margin-8); - - /* MD3 排版 */ - --md-sys-typescale-display-large-size: 3.5rem; - --md-sys-typescale-display-medium-size: 2.75rem; - --md-sys-typescale-display-small-size: 2.25rem; - - --md-sys-typescale-headline-large-size: var(--font-size-4xl); - --md-sys-typescale-headline-medium-size: var(--font-size-3xl); - --md-sys-typescale-headline-small-size: var(--font-size-2xl); - - --md-sys-typescale-title-large-size: var(--font-size-xl); - --md-sys-typescale-title-medium-size: var(--font-size-lg); - --md-sys-typescale-title-small-size: var(--font-size-base); - - --md-sys-typescale-body-large-size: var(--font-size-base); - --md-sys-typescale-body-medium-size: var(--font-size-sm); - --md-sys-typescale-body-small-size: var(--font-size-xs); - - --md-sys-typescale-label-large-size: var(--font-size-sm); - --md-sys-typescale-label-medium-size: var(--font-size-xs); - --md-sys-typescale-label-small-size: 0.6875rem; - - --md-sys-typescale-weight-normal: var(--font-weight-normal); - --md-sys-typescale-weight-medium: var(--font-weight-medium); - --md-sys-typescale-weight-bold: var(--font-weight-bold); - - --md-sys-typescale-line-height-condensed: 1.25; - --md-sys-typescale-line-height-default: 1.5; - --md-sys-typescale-line-height-medium: var(--line-height-normal); - - /* MD3 动画 */ - --md-sys-motion-duration-short1: 100ms; - --md-sys-motion-duration-short2: var(--duration-fast); - --md-sys-motion-duration-short3: 200ms; - --md-sys-motion-duration-short4: 250ms; - --md-sys-motion-duration-medium1: var(--duration-normal); - --md-sys-motion-duration-medium2: 300ms; - --md-sys-motion-duration-medium3: 350ms; - --md-sys-motion-duration-medium4: 400ms; - --md-sys-motion-duration-long1: 450ms; - --md-sys-motion-duration-long2: 500ms; - --md-sys-motion-easing-standard: var(--ease-default); - --md-sys-motion-easing-emphasized: var(--ease-bounce); -} - -/* MD3 深色模式变量 */ -[data-theme="dark"] { - --md-sys-color-primary: var(--primary-color); - --md-sys-color-on-primary: #ffffff; - --md-sys-color-primary-container: rgba(var(--primary-color-rgb), 0.15); - --md-sys-color-on-primary-container: var(--primary-color-light); - - --md-sys-color-secondary: var(--secondary-color); - --md-sys-color-on-secondary: #0f172a; - --md-sys-color-secondary-container: rgba(var(--secondary-color-rgb), 0.15); - --md-sys-color-on-secondary-container: var(--secondary-color-light); - - --md-sys-color-surface: var(--surface-color); - --md-sys-color-on-surface: var(--text-primary); - --md-sys-color-surface-variant: var(--bg-tertiary); - --md-sys-color-on-surface-variant: var(--text-secondary); - - --md-sys-color-outline: var(--border-color); - --md-sys-color-outline-variant: rgba(255, 255, 255, 0.12); - - --md-sys-elevation-level1: 0 1px 2px 0 rgba(0, 0, 0, 0.3); - --md-sys-elevation-level2: 0 4px 6px -1px rgba(0, 0, 0, 0.4); - --md-sys-elevation-level3: 0 10px 15px -3px rgba(0, 0, 0, 0.5); - --md-sys-elevation-level4: 0 20px 25px -5px rgba(0, 0, 0, 0.6); - - --md-sys-color-warning-container: rgba(var(--warning-color-rgb), 0.2); - --md-sys-color-on-warning-container: #fcd34d; - - /* MD3 信息色 - 深色模式 */ - --md-sys-color-info-container: rgba(var(--info-color-rgb), 0.2); - --md-sys-color-on-info-container: #93c5fd; - - /* MD3 成功色 - 深色模式 */ - --md-sys-color-success: var(--success-color); - --md-sys-color-success-container: rgba(var(--success-color-rgb), 0.2); - --md-sys-color-on-success-container: #34d399; - - /* MD3 错误色 - 深色模式 */ - --md-sys-color-error-container: rgba(var(--danger-color-rgb), 0.2); - --md-sys-color-on-error-container: #f87171; -} - -/* ==================== 组件深色模式适配 ==================== */ - -/* list-group-item 深色模式 */ -[data-theme="dark"] .list-group-item { - background-color: var(--bg-secondary); - color: var(--text-primary); - border-color: var(--border-color); -} - -[data-theme="dark"] .list-group-item.list-group-item-warning { - background-color: var(--md-sys-color-warning-container); - color: var(--md-sys-color-on-warning-container); - border-color: rgba(var(--warning-color-rgb), 0.3); -} - -[data-theme="dark"] .list-group-item-warning .text-muted { - color: var(--text-tertiary) !important; -} diff --git a/static/img/bgimg.jpg b/static/img/bgimg.jpg deleted file mode 100644 index a2da1c6e2dfb50aee95b9d2bf0f71bef735c0ae9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 409827 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%0AVa)kYo}Z{;tBESN5i||Hn@qyqEfL{p2}e=9yRRt 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�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

    _7Dn@g|J3NT#b>r#~6Ys4J9 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

    n%I~OOh=!HU9T&h!8jY&ql*sz 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

    ~ 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

    nW^xEa%fEs>m>JH%fLlb$VXOnRO;&%S#| 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 z zOiqH3|7T0MX0;+$;Mx}-QTMb_=}}u<1pzuata^hM^6*znW!1G_s?%Vj9F!~Wn$AM* zr4EH3dhkW>&NT1OH?FQ9;dCe=pf@1!anEz5n^lV=@rsv;mu;)wy?N|IbB&xq;M)r+ zQzI-xSv48D;a!nz*By?P^*ScGAM|G`7H*nSeA^09x{!vtrFu`x{Ri3I(hjb>6?m+S zToDUkMltUarB;s3u#_gkr4vUlcc>bkusq`hb5-y&FvS-;1R5e%k<1>4B=~+`W>Y~{ zCd%?0oK_Mmf#x(Xl{^H@QgFa0jbjd*e%26*wfJ>fv>(+h+%t3xDLcX7!@tuut{*ZV z1mes;tIOxr@PzfLQN_^VD4xwk+JC-Js6(rkEgr?Ji58Sg&0dxGK-a5`!f)!vv^a&z zEGeJ%-ui7O!fI>+1Hj`~8TR%s7*7n1H3sPP-fif~edV;}*HfkxcAes3iSs(%`L5PurMdai}r9S(EeuI(AD$OEa z*v}H;1zf_vq7cf?KaNDM5*_ljKL|||$S|^8>Jf;z@~w0tjF7GXSOMm~^==Ek!9{G! z;r8-8fK2(5Uee6){Aja%m{)&HoBsWktjOT4i$|9d!6Rq5)@L94oW+`L4;TDJfgUMD2d?^1FX_FTrgQD|_$Q|Gw6E(t z#w|-}j5xmpu-iKHL?S6KX|(t9xf~n2W_iFiyMb%xI5<4F3;uH%RB4I4SxsT0El-3?zz#!~KWd`f(7rq|phl9?n&g<1IKPY^7lLYkm4*GcDUs1*J za1qn`8_9dK6di$zk`+r^O~FFmY$aD-2~sBDkvYfQ;PRd;1^x`CXsIQ?TU-~2b1Pg% zezGnO#oAj%Gh8n5k5l$rskR)rXKNWaT7$zo2eQ-Py>5O&2f$pz@|Gu$umf1ohvwdk!AJ@xB6rtToC3MgIyA47Qv~Z##Zu{%ciFUgx`mY0a zP4|-yREA@J)yssD| zw#u~+PO9zXdCm5oy>vxE>1*|(Rdf1L()1z2s=XF=v$7rD45Da-_PB#X%e$%({Xz+CZ?Q?SOLZ8AJ$moV7s1c*1?Pv4jEn>sGj>fCCI;^!a;!%)QP{WlNcKAd7T*|?ep5n# zPiNx>ZB98){`|k69wAQb?LKLD6h7IUHC|91vAn=X>a=qdJp@)oA}1b2+{r(efyd0y zij4Evj%ax^v6ye>l+|tBab%<~dtXcUcF2E<`@&hkGJIV@`?ESpx!MI>sn>*&?WwL#838E1lvNG9GBWAIa0>sUpRN*VheYEx6jcl^BPrIBRaA` zT6+eyx3`_GY=DC`?TC|e0Jh_k0AaO1*_DHjmhCHdKb<2z-EL5v=1@GQ0^sXkZ^cNl z&zW>hk6gh@b2sglbrWHZn@1R9<~-LEbaW-7PJS$xlP-f$W0vTpLdI%3IHgL{`pz&n zb_rbH+&xz}MUBiW#OH^;6!{47UifIKVH^C$21)s;(1CZ5ncnARqWvl2HONYc1|XqH zY9WURm+nfoat;!nEP6qL@n)uvyZ#~DvVj>qhHw^zjVlJu4f>$w5?S*eg^8Y#&T*8xOG$hMr#`uohFiwTcY? znrX*ZWnnO{`#X}dh9e#Qb9n}ZK6$$|C;e^3G_LuCmhC3(j#G^cHKrPzgS&a>Fo&a z^JUh~MtSH{UyMJhN{%6Tv*`yz4bKW3y)8zDYq0I=-(QAcGQy~m-UN^P9YmqZbAQWWA-&n|5)#2$d77aL90lI>w|E`3@nnb0A}%gz z=J}l^$sG{F70I;+?_!0gzsPWyJ|kTI7?#@vN03$h?e$DA$nyr!KFsj$3TXG>ai)6s zWE;?FC#rgo{6cG6ZIm%OcpXxvT54m(2`_xc_?#G6=9M(9WrERN9t$OPlo?C00rqV8vD4V_`oFlGd?J6Hpy|N|e zc|X~@z-;vE*C)t}z0MU(1#s`HYu0uxOI+i83XC*bo!jF&$t^9evf70{d?pws8E{uZ(}oTzzUK zGtU#U%{-s%hKZB~A?$FdP}5xC7)+}8HlTTo1eEcxcxP-b01MI=w5z!J1uxW0xm0K! zd^cJ7_T+D`wno9f6|@ZK8)|)TkrnOv8$@NY!YWSwD#nw4q;M z4HSwyA2%kvCW=tH^F!7tMC&a^jVub)>eVe(T7S{|_tT#0#ccJW!{=DHu56L3=cI($+SX}RNdQ|8 z55>W9twiKkvvKWwJ|PdNgV3d}&rX?~Ir}?a9sS>RT1Z^TkRlV0kDOr(t=h^joj9R(5UftL0bmSe7abfTBt-!m(z%2+x=d{TYLUo*2o`?T| zpM1{SNN>z+0Gw*+lP}EWRF?If?L&)F_%L=2-HRsk$t{^A$Fgz-yu9ip_94|~mlTqn z1q>A$63)F|xjVmuG>F$>)zZyC8V{1dAO2)TlYU67r{gwl6jdmpIs={_#i_ur)+lY3 z{<`PouCrDpg72+7)BP9?MH96#=Xfx<1Ft`g_&Gf#hR`G1e`cej(j`Ghu`t(4OGK4P z!j8007`AtEf{oNpd-=Jcd6MpHFcC@pEiaR2^~>ldjF&28O^v%@+@IUB4EufVvaOsv z;5X;c?94!ovK?&Ki%SPfS}nB<%T-1@8MBzc#8(;}Z8o1OSlmb9D43VbEI~0i^gyi& zLTr>ZA*d>Z!LL8ldF+~L`CCYldVB7DW_4+!OZ3r&gXq#vK~}--i1y*W_U)&33{b|@ zZjVzTKV^tVwuY&huRzalTB(y!RaBJM5~)hdIWn8#oMWUx(Eq)mpg=4C`b^Fi`Oi2iue9}*uLA`m|0Dcr;eaR|WHa7d|LC@BIGufjOaWy=puR7psM{EYM|&YU zL}1|Jl4Ys>q_?}KJwm+iLu1>KAn$L`zxn|yXH6vg@NJu`wIN(eHPQ0dr?OXNzo3}v z5LAng8rL&YS@$ad|F{@T;nBeBtmljL8r!8+ftFX)F~?%*BQg)iT*EhJ;+NXmzYjB& z(pbhBi?+A`-XS#skD8q)eXrfn`5dK>g&==^!c~t4a`@5ZrQle zbZ3wfOA(s|b02HswJMd%vc?Hbw&2DwHlmVczra{~6#RWD6@AljQN9xH!nb^(dwh|7 z-0~Ov4h+;^n~p&w)r1%Wu%0R*t!p4ejjqxWP=ThSmI3LFxzr zv;BpI8b4LTT<0(JA+W%{4w#&OK!VV)KWA|TF9rymdLAch0v#h=fEhdb!kw3^mq0*& z$qCD2C{e&5w||lM+{X)w>i)I@ZXuR3aY#Ihv2reOaHsl?t4@`G4`9eO$f(@D)W*N= zW*qvXw3iOASfv0@RM}obyHgc6#}xMk3v0i^DpFqc5c0d49G0fQ8&41bc2ya!F9!5g z_Cj+P0rpiZsH#MVO;o^YoCz{ZEjS`9m_9Q|_YC5wit|uv6cV2hGmc|tFuGR~RXeE3 zptM1Juomm7w2&zRY7~rGgigo#yHfSIGU`SfNfB*-IcRGXvMNUJi0tORu~Q0gerxe| zoAAt)ov74z-8cJ6zVH*d_Al3+wcB#-V8?`YxMUR4@w_2a?fcShZ~Xq+`DWF!*f0G= zE2C;1^J?<1G6`6F(qA%R{8h&NUmr478$L-6Dd;X=ojroFubw__JgGZ@9E{1oKCHOk z;|~VM-`q{kUhp)AfE@3 zb-19+2r8>Z49ZbjoV(^QVy7fJol)-%trlgnW*XgRogFQ67bVe^0s>FnbaC$sKJyd@ z)=F<9;2ECr{RBzp0!{e$TNA3#mB(Lw`#WR1m%t6x^gySYde-y6OCPbPtWKvf zcA;ih$;dC&(``?15E%Z26lvp9-8O`DREA32xk1UJPUSJG^OQ-5G}CSjg+Mk`VMmg~ z+(0K*wy(dRoIunyEP~g!^L9t-ei--&^a_Ls!o_U6F2=o9Op0)W-!n#=9z4azi48PS z1LK|++6tJ8S(o_)F8~u!qpWK38ia2~WtWIJzjW!~$WbJvFL|FvL2kwl+0F=@x@5(0Z_SDUsNTnk%->XWVd9l2 zb&?8}UWfKtEe-PvnXWiQ8{6dqiq_Xf(5kP?t z(Z@~lL@a~*_L)*g8$L*{m-S1;`Ddoz9u?F);MU8azeQna7lhdneqjguk7 zZMpFh>06pvSrk_~ZT2&YPlwzFPY@`X7NOzG4F48_TdaEBx6~1jy@*g-!aZXtt*}J{ zXd*0lP{wLZ?sG+W3e>%mL_tY{-{m|L(*BThEAA3q`?E_72fNi`NZsKn2>+=$^pG$R zknptOi|h)$Qu*;BMidlw-Cn)HCidh&(Q(wEM872>IzE!w9bKUjMw=%D+rKlsQ(#*# zim{4-GH#B+bX2_jKoz9!H<-S6V59EpJ1G11k|X4rA*X9-TV?LPOIzUJbiD9bjBq%s z-w}wJH3d1uxb(Ik#QJytvVT{I=j#=at68E!M~V$8sH6Xcza!A?UdD!OL+UC81)KIp zwVcDHXW^sLqzRNtJ#X?MBMqsqt1x!m^y!pW-)iSbBbkO4*wyBN?>ul$3@D*)F&@ra z?`9W!`2#o7FKZN`;CikLM-Ugx6l+`(yLhA;2t2oO_1Krt+Zp%KrkMJWHxG0n?IuT*F+d;^jBW3>%Q=Jy1ME)@pBXY7+p8V zFJdo#Me6GM|0}Ps+MEgdV%Xz{AZTmnyDLbaY=ZGCe3UAg?@hRxPO0)(S1RESDODsC zU)|kOAmnWWB-4S>Eut_~BcJVnMPEu>3o8u{@q7jQHMFLvkzCvv$9=wR9F;kz=K^@_ zoa9;9RBZ95BKd(Ni#|(=+`vbd+w@~ASa-w?+tanv{kUf!Tvs(;2faMg%YPEKGSKP$ zL~Hpf8%1kna}%k@N?_a7HUqa@Eo>G=8pYkjw{dbe?k)WKq&B`^huloKUTI#-C!uam z(;I9~qVkXTo_HAlE3J0Uyy~ENuMK5cJ>%t%Iy&uq5F&}NMdUU=*510$1Y5q|d^0V2 zN0=b?KnQra*Y^XjfwqywZioUeCJzyXr%)~QILqJL;ujGnVv_E9y?k)@32~SHI#xj~ zYlFxl?B2yV#?dsMpgXYm9K;RIgyrZsQ5QZa$J!15NbZ>x4KcEc!f=sJhbY|VRZz-G z&ePwZ@PaWbSHArx!2=#lZd7!~SC7V9c%NK$E3$kU9F>Nxu_;m8gP$a$ z<1*>B)e|$Z><-j4Z5B}S8Il&i3V%r?*vaU>+tjLDU_TD!rEc|)mqNkwPDWvjehC%9 zp&Pu!RyQznD)j=|_!*OAA;r{m44{0^Z4r|H-YkA)zZU`o5Rh6gF5N7N;JjdTGM0m=9=@y-g&$x}1#~J}zGM^fdN3d4z@?KAr)?M)C9dtM-^p zU^O%|);cBoL7@zhKYR z*tl?JU#a>|v?IPQaSAfM5l%Qx?jGY&jP!@Uo8nxMcgYZdZ>j3QD?XAqDN&*Ke_Vh{ z_06kXmP7EZz$Ru}T|-=AVMf1Ayq64CL38q52Xg&~tmh%uSs6DbWE1D$ttjPaEt8g^|vXmwrWWyH&to1c=?2ywpDMmOB zsGEab+23e9A4YWWF}M`t+cGh!f|}0i?xbvpF47b?1)>zdAZD>WoUrFdTyGfcZ?qkYkktW4A`{1W=pQx`^Ah{@E4kzl;J^QJ~*I345{XO-IufcYn? z%g|#m#IvA%4AYc}EHd9X3Um5vLbWo=5|{P2|HB&G+yY*44gWEHjq3X*xTY`P%E+Lu ztct=RbGC_0$5tlCx*?7C5@kQT3G^ug@LlRf4T5SOC2vExus4fdihRwVB=6}oE4Gud zu=MMPr|HcsF@>)Np1Aa>ifSWLk$5U{MZJhjQOj~_21B1gt5s;d!`49gCV zuY!2$Icd21z~>OC$Wu3jVrMr!4f^nqa5Ht)J+2SDdCf_vR-Gd`7kPT2j1)qZ@D$g7-sGx6G6qt>9{dsJG^zDHMgw6dyPp43J@9gJg2@frT-aZX3 zCd$rn(@T6)q=6Jf@hta_jX~NG&#NBY)mN;p0qlULQz=?HyU8B!A~iIZBCV8Co&~p$ zm4^=x8q_Gt7$wcca~<4d`}bBk?|PC=5vTBxS=q)z7~U^|CtszrRkC#DJ>2b5PvOoZ z1PSsS&NjCyn2;!}Arfic$^-2k*a*s`<+))am{X#hv6$hd31wJ~J?{rlcShe?WNR#C z2)@~L0vpn79lFLv){^;5@*HneQ;Th&shx`IOc)F<>>W*o`i-io z9>{iSzo(QSm#QEc+-c$|8+O3Ij@FXX*dW(5=EhBXhf>HoaJK=mF^bZ5&;MFEm0iR4 zg~DRdJ9~{xWtpT?mnGbJYkRP9tX%=O0(+tL=UY+k(G`mtb9O??U)k~UZ~%SY6($vn zIUSI4T=W6=1B%WtRI`nIZEY=f*bGixlu+cvI8j?FLm3rFMrp8l+^|hK4gkGwv8ww6 zlGO7T3n1Y@Y*ojiVl7q5sTl1AVt#o#@;LgupPxB{N3!d$ZOa!_|83?KG!vCDQP2k-8V@Ur33EMZWpr9j(!=rg14yB{^3pe+?4+sxm+*k#<$x zEk~Td-?R!IOMBmWHUUqscV3eVf(7_OC8-8S7`4y43YVZMnuFGzZ@>71jyya@ryijJ zVuvbMZq9tTB0|jXTJjZbmv0`GdX7#qpLz0xLkNE_vW^b`YiMm1(Xe=<8k6TVf+qQW z-jbQSw%5hN8+53MBH-L1Qm%e!DBzE1z=15GE?ie; ziRWX1^CYC=qoLd<$*m|kz}7gxJ8C-Mp4w_D2xX}Sm*Gu9I&Z(Wr?FLs!?1UUj$>`Y z)oOG{0Mjp9#&t$g`{xyT(ooP2`C=)?5V73d(RE4t@&S!qFZVDfT2>SqI%A}cBAK{B zWUBODAQV~UQcYenT#%^CHsV!wTg2~_llxtU>uW`w>pjozVb8#Gnt+QeWgLW-dI=G7 z+*kadtidLYk0_iCUF{x|Ez|bd2}t9Z-0&^vJoQ^joJoPGUbBpAUOPo-F^Xi=exS1> zP*>Oc-WlLDoDVp-f5l~-%B;s_PKOtyC0B9EtHrZ}!tY1K=qplDor%Rg+!04q!M!jS zgGKzL3t9WICfjoKI!@mfL|-`D`|}DphKa@2*k~(%O^E0&Tw$Ra`{d7nHUwB&p;Urf zGG~*5w4cY~pYrNQYDLAL^v9kfk|BwC?C2L%+)Lipu!U-43UzI5n)A$cCGHYm4-aY{ zfuTL}$$Ft-hfhakB$$6khZpHQO@N?Gti+AQ?p`W(+7 zN%;SFC1iGI>XA62k4Yz;ZF{h0zXltl4;FRk#i zz4BV%Ly%IRRpL6DTw-KJYT?xhtrlnUu+q@d%;_B^&qII&&q~Sp3}&u|um*3>0p{rTf#A?&M1FMlUr| zBab$;*F;`s>o#v#%3?I3$+R6V`R;hUu;u3(*D*BVNeWu?_;i$w@rdT z40YC^-b^wyIk;``Adl#2e-4bkTN%_ER0nxgAJ9YF{@+cjga9VhoT+OjKWujwc z_2<`g1O!%g9#h^Mcs;&M2Xv3PQus!{!K~zB>0Q7UQ%*Hv@*jF{JvGln!#~j0Y^y3I zK4Kn>X81h>np|m>iE>2ScXK;7VrjmI)xG%-5# z`SpzyDG3nZU5Q(ErV-qwbE~$TGN{dM=neChn>WKk)n@TpEGy{4c0I42SliHU6?X-8Adtzj_V0HT*Rei%nT7EJ?kg= zUAdY#m^^|0p7kC_P4jujBdb;^!hvt+ z`7!98%R+D;+dzQ`5$Z#kTNW=UIe_~oiFRu~$uOvln;P{FrA&A88~ho`% zfYUULwbPD{j$Q=<<-G-_m|BhvODdG;Q&vDQN%Re1SW4Yo?}OQtkm;MdCUDoJoj6T6 zB?rxCK8KMwY6m9^ag-qBV}U`qvDf6-$0$Wj19k3&-M_?q-%X4Ry=u%1v1BiP*qrl2 zTA|%{UgxuNL^^$qWAM6dPW>4RBNZ11BDNl5_*P6vL`jau#pE@+DGDqQS%81}*9MT! zjGvUe)sHiSyEi}8a^cm_3R|r_&4L=b1#XVa_zb0H;^KpuetfzEywYE2hyo|5!3>BI z=`+%)`WDBP(T%(NM0WsX&E_bQwhv zOn+KcU`aHLTJjBzu|!4L2wL&?6Ro7|`vL$BI;`m1wBOCs*Zxb_)$4SMteqsy)$xi_ zM+D$%JX1UXfeUj;xdy##1fKJ;0bn}tvEe%H?IZB%5^{es>?D3Dx_rtfnA*0Z$n`Ql zyQ{ly1-b(B@a4u}GjMI;$i|Q}UwVRtVwZeK({RFl9v=sT$U zuaCeV{R*MAHDAB>m+OXEf_CLGau#?{5q{Mno&`%AdbBuM+(zLQf8!%fuW|2T#ppt59A(YEZO#! zyro~@tS;C+cIOqpi?xdM5w@%SZDDGrjy5ew_s?D#I8d(ZA;+Q7+tXYgb#f9YoRcii zuii-?8an=QN*TMbo@M{Sr?R?o=J4VVe!ln71tA_?|L<=zdx$&*sGPW}SlF`rCOn}s z!wiea5QtLg+^ck*g|DWB7X_C`qhjm&7MseM4>hN>r+krAetToC2Ay6>1&om#)CEP^ zYM}*;iLY-I!dNIO3dF855f-WxH;mq0zilBQWzFCCo$#iHVC(W~z3KlW>MekxdcQwlMMOZPyQNE7xM|#6p{hmZwH7NZ z9yi%dp?%2dSM#y|a|{JmfPTQkztmL4`aWL2>_DZw3oU<=?UDu464AR=-n92zsmWmy zSCRnX_}df}69W1smEzxO|IXrVHIo`Fn>{XM>O=jP}=j z(Q>>7NTuwE<|5~y5`ODJnpH21sZu}TME*>*Ejp$4Y+N6DRl^0#iSwsP0N@;q?Lbct z(F#OB38M4ckJStDf6KC86~Qv;Qq2Nh(w;k}f+<5e2F=BF=!-wm_M)#1Y)vTq){7%W zaze+$cZJ?LQ#S|ph+xt^E*|sxm`f`e9+hnR*It?1_4L4aw)4&wwMLX@$t$5)LQsr_ zT6nSlDY;;Yn=d@4bV~DSf63!#O)dojLEYGwMDpJ)O1+<5;eEZu>!Sb470yc}U$&pQ z`*|2Z-6}Y&V%smzntWg#8UQ0hW5owyR^{w`in`!~yO`Pgi}TNk$mY;^hQ6a|ex!(J z@eSWkJNF}Dr<{z78+|Vj)HQpYh+#Yxu3VyTo_QOEr9)xipHK)lel9nwp-Dn=l}&p* z(>LIA`PUb9_#C!FmRp#;Hn0{p#e+SeELk4yYJ`P^hVHeT>}14aPOa1)eO2SWK*f_f9XhM zPy979Zp2yf=+EyI8a%U4vTZ}5IrbV}9eGZYj)p~T8Ac01)b({d?$(lyAA@ELqBLv! zNhJNad83DmGm~A3J4iHt%KF^f16msOY!n(aj1dE0H@m{F+QnBV=JS4;vk?K91FE5ZmOhs17!7cnWdV+EY zOE+SZQiY`QiB?6)&S$yZ)D+fP-{TO3i1!ioRrWhrpBkRP0iKbWnWe35#BVMNV&)F> zGqD^^ERLw@k_kc$^AeY*gde|UjpmV!rJ2)z*}QXKtR~x}8zYln;2Gyrr{w!GOftr_ za~b;iEwLE0X7$OMB&*Rrt00g#d$vt?zcBhPV@v*AxuwK4xDr1jn2j^+kB3$+tVQkq z`Si|?ppu~d)Mp$q<~6qEnxV54=bQLBS&uk)RKaUS(oE`xrQ6BnPy{H+XsEEw`-I%t zJN`OIwDno^OOaKJX5`IX7l4Cmma3QP>8joxk6rofFIV1ekaex)1)XrN1R!Xt5}H)@ zbg+MuemJ#RmG}5?Miaxrolk39Q>pQv{~_;>`#-R>>4hk)Q8$?vq4nX(YP^*QQ~#XE zn7cD)rVz>F_N%s3U$~bYiG@cIN?jGWo{*4`50HdiZ*r#ltk1JH!$&E5I!i6=Zo4P( zv|Cx+L?CO5n2%#zE8U;lc~)sKv3sSSA-iGSLcXC05H53s-<>wiU) z<&6Ib%CG-%!Lo5Jiz&o^!Ecgu-pK zMh?dFSmy557qVf;;A_Xx<1fWOvI3^1q$&^Te3P4evKnTe7f9OmyE_j#P(3!=(By7v zcrhVDI~u_0PiX6*TJ{Dz{ba@CBb1n@^BT?K=73o<=TkM!d(Y+=>*4y`jX~hG)aCTr zFTM!S$NW zJ6r{&CI6UGI#4WG zZp+{Rx6}|L-pcsyH1C11XBr0cIpvr(!)R(GT21Zb0Ia2GxumkW@i>u_EDDQDMB{6_ z^;fxQV=@vw(d#R>?pt}z*2m($(q@y)!39EA8firt^eaISzWn#WQ1mDKEh}>RCk4F) zi5`sKK9Z|VZ6@Csg9X_Oz_pF7{g`ZOY`ed$Xq;`M?4ycW?iIH`)_Nw)c{Q&sS0&g( zK+fqrouP)f&WPlM6UDxn_O6pNWA625u4C{)*@Kc_{{3fFzIFz$+t0Y~pjxYI;(+qi zDgxzrcShB<9oMv~_+w;$T4-F$8<$DUQ8~TKO?X@DAy)uG0YLur)8N?8p$?3Hv?)~$ zL5c}iHa*sGeChM;SGpxq8;3kCUQNZPmzH%#6ak{P!?rEEhpVqFgedmEuLga+1ik#4 zx}0u`lp{5j`Z~Vq{!Heqa5q@E^3!>D6{4sUL{^($b3S!Y@u_3n;;ZJW)xh8+?dr~z zA?m!&t_A2Yzv{ff@JVbSp_a@KJ~61g%a(QAtFWu)btN5W7p*@>!oRikkcpppch=^8 zbVzD?G`KN&Fhu-ee=XsWe#Esp`|IRY*7z0qJ&m_%w2ZVydB-hUpf5Z@CwdZ>$4_q+ z^FJqrL{4!==$UFJ3WNx@ttk={{`9Ef-z(gANpe49_!cNd>=#QDX*S)eWINe@4$%Zl z-8xm#L67XrC!6K%@&35O`~}nBJ^nDuLY*XF>s@JQWEN82dHP3-= zjL%>$9#KA?e#vi&{~YnU(|vrJ2WXNd~ct zJ%`)Bco^u_CYhsWtvk0ypF!hs zT9*#1mQb&_7#j#@Krpn)=DyIw{Vf81-@VV%x;hS)&sut2hrDcG_3T7TDQpSKv15Yx z5OPCg`ZzOP@G5gT zTv|t}hkIsZSFnX!@A0THP;n`>*C@8cXSkR*--7RZ=gq=pKKQtIP^?h)UP|F zWZNGcraUbWt$m3o&OQ>hYPYRT%~LP?+ZAVjJ#mC8|I^S9 z3f6@c1Ne-_jUj{Y=0Y7^es{kPy4R~dT@BomJ+h$SI}b4@HGPFnf-NEYq-rc{@W zpf-P~2M7+zRagpnGx~|k=1V-_`po*`kg%hx=owP-vQ|3!isq`FU?KesdtY2a9kT9! zOmvw(HJBV&B+rs{u(ch`?N5H-{F=i9W6NxEe9;irgosGhae;(iClLsqlRB6tpb-uN zR1EgJ)*9$QDJKg=u|}l_7rRc`A?qq7sWuSCNRmCaX8gpTAF?OX6{IoE9rs$d-`z7! z7KdXzThJ_<0U;Rd~TOEbY)W(H2cr6%E2BoVANc4i)r(OfH{XA?L2H z$L@RY&hhH-0~O!^$3D&CBIy_iE#Ztkz%(XJx96EbuOF<3L|h1%${E#KO+ zNq}t&XXs-vv>~J&oN`q1m{Fwpx%p4Ig^@=yad^)6p-{$=?Jl01fbo%8U#aaIkLPFO zq!NF~kU_gpc>2XESB6Z}+O5fNfAi*m`8z~AJ;-_8+I{{`Qql+UHK?v0>8gGkvX~YD zZ30xND3!*Nk?>@!CBJjr@Q8zkNoS9QU;j&Eons2$^OExpy%wX8S6!nImoeuzaua`P zhE>tRfm>-0uDJkH#VTcmYZtI5^;;W2AikSRfp={@9m7Nk62puMrth65!iv#{MBFw_ zg|Rw4_E#t*cEK>E!CJg;00{ZX`0}Rqz-HlQuEv92@dF3db*$b4(>>e&oMF7{Tm`jZ z0vfnSq_1uQgb4eM&wylA6H&hE>zs`M#dUulxXRuxQ}{!OBc5{e@zWVl=o$XqOBV8* zD{IezLBuVbzBe^9o)z6am%pli@I#hS0zS=IZ;OYi--bk9^Er-IZd-zExjG9`%v4=x zC+;-M&|Xbgl{aVI+59eZ^tA#WV?^wij7NL(`!jy5K~c6ovRI)-+vwb^}m3u z2--}x)I9yYDf-8SlF++!kI?Uj_pgOFC~VoYxt>Yz`Gj^aVBnegBnqztrQajF?h>1t zs27X)QpPl*a99fq?az0iIIKUXcmP$}Lg0m2}lA#|zO$kVhlAVaIuCrPXQ znmRo1+#YMD~EJ$R^vr#5dJPVUu8_f_nk1TD5 z-^rWa%)o9&T!Ip+1CdmB*RZaB6@eBPM-QUgCsC?9Was{YEDqREryZaKr5tln(D)x= z7YFJER5E~v2<)}>pgAUz_DMB;v|E+syD*X7r{AMr3gR;9C@~uP4U%L@Gq@Yh8_}%c z>%8R*k7csGMX~61BuqOFehAlG*&3}`OG}sPhXO|qP zQdEaBhWXGp;r{Jc+4*L`VzLKo`eW?-?9Cq>sF39(k|#2|tp4COUH{vw2H)IU;|Mv$ z9Kx^aZzJ(sUFc}vb)`SQ3W~Sq$=-75ypr#{X?J;bm{vBF0a56Qp~ruX460&+O;$ z2btVsdtW6T+p`&_#BbWBR!%8 zy2QKNW_5Yl5n~+K`@6ZE{4Z+Cu>V&Hk^Bx=&QPCHe-OL#izxy0yFaZ+PbH7pc1D8a zkn0+|$*=Gm&(|oLdB?&3#F0-3l0Ag@!V{v92ad1+#NNBbn8po0k*Dp+gPZ8KuGz+( zKc&5-@g!X{4|l(x!RWTCyUy_LwuC!}vfv$Y*BX9(fuG{4e_wR=ynsxsa5O&>DfF-* zD{EKkAavK8U6|g~DQk54#94CK`>ahOx&K*dOYLOCuTRcE7#%RP#FU^Vpc1T2zqjau zk$hznUEJ!NcjmiotrPJkP3b!cBE#v2@6k8|@4&3h2a~c+ha$s(m&yjG^(-Tpd5jFN z%Ui+kAMGya*HgM(qNJ+LgEFfF0CKnc4Fx#wpmVdGbZ_@CI3TXwT`A3j&2 zn|IyL-QtDi5}2zqXe{HyQpe|z^WR*Sy!3rBszfs+eEs66Bkg}wG|nDaSr-?M^erm4 z%F!P7fPJKMpG5Eked8p9mwp#YDX*yL=R6N(^*L5HQEc@6dw?$N^(-%2+~}><A3yeH)q@}=G%q)XIR!nC*d*4s??=g&w}6rT4x7o zrM^3VdvH20JyCnlq{C0Xm!BIS=9I8u_j@uq7E)JnK}4+il!6{Fo-Okp8C+t&JV3Uw zW!fy0GqV4f55K($0tr&;KuCUQwp``<12EQE`}sjiz(K(?pN$Y10!y(t)1neO>M(OW zCcS~oiRjvfyUS3_%e!0pQP#t+m{HWL0TTaJaJN5S4hAt`()9}SpMCZ68kOpt)4Y2R z@4Y>0f{o{`Zw{r1T;Xz~>l!E%xHq0G6HMm_Brg9Y&Y_nZUZZCC+ux6oAaRPxs&!dj z!9L_0n*JnEUFi_?AO#WImXy()bW(7ge>pOCu&;^UU7nTmd@PhmwoW@dlsb)=k&m5F zf-)b%>1w++oM!KHJ!Ic}H#Ks1;f>1ex?aJ%zM*m{=)-nKO1bNYy#277Sor6ui%_~H z`Ja<;#5kD=LeLFO?a#J;80#{by%ik3iDm6V9;F_keKNK))o#~0eQ+>dQgP|Xr_%09H}HTQ5q zMcudrYd_nu%>|P|o0I`Mu^zgBb^Xq}7Oj)Dn`veJ>9c5hjpG4(%uLM5Clq=75u9SL z(cf$0$ck+pJO#^T{RVUfYk2}0q{6&N>;(^*BFc567{$NaL|H5GvH4m(>d}%MID>(Y z>F!gGe?d^u5YUZBiQC_`b9rL1_o@lS1jV21E#7|a$eWIxX2)-(Onrbdlt!P?f<{Mo z+V`R6&cvt`Y!wM(>WcU!gHauQQ{8X{S%&7=;uvBgso88gB%^P8^wnP|UM}n(r`Flj z8JVcJT^z|X{+ybP@Ir00m=6&OebccNgs3m*WgT4aoR_EzUdd(G>GM7GTEv=+2I)Jx zZ27r-W0G19O$ijy!MHUK=@r!YL*f+pw~=NFlhH<|cn2Fo3&oD`7IRAb^>H{@dE9Zb7v%h#AUw5QWXTb{Bf_pG$T#`Ye^!fu=75_2KjW_iEklvSC12Ot0?a zQjAX`*IMdN)dU4sRd*~oYjOMP&1~bO%t$z$>n`fdR7!en%6Qx&5TOJ>@D{)Xo2#;6 zfO$)D=#u!XRK@_%^Ty2Q9fX#IXyQ|@8|e$zwWZB!I-V&evRJXy-6hFOe}33NsVJ|L zx}%1%B3EF1xbov{Je%}CBpOUy=w%FVO``ZYY0;e?I}Q0fbrN7|Ez5H3O0geDLKhy} zB*ykr>Jpuu6_lK3(7$HaQf`fK?gVqD=WZw5UwdCq*_&_8P+sFC6DEJutVWkeQZJ5w z-2#rfO6{J;3lw}800;v^d+-CMyV$Up01B=)vxA~KQM->O4#uZ}QJ>6aSF1lg7Ji|1 zRC?f_Gk19w_b-hHHp^0Vb^nD7>Gxe3vPH6%Z)31=geC@O9<EF1UrBo1@hY!&D$5scd69t@jo39ttan{X*1$8CtpE_B;@z9f<^)FzH$$^bL)An6S zfL7r+jcfHZi$(^5mX5qv|H2@ledYm`wv5X^2e$2(nXpN2ZSx*|QnGavm&5$@>D+fc zLh1U-@jf^09@cfrYJn7kcXcUpRqNp(qCAQ#LUWmy={S^@Kt}hr%|)|kVgc2RFw;5@JCz`D~6PO^l}n}sCr8t zbb`Nq{I5R*fP!-pMnxWwf~#wi^*3}SQEMFoPa|9V;r{|{J$c_dnhInZWQ%%aO5gSH zoctZhgLd8hYr4K|vVb;NbPkop^g2S`@4f$CfFAQ$L@BnmVE~;~{BKp0EH3p?alj!j;Ows6)Y0vF@+r}sA5_D#Y(k}?q`QoTPRJF z(|ybZ4tGFwkMj-;Zy%0+8OyozBy5VPoj00M^%;X$rPWc#z`6sWA#VIV&DCAkB2C+U zTu*ORb(rx2=(!Dl1qDGtBAVi)X^{%hGIlI)AV=Rey3G{&TmpGME#Zpdo4U7A*fTt-Ns&Z5}_QN$@5>B`!6%SYA3b0vb8}OMSj~4fQw|bRBG

    $JpF&Vk8RG8H*R*^g}RxNBj3UU=TI#9N%Cv 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

    3JX_yFC%`S&XToY#K>Rn7a4=YcGsHMl_)9Gu3K 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 z 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

    #gPI*@M;h2=87$b(<2 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 vL9B*MIswFxSun^GvHm~h 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

    wTYW|y@U`JSFCVARmQ?~Y|cXWNl6x`iT{kMsq79 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∨j911cu=)LfJ7RWv>$q)k+rFL4ifyq`s?!b*&O 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 R+CgXb>(2-GKJgB@@;^S-WPbnv diff --git a/static/img/favicon.svg b/static/img/favicon.svg deleted file mode 100644 index 8764a1a..0000000 --- a/static/img/favicon.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/static/img/head-logo.png b/static/img/head-logo.png deleted file mode 100644 index 02cda23362ecdc1ffcda69275bdb1f0bec06405c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1113 zcmZQzV30{GsVqn=%S>Yc0uY^>nP!-qnF!=F&dNqz zNrh+xIn2Niq!A$qlz|D}%q%D>07`DoEJ)4=(!$C4IYoKNITb+Dc^Ozi`gjt{44oSp zctC8C%;JJn5CaG_pjetX-h^Eh3$YJ!DQNn>kfq|8gk>Rdk^2YZZ|GHNdsm_x5* z6s@>+q^onYDOa)XNqrUNyug!_qkaC@mj1lDDy>|{X66C|CJkGu)3cUDn6y7hPJb2u zFz6}A(bx;TWr9V2r4J;u)d{ppBsn~<{5avw-`;878{L<1=qd7is@cLVUEgZ(pJNfb z%(Bd#ON%GIIx<6->)palX>Hu?f{&DbK44j(`!(ap^j$n}rXN4+wj|J-zwvC^qv-|y z_Ivg)6ka@1HFY^3%)bjbjOH;*I4}d_mNAmyu1D>fGi#=XGq>OWlyGy~Wy_cGu1|D4 z_RQ`Tnx*jTYVu#LSl@q%Wr0qod=C`93w^Qha{kG>?_#BX#ifCY{>IZ!GbPUB*2}K+ zo96g`THhhw11{S6R#z7O)cP4ACVIraN`g!MX|~xd3Co+eZ?3I4Zz?7Kr84y4&t}bs z?Gt}9rG_U38%^1<>|DMpyXFt44X){?Z|rNs-HvVfVb?K#<=>|ByMHO3kuZ*&=OzV69ANs13n0;IAOxu@TXFUVI+V*JjCzv;xPcJ>5FRNXs z=6IoB?a{JVZ490%mnx#Rnr*-Cyr8go`6l&=dHkMTlDuxV7y9ZWAJk;}t-9;lvg(#? z*6#T~R2vFHPQN?X({6RcfkAlf5ffXU8SUv4mq)$V@imRt6pmN=Rm-O!wXm*m@y8kU p76G^Hn { - this.handleLogin(e); - }); - } - - // 注册表单 - const registerForm = document.getElementById('register-form'); - if (registerForm) { - registerForm.addEventListener('submit', (e) => { - this.handleRegister(e); - }); - } - - // 忘记密码表单 - const forgotPasswordForm = document.getElementById('forgot-password-form'); - if (forgotPasswordForm) { - forgotPasswordForm.addEventListener('submit', (e) => { - this.handleForgotPassword(e); - }); - } - - // 重置密码表单 - const resetPasswordForm = document.getElementById('reset-password-form'); - if (resetPasswordForm) { - resetPasswordForm.addEventListener('submit', (e) => { - this.handleResetPassword(e); - }); - } - - // 退出登录按钮 - const logoutBtn = document.getElementById('logout-btn'); - if (logoutBtn) { - logoutBtn.addEventListener('click', (e) => { - e.preventDefault(); - this.handleLogout(); - }); - } - }, - - /** - * 初始化表单 - */ - initForms() { - // 初始化表单验证 - this.initFormValidation(); - }, - - /** - * 初始化表单验证 - */ - initFormValidation() { - const forms = document.querySelectorAll('.needs-validation'); - forms.forEach(form => { - form.addEventListener('submit', event => { - if (!form.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); - } - form.classList.add('was-validated'); - }, false); - }); - }, - - /** - * 处理登录 - */ - async handleLogin(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - try { - Utils.showLoading(); - - const response = await fetch(form.action, { - method: 'POST', - headers: { - 'X-CSRFToken': Config.csrfToken, - }, - body: formData, - }); - - if (response.ok) { - // 登录成功,重定向到指定页面或首页 - const redirectUrl = this.getSafeRedirectUrl(); - window.location.href = redirectUrl; - } else { - const errorData = await response.json(); - Utils.showAlert(errorData.message || '登录失败,请检查用户名和密码', 'danger'); - } - } catch (error) { - console.error('Login error:', error); - Utils.showAlert('登录失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 获取安全的重定向地址(仅允许站内相对路径) - */ - getSafeRedirectUrl() { - const next = new URLSearchParams(window.location.search).get('next'); - if (!next) { - return '/'; - } - - // 拒绝协议相对URL(如 //evil.com) - if (next.startsWith('//')) { - return '/'; - } - - try { - const url = new URL(next, window.location.origin); - // 仅允许同源地址 - if (url.origin !== window.location.origin) { - return '/'; - } - // 仅允许以 / 开头的站内路径 - if (!url.pathname.startsWith('/')) { - return '/'; - } - return `${url.pathname}${url.search}${url.hash}`; - } catch (e) { - return '/'; - } - }, - - /** - * 处理注册 - */ - async handleRegister(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - // 验证密码 - if (data.password1 !== data.password2) { - Utils.showAlert('两次输入的密码不一致', 'danger'); - return; - } - - try { - Utils.showLoading(); - - const response = await API.post('/accounts/api/register/', data); - - if (response.status === 'success') { - Utils.showAlert('注册成功,请登录', 'success'); - setTimeout(() => { - window.location.href = '/accounts/login/'; - }, 1500); - } else { - Utils.showAlert(response.message || '注册失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Register error:', error); - Utils.showAlert('注册失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理忘记密码 - */ - async handleForgotPassword(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - try { - Utils.showLoading(); - - const response = await API.post('/accounts/api/forgot-password/', data); - - if (response.status === 'success') { - Utils.showAlert('重置密码链接已发送到您的邮箱', 'success'); - form.reset(); - } else { - Utils.showAlert(response.message || '发送失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Forgot password error:', error); - Utils.showAlert('发送失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理重置密码 - */ - async handleResetPassword(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - // 验证密码 - if (data.password !== data.confirm_password) { - Utils.showAlert('两次输入的密码不一致', 'danger'); - return; - } - - try { - Utils.showLoading(); - - const response = await API.post('/accounts/api/reset-password/', data); - - if (response.status === 'success') { - Utils.showAlert('密码重置成功,请使用新密码登录', 'success'); - setTimeout(() => { - window.location.href = '/accounts/login/'; - }, 1500); - } else { - Utils.showAlert(response.message || '重置失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Reset password error:', error); - Utils.showAlert('重置失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理退出登录 - */ - async handleLogout() { - if (!Utils.confirm('确定要退出登录吗?')) { - return; - } - - try { - Utils.showLoading(); - - await fetch('/accounts/logout/', { - method: 'POST', - headers: { - 'X-CSRFToken': Config.csrfToken, - }, - }); - - window.location.href = '/accounts/login/'; - } catch (error) { - console.error('Logout error:', error); - Utils.showAlert('退出失败,请稍后重试', 'danger'); - Utils.hideLoading(); - } - } -}; - -// 用户资料管理器 -const Profile = { - /** - * 初始化用户资料 - */ - init() { - this.bindEvents(); - this.initAvatarUpload(); - }, - - /** - * 绑定事件 - */ - bindEvents() { - // 资料表单 - const profileForm = document.getElementById('profile-form'); - if (profileForm) { - profileForm.addEventListener('submit', (e) => { - this.handleProfileUpdate(e); - }); - } - - // 密码修改表单 - const passwordForm = document.getElementById('password-form'); - if (passwordForm) { - passwordForm.addEventListener('submit', (e) => { - this.handlePasswordChange(e); - }); - } - - // 头像上传按钮 - const avatarUploadBtn = document.getElementById('avatar-upload-btn'); - if (avatarUploadBtn) { - avatarUploadBtn.addEventListener('click', () => { - document.getElementById('avatar-input').click(); - }); - } - }, - - /** - * 初始化头像上传 - */ - initAvatarUpload() { - const avatarInput = document.getElementById('avatar-input'); - if (avatarInput) { - avatarInput.addEventListener('change', (e) => { - this.handleAvatarUpload(e); - }); - } - }, - - /** - * 处理资料更新 - */ - async handleProfileUpdate(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - try { - Utils.showLoading(); - - const response = await API.post('/accounts/api/profile/update/', data); - - if (response.status === 'success') { - Utils.showAlert('资料更新成功', 'success'); - } else { - Utils.showAlert(response.message || '更新失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Profile update error:', error); - Utils.showAlert('更新失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理密码修改 - */ - async handlePasswordChange(event) { - event.preventDefault(); - - const form = event.target; - if (!form.checkValidity()) { - return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - // 验证新密码 - if (data.new_password !== data.confirm_password) { - Utils.showAlert('两次输入的新密码不一致', 'danger'); - return; - } - - try { - Utils.showLoading(); - - const response = await API.post('/accounts/api/password/change/', data); - - if (response.status === 'success') { - Utils.showAlert('密码修改成功,请重新登录', 'success'); - form.reset(); - setTimeout(() => { - window.location.href = '/accounts/login/'; - }, 1500); - } else { - Utils.showAlert(response.message || '修改失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Password change error:', error); - Utils.showAlert('修改失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理通知设置更新 - */ - async handleNotificationUpdate(event) { - event.preventDefault(); - - const form = event.target; - const formData = new FormData(form); - - try { - Utils.showLoading(); - - const response = await fetch(window.location.href, { - method: 'POST', - headers: { - 'X-CSRFToken': getCsrfToken(), - }, - body: formData - }); - - if (response.ok) { - Utils.showAlert('通知设置更新成功', 'success'); - } else { - Utils.showAlert('设置更新失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Notification update error:', error); - Utils.showAlert('设置更新失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 处理头像上传 - */ - async handleAvatarUpload(event) { - const file = event.target.files[0]; - if (!file) return; - - // 验证文件类型 - const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']; - if (!allowedTypes.includes(file.type.toLowerCase())) { - Utils.showAlert('请选择JPG、PNG或GIF格式的图片文件', 'danger'); - return; - } - - // 验证文件大小(限制为5MB,与后端保持一致) - if (file.size > 5 * 1024 * 1024) { - Utils.showAlert('图片大小不能超过5MB', 'danger'); - return; - } - - // 验证文件扩展名 - const fileName = file.name.toLowerCase(); - const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']; - const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext)); - - if (!hasValidExtension) { - Utils.showAlert('请选择JPG、PNG或GIF格式的图片文件', 'danger'); - return; - } - - const formData = new FormData(); - formData.append('avatar', file); - - try { - Utils.showLoading(); - - const response = await fetch('/accounts/api/avatar/upload/', { - method: 'POST', - headers: { - 'X-CSRFToken': Config.csrfToken, - }, - body: formData, - }); - - if (response.ok) { - const data = await response.json(); - if (data.status === 'success') { - // 更新头像显示 - const avatarImg = document.getElementById('profile-avatar'); - if (avatarImg) { - // 添加时间戳防止缓存 - avatarImg.src = data.avatar_url + '?t=' + new Date().getTime(); - } - Utils.showAlert('头像上传成功', 'success'); - } else { - Utils.showAlert(data.message || '上传失败', 'danger'); - } - } else { - Utils.showAlert('上传失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Avatar upload error:', error); - Utils.showAlert('上传失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - } -}; - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', () => { - const isAuthPage = document.querySelector('.auth-page'); - const isProfilePage = document.querySelector('.profile-page'); - - if (isAuthPage) { - Auth.init(); - } - - if (isProfilePage) { - Profile.init(); - } -}); - -// 导出到全局 -window.Auth = Auth; -window.Profile = Profile; \ No newline at end of file diff --git a/static/js/auth-animation.js b/static/js/auth-animation.js deleted file mode 100755 index 70716a9..0000000 --- a/static/js/auth-animation.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * 认证页面动画效果 - * 为登录和注册页面添加动态背景效果 - */ - -document.addEventListener('DOMContentLoaded', function() { - // 检查是否在登录或注册页面 - if (document.body.classList.contains('login-page') || document.body.classList.contains('register-page')) { - initAuthAnimation(); - } -}); - -function initAuthAnimation() { - // 创建粒子背景效果 - createParticleEffect(); - - // 监听表单交互,添加动态效果 - addFormInteractionEffects(); -} - -function createParticleEffect() { - // 创建Canvas元素 - const canvas = document.createElement('canvas'); - canvas.style.position = 'fixed'; - canvas.style.top = '0'; - canvas.style.left = '0'; - canvas.style.width = '100%'; - canvas.style.height = '100%'; - canvas.style.zIndex = '1'; - canvas.style.pointerEvents = 'none'; - canvas.id = 'auth-particles'; - - document.body.appendChild(canvas); - - const ctx = canvas.getContext('2d'); - let particles = []; - const particleCount = 50; - - // 设置canvas尺寸 - function resizeCanvas() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - - resizeCanvas(); - window.addEventListener('resize', resizeCanvas); - - // 粒子类 - class Particle { - constructor() { - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.size = Math.random() * 2 + 0.5; - this.speedX = Math.random() * 1 - 0.5; - this.speedY = Math.random() * 1 - 0.5; - this.color = `rgba(203, 213, 225, ${Math.random() * 0.5 + 0.1})`; - } - - update() { - this.x += this.speedX; - this.y += this.speedY; - - if (this.x > canvas.width || this.x < 0) { - this.speedX = -this.speedX; - } - if (this.y > canvas.height || this.y < 0) { - this.speedY = -this.speedY; - } - } - - draw() { - ctx.fillStyle = this.color; - ctx.beginPath(); - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - ctx.closePath(); - ctx.fill(); - } - } - - // 创建粒子 - for (let i = 0; i < particleCount; i++) { - particles.push(new Particle()); - } - - // 连接粒子 - function connectParticles() { - for (let a = 0; a < particles.length; a++) { - for (let b = a; b < particles.length; b++) { - const dx = particles[a].x - particles[b].x; - const dy = particles[a].y - particles[b].y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < 100) { - const opacity = 1 - distance / 100; - ctx.strokeStyle = `rgba(203, 213, 225, ${opacity * 0.2})`; - ctx.lineWidth = 0.5; - ctx.beginPath(); - ctx.moveTo(particles[a].x, particles[a].y); - ctx.lineTo(particles[b].x, particles[b].y); - ctx.stroke(); - } - } - } - } - - // 动画循环 - function animate() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < particles.length; i++) { - particles[i].update(); - particles[i].draw(); - } - - connectParticles(); - - requestAnimationFrame(animate); - } - - animate(); -} - -function addFormInteractionEffects() { - // 为表单控件添加聚焦效果 - const inputs = document.querySelectorAll('.form-control'); - inputs.forEach(input => { - input.addEventListener('focus', function() { - this.parentElement.classList.add('focused'); - }); - - input.addEventListener('blur', function() { - this.parentElement.classList.remove('focused'); - }); - }); - - // 为登录卡片添加鼠标移动效果 - const authCard = document.querySelector('.auth-card'); - if (authCard) { - authCard.addEventListener('mousemove', function(e) { - const rect = authCard.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const centerX = rect.width / 2; - const centerY = rect.height / 2; - - const angleY = (x - centerX) / 25; - const angleX = (centerY - y) / 25; - - authCard.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) translateY(-5px)`; - }); - - authCard.addEventListener('mouseleave', function() { - authCard.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) translateY(-5px)'; - }); - } -} \ No newline at end of file diff --git a/static/js/base.js b/static/js/base.js deleted file mode 100755 index 926edfc..0000000 --- a/static/js/base.js +++ /dev/null @@ -1,275 +0,0 @@ -/** - * 基础JavaScript功能 - */ - -// 全局配置 -const Config = { - apiBase: '/api/', -}; - -function getCsrfToken() { - return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || - document.querySelector('[name=csrfmiddlewaretoken]')?.value || - document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1] || - ''; -} - -Config.csrfToken = getCsrfToken(); - -// 工具函数 -const Utils = { - /** - * 显示加载动画 - */ - showLoading() { - const loading = document.createElement('div'); - loading.className = 'loading-overlay'; - loading.id = 'loading-overlay'; - loading.innerHTML = ` -

    - `; - document.body.appendChild(loading); - }, - - /** - * 隐藏加载动画 - */ - hideLoading() { - const loading = document.getElementById('loading-overlay'); - if (loading) { - loading.remove(); - } - }, - - /** - * 显示提示信息 - */ - showAlert(message, type = 'info') { - const alert = document.createElement('div'); - alert.className = `alert alert-${type} alert-dismissible fade show`; - alert.innerHTML = ` - ${message} - - `; - - const container = document.querySelector('.container') || document.body; - container.insertBefore(alert, container.firstChild); - - // 5秒后自动关闭 - setTimeout(() => { - alert.classList.remove('show'); - setTimeout(() => alert.remove(), 150); - }, 5000); - }, - - /** - * 确认对话框 - */ - confirm(message) { - return window.confirm(message); - }, - - /** - * 格式化日期时间 - */ - formatDateTime(dateStr) { - const date = new Date(dateStr); - return date.toLocaleString('zh-CN'); - }, - - /** - * 格式化文件大小 - */ - formatFileSize(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; - }, - - /** - * 防抖函数 - */ - debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - - /** - * 节流函数 - */ - throttle(func, limit) { - let inThrottle; - return function(...args) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; - } -}; - -// API请求封装 -const API = { - /** - * 发送GET请求 - */ - async get(url, params = {}) { - const queryString = new URLSearchParams(params).toString(); - const fullUrl = queryString ? `${url}?${queryString}` : url; - - const response = await fetch(fullUrl, { - method: 'GET', - headers: { - 'X-CSRFToken': Config.csrfToken, - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return response.json(); - }, - - /** - * 发送POST请求 - */ - async post(url, data = {}) { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': Config.csrfToken, - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return response.json(); - }, - - /** - * 发送PUT请求 - */ - async put(url, data = {}) { - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': Config.csrfToken, - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return response.json(); - }, - - /** - * 发送DELETE请求 - */ - async delete(url) { - const response = await fetch(url, { - method: 'DELETE', - headers: { - 'X-CSRFToken': Config.csrfToken, - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return response.json(); - } -}; - -// 表单处理 -const FormHandler = { - /** - * 初始化表单 - */ - init(formSelector, options = {}) { - const form = document.querySelector(formSelector); - if (!form) return; - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - if (options.beforeSubmit) { - const result = await options.beforeSubmit(form); - if (result === false) return; - } - - const formData = new FormData(form); - const data = Object.fromEntries(formData.entries()); - - try { - Utils.showLoading(); - - const response = await API.post(form.action, data); - - if (options.onSuccess) { - options.onSuccess(response, form); - } - } catch (error) { - console.error('Form submission error:', error); - - if (options.onError) { - options.onError(error, form); - } else { - Utils.showAlert('提交失败,请稍后重试', 'danger'); - } - } finally { - Utils.hideLoading(); - } - }); - } -}; - -// 初始化 -document.addEventListener('DOMContentLoaded', () => { - // 初始化所有提示框的关闭按钮 - document.querySelectorAll('.alert[data-dismiss="alert"]').forEach(alert => { - alert.querySelector('.close').addEventListener('click', () => { - alert.classList.remove('show'); - setTimeout(() => alert.remove(), 150); - }); - }); - - // 初始化所有确认按钮 - document.querySelectorAll('[data-confirm]').forEach(button => { - button.addEventListener('click', (e) => { - if (!Utils.confirm(button.dataset.confirm)) { - e.preventDefault(); - } - }); - }); -}); - -// 导出到全局 -window.Config = Config; -window.Utils = Utils; -window.API = API; -window.FormHandler = FormHandler; -window.getCsrfToken = getCsrfToken; diff --git a/static/js/dashboard.js b/static/js/dashboard.js deleted file mode 100755 index be01d27..0000000 --- a/static/js/dashboard.js +++ /dev/null @@ -1,250 +0,0 @@ -/** - * 仪表盘JavaScript功能 - */ - -// 仪表盘管理器 -const Dashboard = { - charts: {}, - refreshInterval: null, - - /** - * 初始化仪表盘 - */ - init() { - this.initCharts(); - this.startAutoRefresh(); - this.bindEvents(); - }, - - /** - * 初始化图表 - */ - initCharts() { - // 主机状态分布图 - const hostStatusCtx = document.getElementById('hostStatusChart'); - if (hostStatusCtx) { - this.charts.hostStatus = new Chart(hostStatusCtx, { - type: 'doughnut', - data: { - labels: ['在线', '离线', '未知'], - datasets: [{ - data: [0, 0, 0], - backgroundColor: [ - '#28a745', - '#dc3545', - '#6c757d' - ] - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom' - } - } - } - }); - } - - // 操作趋势图 - const operationTrendCtx = document.getElementById('operationTrendChart'); - if (operationTrendCtx) { - this.charts.operationTrend = new Chart(operationTrendCtx, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: '操作次数', - data: [], - borderColor: '#007bff', - fill: false, - tension: 0.1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - ticks: { - stepSize: 1 - } - } - }, - plugins: { - legend: { - position: 'bottom' - } - } - } - }); - } - }, - - /** - * 更新图表数据 - */ - async updateCharts() { - try { - const data = await API.get('/dashboard/api/stats/'); - - if (this.charts.hostStatus) { - this.charts.hostStatus.data.datasets[0].data = [ - data.hosts.online, - data.hosts.offline, - data.hosts.error - ]; - this.charts.hostStatus.update(); - } - - if (this.charts.operationTrend) { - const trendData = await this.getOperationTrend(); - this.charts.operationTrend.data.labels = trendData.labels; - this.charts.operationTrend.data.datasets[0].data = trendData.data; - this.charts.operationTrend.update(); - } - } catch (error) { - console.error('Failed to update charts:', error); - } - }, - - /** - * 获取操作趋势数据 - */ - async getOperationTrend() { - try { - const response = await API.get('/dashboard/api/stats/', { type: 'operations' }); - // 这里可以根据实际API返回的数据格式进行调整 - return { - labels: [], - data: [] - }; - } catch (error) { - console.error('Failed to get operation trend:', error); - return { labels: [], data: [] }; - } - }, - - /** - * 开始自动刷新 - */ - startAutoRefresh() { - // 每5分钟刷新一次数据 - this.refreshInterval = setInterval(() => { - this.updateCharts(); - }, 300000); - }, - - /** - * 停止自动刷新 - */ - stopAutoRefresh() { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - }, - - /** - * 绑定事件 - */ - bindEvents() { - // 刷新按钮 - const refreshBtn = document.getElementById('refresh-dashboard'); - if (refreshBtn) { - refreshBtn.addEventListener('click', () => { - this.updateCharts(); - }); - } - - // 组件配置按钮 - const configBtn = document.getElementById('widget-config-btn'); - if (configBtn) { - configBtn.addEventListener('click', () => { - window.location.href = '/dashboard/widget-config/'; - }); - } - } -}; - -// 组件配置管理器 -const WidgetConfig = { - /** - * 初始化组件配置 - */ - init() { - this.bindEvents(); - }, - - /** - * 保存配置 - */ - async saveConfig() { - const widgets = []; - - document.querySelectorAll('.widget-item').forEach(item => { - const widgetId = item.dataset.widgetId; - const isEnabled = item.querySelector('.widget-enabled').checked; - const displayOrder = item.querySelector('.widget-order').value; - - widgets.push({ - widget_id: parseInt(widgetId), - is_enabled: isEnabled, - display_order: parseInt(displayOrder) - }); - }); - - try { - Utils.showLoading(); - - const response = await API.post('/dashboard/api/widget-config/', { widgets }); - - if (response.status === 'success') { - Utils.showAlert('配置保存成功', 'success'); - setTimeout(() => { - window.location.href = '/dashboard/'; - }, 1000); - } else { - Utils.showAlert(response.message || '保存失败', 'danger'); - } - } catch (error) { - console.error('Failed to save widget config:', error); - Utils.showAlert('保存失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 绑定事件 - */ - bindEvents() { - const saveBtn = document.getElementById('save-widget-config'); - if (saveBtn) { - saveBtn.addEventListener('click', () => { - this.saveConfig(); - }); - } - } -}; - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', () => { - const isDashboardPage = document.querySelector('.dashboard-page'); - const isConfigPage = document.querySelector('.widget-config-page'); - - if (isDashboardPage) { - Dashboard.init(); - } - - if (isConfigPage) { - WidgetConfig.init(); - } -}); - -// 导出到全局 -window.Dashboard = Dashboard; -window.WidgetConfig = WidgetConfig; diff --git a/static/js/email_code.js b/static/js/email_code.js deleted file mode 100755 index e4e7461..0000000 --- a/static/js/email_code.js +++ /dev/null @@ -1,79 +0,0 @@ -(function () { - function qs(sel) { return document.querySelector(sel); } - var btn = qs('#get-email-code'); - if (!btn) return; - - var countdownTimers = {}; - - function startCountdown(button, initialText) { - var buttonId = button.id || 'unknown-button'; - if (countdownTimers[buttonId]) { - clearInterval(countdownTimers[buttonId]); - } - var count = 60; - var originalText = initialText || button.textContent || '获取验证码'; - button.disabled = true; - button.textContent = originalText + ' (' + count + 's)'; - countdownTimers[buttonId] = setInterval(function () { - count--; - button.textContent = originalText + ' (' + count + 's)'; - if (count <= 0) { - clearInterval(countdownTimers[buttonId]); - delete countdownTimers[buttonId]; - button.disabled = false; - button.textContent = originalText; - } - }, 1000); - } - - btn.addEventListener('click', function (e) { - e.preventDefault(); - var emailInput = document.querySelector('input[name="email"]') || document.querySelector('input[type="email"]'); - var email = emailInput && emailInput.value && emailInput.value.trim(); - if (!email) { alert('请先输入邮箱'); return; } - - var isForgotPassword = window.location.pathname.includes('forgot-password'); - var endpoint = isForgotPassword ? '/accounts/email/send-forgot-password-code/' : '/accounts/email/send-code/'; - var provider = window.CAPTCHA_PROVIDER || 'none'; - - if (provider === 'tianai') { - return; - } - - function postCode(payload, buttonRef) { - fetch(endpoint, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]') ? document.querySelector('[name=csrfmiddlewaretoken]').value : '' - }, - body: payload - }).then(function (resp) { - if (resp.ok) { - alert('验证码已发送,请注意查收'); - if (buttonRef) startCountdown(buttonRef, '获取验证码'); - } else { - if (buttonRef) { - buttonRef.disabled = false; - buttonRef.textContent = '获取验证码'; - } - resp.json().then(function (j) { alert('发送失败:' + (j.message || JSON.stringify(j))); }).catch(function () { alert('发送失败'); }); - } - }).catch(function (err) { - console.error(err); - if (buttonRef) { - buttonRef.disabled = false; - buttonRef.textContent = '获取验证码'; - } - alert('网络错误'); - }); - } - - var fd = new FormData(); - fd.append('email', email); - if (window.REGLINK_TOKEN) { - fd.append('reglink_token', window.REGLINK_TOKEN); - } - postCode(fd, btn); - }); -})(); diff --git a/static/js/operations.js b/static/js/operations.js deleted file mode 100755 index d108dd6..0000000 --- a/static/js/operations.js +++ /dev/null @@ -1,538 +0,0 @@ -/** - * 操作记录JavaScript功能 - */ - -// 操作日志管理器 -const OperationLog = { - /** - * 初始化操作日志 - */ - init() { - this.bindEvents(); - this.initFilters(); - }, - - /** - * 绑定事件 - */ - bindEvents() { - // 导出按钮 - const exportBtn = document.getElementById('export-logs-btn'); - if (exportBtn) { - exportBtn.addEventListener('click', () => { - this.exportLogs(); - }); - } - - // 筛选表单 - const filterForm = document.getElementById('filter-form'); - if (filterForm) { - filterForm.addEventListener('submit', (e) => { - this.handleFilterSubmit(e); - }); - } - - // 重置筛选按钮 - const resetBtn = document.getElementById('reset-filter-btn'); - if (resetBtn) { - resetBtn.addEventListener('click', () => { - this.resetFilters(); - }); - } - - // 查看详情按钮 - document.querySelectorAll('.view-log-detail-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const logId = e.target.dataset.logId; - this.showLogDetail(logId); - }); - }); - }, - - /** - * 初始化筛选器 - */ - initFilters() { - // 初始化日期选择器 - const dateInputs = document.querySelectorAll('.date-picker'); - dateInputs.forEach(input => { - // 这里可以集成日期选择器库 - // 例如:flatpickr、bootstrap-datepicker等 - }); - }, - - /** - * 处理筛选表单提交 - */ - async handleFilterSubmit(event) { - event.preventDefault(); - - const form = event.target; - const formData = new FormData(form); - const params = Object.fromEntries(formData.entries()); - - try { - Utils.showLoading(); - - // 构建查询字符串 - const queryString = new URLSearchParams(params).toString(); - window.location.href = `${window.location.pathname}?${queryString}`; - } catch (error) { - console.error('Failed to filter logs:', error); - Utils.showAlert('筛选失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 重置筛选器 - */ - resetFilters() { - const filterForm = document.getElementById('filter-form'); - if (filterForm) { - filterForm.reset(); - window.location.href = window.location.pathname; - } - }, - - /** - * 显示操作日志详情 - */ - async showLogDetail(logId) { - try { - Utils.showLoading(); - - const response = await API.get(`/operations/api/logs/${logId}/`); - - if (response.status === 'success') { - this.showLogDetailModal(response.data); - } else { - Utils.showAlert(response.message || '获取详情失败', 'danger'); - } - } catch (error) { - console.error('Failed to get log detail:', error); - Utils.showAlert('获取详情失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 显示操作日志详情模态框 - */ - showLogDetailModal(logData) { - const modalHtml = ` - - `; - - const modalContainer = document.createElement('div'); - modalContainer.innerHTML = modalHtml; - document.body.appendChild(modalContainer); - - const modal = new bootstrap.Modal(document.getElementById('logDetailModal')); - modal.show(); - - document.getElementById('logDetailModal').addEventListener('hidden.bs.modal', () => { - modalContainer.remove(); - }); - }, - - /** - * 获取状态徽章HTML - */ - getStatusBadge(status) { - const statusMap = { - success: '成功', - failed: '失败', - pending: '进行中' - }; - return statusMap[status] || status; - }, - - /** - * 导出操作日志 - */ - async exportLogs() { - try { - Utils.showLoading(); - - // 获取当前筛选条件 - const filterForm = document.getElementById('filter-form'); - const formData = filterForm ? new FormData(filterForm) : new FormData(); - const params = Object.fromEntries(formData.entries()); - - // 调用导出API - const response = await fetch('/operations/api/export/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': Config.csrfToken, - }, - body: JSON.stringify(params), - }); - - if (response.ok) { - // 下载文件 - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `operation_logs_${new Date().getTime()}.xlsx`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - - Utils.showAlert('导出成功', 'success'); - } else { - Utils.showAlert('导出失败,请稍后重试', 'danger'); - } - } catch (error) { - console.error('Failed to export logs:', error); - Utils.showAlert('导出失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - } -}; - -// 任务管理器 -const TaskManager = { - /** - * 初始化任务管理 - */ - init() { - this.bindEvents(); - this.startAutoRefresh(); - }, - - /** - * 绑定事件 - */ - bindEvents() { - // 取消任务按钮 - document.querySelectorAll('.cancel-task-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const taskId = e.target.dataset.taskId; - this.cancelTask(taskId); - }); - }); - - // 重试任务按钮 - document.querySelectorAll('.retry-task-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const taskId = e.target.dataset.taskId; - this.retryTask(taskId); - }); - }); - - // 查看任务详情按钮 - document.querySelectorAll('.view-task-detail-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const taskId = e.target.dataset.taskId; - this.showTaskDetail(taskId); - }); - }); - }, - - /** - * 开始自动刷新 - */ - startAutoRefresh() { - // 每30秒刷新一次任务状态 - this.refreshInterval = setInterval(() => { - this.updateTaskStatus(); - }, 30000); - }, - - /** - * 停止自动刷新 - */ - stopAutoRefresh() { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - }, - - /** - * 更新任务状态 - */ - async updateTaskStatus() { - const taskItems = document.querySelectorAll('.task-item[data-status="running"]'); - - for (const item of taskItems) { - const taskId = item.dataset.taskId; - try { - const response = await API.get(`/operations/api/tasks/${taskId}/`); - - if (response.status === 'success') { - this.updateTaskItem(item, response.data); - } - } catch (error) { - console.error(`Failed to update task ${taskId}:`, error); - } - } - }, - - /** - * 更新任务项 - */ - updateTaskItem(item, data) { - // 更新状态 - const statusBadge = item.querySelector('.task-status'); - if (statusBadge) { - statusBadge.className = `task-status ${data.status}`; - statusBadge.textContent = this.getStatusText(data.status); - } - - // 更新进度 - const progressBar = item.querySelector('.progress-bar .progress'); - if (progressBar) { - progressBar.style.width = `${data.progress}%`; - } - - // 更新数据属性 - item.dataset.status = data.status; - }, - - /** - * 获取状态文本 - */ - getStatusText(status) { - const statusMap = { - pending: '等待中', - running: '执行中', - success: '成功', - failed: '失败', - cancelled: '已取消' - }; - return statusMap[status] || status; - }, - - /** - * 取消任务 - */ - async cancelTask(taskId) { - if (!Utils.confirm('确定要取消此任务吗?')) { - return; - } - - try { - Utils.showLoading(); - - const response = await API.post(`/operations/api/tasks/${taskId}/cancel/`); - - if (response.status === 'success') { - Utils.showAlert('任务已取消', 'success'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - Utils.showAlert(response.message || '取消失败', 'danger'); - } - } catch (error) { - console.error('Failed to cancel task:', error); - Utils.showAlert('取消失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 重试任务 - */ - async retryTask(taskId) { - if (!Utils.confirm('确定要重试此任务吗?')) { - return; - } - - try { - Utils.showLoading(); - - const response = await API.post(`/operations/api/tasks/${taskId}/retry/`); - - if (response.status === 'success') { - Utils.showAlert('任务已重新开始', 'success'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - Utils.showAlert(response.message || '重试失败', 'danger'); - } - } catch (error) { - console.error('Failed to retry task:', error); - Utils.showAlert('重试失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 显示任务详情 - */ - async showTaskDetail(taskId) { - try { - Utils.showLoading(); - - const response = await API.get(`/operations/api/tasks/${taskId}/`); - - if (response.status === 'success') { - this.showTaskDetailModal(response.data); - } else { - Utils.showAlert(response.message || '获取详情失败', 'danger'); - } - } catch (error) { - console.error('Failed to get task detail:', error); - Utils.showAlert('获取详情失败,请稍后重试', 'danger'); - } finally { - Utils.hideLoading(); - } - }, - - /** - * 显示任务详情模态框 - */ - showTaskDetailModal(taskData) { - const modalHtml = ` - - `; - - const modalContainer = document.createElement('div'); - modalContainer.innerHTML = modalHtml; - document.body.appendChild(modalContainer); - - const modal = new bootstrap.Modal(document.getElementById('taskDetailModal')); - modal.show(); - - document.getElementById('taskDetailModal').addEventListener('hidden.bs.modal', () => { - modalContainer.remove(); - }); - } -}; - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', () => { - if (document.querySelector('.operation-logs-page')) { - OperationLog.init(); - } - - if (document.querySelector('.tasks-page')) { - TaskManager.init(); - } -}); - -// 导出到全局 -window.OperationLog = OperationLog; -window.TaskManager = TaskManager; diff --git a/static/js/tianai_adapter.js b/static/js/tianai_adapter.js deleted file mode 100644 index 27c4dcf..0000000 --- a/static/js/tianai_adapter.js +++ /dev/null @@ -1,214 +0,0 @@ -(function () { - function qs(sel) { return document.querySelector(sel); } - function $all(sel) { return Array.prototype.slice.call(document.querySelectorAll(sel)); } - - var _countdownTimer = null; - var _activeOverlay = null; - var _activeCaptchaBox = null; - - function startCountdown(button, initialText) { - if (_countdownTimer) clearInterval(_countdownTimer); - var count = 60; - var originalText = initialText || '获取验证码'; - button.disabled = true; - button.textContent = originalText + ' (' + count + 's)'; - _countdownTimer = setInterval(function () { - count--; - button.textContent = originalText + ' (' + count + 's)'; - if (count <= 0) { - clearInterval(_countdownTimer); - _countdownTimer = null; - button.disabled = false; - button.textContent = originalText; - } - }, 1000); - } - - function getGenerateUrl(captchaType) { - var baseUrl = '/captcha/generate'; - if (captchaType && captchaType !== 'SLIDER') { - return baseUrl + '?type=' + encodeURIComponent(captchaType); - } - return baseUrl; - } - - function createModal() { - if (_activeOverlay) return _activeOverlay.querySelector('#tianai-captcha-box'); - - var overlay = document.createElement('div'); - overlay.id = 'tianai-captcha-overlay'; - overlay.style.cssText = 'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;'; - - var captchaBox = document.createElement('div'); - captchaBox.id = 'tianai-captcha-box'; - captchaBox.style.cssText = 'position:relative;'; - - overlay.appendChild(captchaBox); - document.body.appendChild(overlay); - - _activeOverlay = overlay; - _activeCaptchaBox = captchaBox; - - overlay.addEventListener('click', function (e) { - if (e.target === overlay) { - destroyModal(); - } - }); - - return captchaBox; - } - - function destroyModal() { - if (_activeOverlay) { - _activeOverlay.remove(); - _activeOverlay = null; - _activeCaptchaBox = null; - } - } - - function showTianaiCaptcha(onSuccess, captchaType) { - var captchaBox = createModal(); - - var config = { - requestCaptchaDataUrl: getGenerateUrl(captchaType), - validCaptchaUrl: "/captcha/check", - bindEl: "#tianai-captcha-box", - validSuccess: function (res, c, tac) { - var token = null; - if (res && res.data && res.data.token) { - token = res.data.token; - } else if (res && res.token) { - token = res.token; - } - tac.destroyWindow(); - destroyModal(); - if (onSuccess) onSuccess(token); - }, - validFail: function (res, c, tac) { - tac.reloadCaptcha(); - }, - btnCloseFun: function (el, tac) { - tac.destroyWindow(); - destroyModal(); - } - }; - var style = {}; - var tac = new window.TAC(config, style); - tac.init(); - } - - function setTokenField(form, token) { - var input = form.querySelector('input[name="captcha_token"]'); - if (!input) { - input = document.createElement('input'); - input.type = 'hidden'; - input.name = 'captcha_token'; - form.appendChild(input); - } - input.value = token || ''; - } - - function postEmailCode(email, token, button) { - var isForgotPassword = window.location.pathname.includes('forgot-password'); - var endpoint = isForgotPassword ? '/accounts/email/send-forgot-password-code/' : '/accounts/email/send-code/'; - - var formData = new FormData(); - formData.append('email', email); - if (token) { - formData.append('captcha_token', token); - } - if (window.REGLINK_TOKEN) { - formData.append('reglink_token', window.REGLINK_TOKEN); - } - - fetch(endpoint, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]') ? document.querySelector('[name=csrfmiddlewaretoken]').value : '' - }, - body: formData - }).then(function (resp) { - if (resp.ok) { - alert('验证码已发送,请注意查收'); - if (button) startCountdown(button, '获取验证码'); - } else { - if (button) { - button.disabled = false; - button.textContent = '获取验证码'; - } - resp.json().then(function (j) { alert('发送失败:' + (j.message || JSON.stringify(j))); }).catch(function () { alert('发送失败'); }); - } - }).catch(function (err) { - console.error(err); - if (button) { - button.disabled = false; - button.textContent = '获取验证码'; - } - alert('网络错误'); - }); - } - - document.addEventListener('DOMContentLoaded', function () { - if (window.CAPTCHA_PROVIDER !== 'tianai') return; - - var captchaType = window.CAPTCHA_TYPE || 'SLIDER'; - - $all('[data-tianai-captcha-trigger]').forEach(function (btn) { - btn.addEventListener('click', function (e) { - e.preventDefault(); - e.stopImmediatePropagation(); - - var form = btn.closest('form'); - showTianaiCaptcha(function (token) { - if (form) setTokenField(form, token); - var action = btn.dataset.action; - if (action === 'submit') { - form.submit(); - } - }, captchaType); - }); - }); - - $all('#get-email-code[data-tianai-email-trigger]').forEach(function (button) { - var newBtn = button.cloneNode(true); - button.parentNode.replaceChild(newBtn, button); - - newBtn.addEventListener('click', function (e) { - e.preventDefault(); - e.stopImmediatePropagation(); - if (this.disabled) return; - - var emailField = document.querySelector('input[type="email"]'); - if (!emailField || !emailField.value) { - alert('请先输入邮箱'); - emailField && emailField.focus(); - return; - } - - var emailCaptchaType = window.CAPTCHA_TYPE_EMAIL || captchaType; - showTianaiCaptcha(function (token) { - postEmailCode(emailField.value, token, newBtn); - }, emailCaptchaType); - }); - }); - - $all('form').forEach(function (form) { - var hasCaptchaTrigger = form.querySelector('[data-tianai-captcha-trigger]'); - if (!hasCaptchaTrigger) return; - - form.addEventListener('submit', function (e) { - var tokenField = form.querySelector('input[name="captcha_token"]'); - if (!tokenField || !tokenField.value) { - if (window.CAPTCHA_PROVIDER === 'tianai') { - e.preventDefault(); - showTianaiCaptcha(function (token) { - setTokenField(form, token); - form.submit(); - }, captchaType); - } - } - }); - }); - }); -})(); diff --git a/static/scripts/init.ps1 b/static/scripts/init.ps1 deleted file mode 100644 index 89319e9..0000000 --- a/static/scripts/init.ps1 +++ /dev/null @@ -1,222 +0,0 @@ -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -$EncodedToken = $args[0] -if (-not $EncodedToken) { - Write-Host "Usage: & ([ScriptBlock]::Create(`$script)) [debug]" -ForegroundColor Red - return -} -$Debug = $args[1] -eq 'debug' -or $args[1] -eq '1' - -$padLen = 4 - ($EncodedToken.Length % 4) -if ($padLen -ne 4) { $EncodedToken += '=' * $padLen } -try { - $jsonBytes = [System.Convert]::FromBase64String($EncodedToken) - $jsonStr = [System.Text.Encoding]::UTF8.GetString($jsonBytes) - $tokenObj = $jsonStr | ConvertFrom-Json - $Token = $tokenObj.t - $Scheme = $tokenObj.s - $ServerHost = $tokenObj.h -} catch { - Write-Host "Token解码失败" -ForegroundColor Red; return -} -if (-not $Token -or -not $Scheme -or -not $ServerHost) { - Write-Host "Token格式无效" -ForegroundColor Red; return -} -$ServerUrl = "${Scheme}://${ServerHost}" -if ($Debug) { Write-Host " ServerUrl: $ServerUrl" -ForegroundColor DarkGray } - -Write-Host "=== 2c2a WinRM 证书自动配置 ===" -ForegroundColor Cyan - -Write-Host "[1/17] 验证Token..." -ForegroundColor Yellow -$validateResp = Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/validate/?token=$Token" -Method Get -if (-not $validateResp.valid) { - Write-Host "Token无效或已过期" -ForegroundColor Red; return -} -if ($Debug) { Write-Host " ServerHost: $ServerHost" -ForegroundColor DarkGray } -Write-Host " Token验证通过" -ForegroundColor Green - -Write-Host "[2/17] 上传主机名..." -ForegroundColor Yellow -$hostname = $env:COMPUTERNAME -$body = @{ token = $Token; hostname = $hostname } | ConvertTo-Json -Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/upload-hostname/" -Method Post -Body $body -ContentType "application/json" -if ($Debug) { Write-Host " 主机名: $hostname" -ForegroundColor DarkGray } -Write-Host " 主机名已上传: $hostname" -ForegroundColor Green - -Write-Host "[3/17] 等待证书签发..." -ForegroundColor Yellow -$certData = $null -$maxWait = 120 -$waited = 0 -while ($waited -lt $maxWait) { - Start-Sleep -Seconds 5 - $waited += 5 - try { - $statusResp = Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/validate/?token=$Token" -Method Get - if ($Debug) { Write-Host " Token状态: $($statusResp.status)" -ForegroundColor DarkGray } - } catch { - continue - } - try { - $certResp = Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/download-certs/?token=$Token" -Method Get - if ($certResp.ca_cert) { - $certData = $certResp - break - } - } catch { - } - Write-Host " 等待中... ($waited/${maxWait}s)" -ForegroundColor DarkGray -} -if (-not $certData) { - Write-Host "证书签发超时" -ForegroundColor Red; return -} -if ($Debug) { Write-Host " 证书数据已获取" -ForegroundColor DarkGray } -Write-Host " 证书已就绪" -ForegroundColor Green - -Write-Host "[4/17] 下载证书文件..." -ForegroundColor Yellow -$TempDir = "$env:TEMP\2c2a_Certs" -New-Item -ItemType Directory -Force -Path $TempDir | Out-Null -[System.IO.File]::WriteAllBytes("$TempDir\ca.crt", [System.Convert]::FromBase64String($certData.ca_cert)) -[System.IO.File]::WriteAllBytes("$TempDir\client.crt", [System.Convert]::FromBase64String($certData.client_cert)) -[System.IO.File]::WriteAllBytes("$TempDir\server.pfx", [System.Convert]::FromBase64String($certData.server_pfx)) -$PfxPassword = $certData.pfx_password -$NtlmUser = $certData.ntlm_user -$NtlmPassword = $certData.ntlm_password -$UpnValue = $certData.upn_value -if ($Debug) { Write-Host " 保存路径: $TempDir" -ForegroundColor DarkGray } -Write-Host " 证书文件已保存到 $TempDir" -ForegroundColor Green - -Write-Host "[5/17] 导入证书..." -ForegroundColor Yellow -$tempCa = Import-Certificate -FilePath "$TempDir\ca.crt" -CertStoreLocation Cert:\LocalMachine\Root -$caIssuerPattern = $tempCa.Subject -replace '.*CN=([^,]+).*','$1' -Get-ChildItem Cert:\LocalMachine\Root | Where-Object Subject -match $caIssuerPattern | Remove-Item -Force -$importedCa = Import-Certificate -FilePath "$TempDir\ca.crt" -CertStoreLocation Cert:\LocalMachine\Root -Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Issuer -match $caIssuerPattern } | Remove-Item -Force -Get-ChildItem Cert:\LocalMachine\TrustedPeople | Where-Object Subject -match "winrm-client" | Remove-Item -Force -$secPwd = ConvertTo-SecureString $PfxPassword -AsPlainText -Force -$importedServer = Import-PfxCertificate -FilePath "$TempDir\server.pfx" -CertStoreLocation Cert:\LocalMachine\My -Password $secPwd -Import-Certificate -FilePath "$TempDir\client.crt" -CertStoreLocation Cert:\LocalMachine\TrustedPeople -if ($Debug) { Write-Host " CA Thumbprint: $($importedCa.Thumbprint)" -ForegroundColor DarkGray } -if ($Debug) { Write-Host " Server Thumbprint: $($importedServer.Thumbprint)" -ForegroundColor DarkGray } -Write-Host " 证书导入完成" -ForegroundColor Green - -Write-Host "[6/17] 创建本地用户..." -ForegroundColor Yellow -$SecurePassword = ConvertTo-SecureString $NtlmPassword -AsPlainText -Force -if (-not (Get-LocalUser -Name $NtlmUser -ErrorAction SilentlyContinue)) { - New-LocalUser -Name $NtlmUser -Password $SecurePassword -Description "2c2a WinRM Certificate Auth User" -} else { - Set-LocalUser -Name $NtlmUser -Password $SecurePassword -} -Add-LocalGroupMember -Group "Administrators" -Member $NtlmUser -ErrorAction SilentlyContinue -if ($Debug) { Write-Host " 用户: $NtlmUser" -ForegroundColor DarkGray } -Write-Host " 用户 $NtlmUser 已创建" -ForegroundColor Green - -Write-Host "[7/17] 配置HTTPS监听器..." -ForegroundColor Yellow -Get-ChildItem WSMan:\localhost\Listener | Where-Object { $_.Keys -match "Transport=HTTPS" } | Remove-Item -Recurse -Force -New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint $importedServer.Thumbprint -Force -if ($Debug) { Write-Host " Thumbprint: $($importedServer.Thumbprint)" -ForegroundColor DarkGray } -Write-Host " 监听器已绑定: $($importedServer.Thumbprint)" -ForegroundColor Green - -Write-Host "[8/17] 配置客户端证书映射..." -ForegroundColor Yellow -Get-ChildItem WSMan:\localhost\ClientCertificate | Remove-Item -Recurse -Force -$cred = New-Object System.Management.Automation.PSCredential($NtlmUser, $SecurePassword) -New-Item -Path WSMan:\localhost\ClientCertificate -Subject $UpnValue -Issuer $importedCa.Thumbprint -Credential $cred -Force -if ($Debug) { Write-Host " Subject=$UpnValue Issuer=$($importedCa.Thumbprint)" -ForegroundColor DarkGray } -Write-Host " 映射已建立: Subject=$UpnValue" -ForegroundColor Green - -Write-Host "[9/17] 配置Schannel注册表..." -ForegroundColor Yellow -reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v ClientAuthTrustMode /t REG_DWORD /d 2 /f -reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v SendTrustedIssuerList /t REG_DWORD /d 0 /f -if ($Debug) { Write-Host " ClientAuthTrustMode=2, SendTrustedIssuerList=0" -ForegroundColor DarkGray } -Write-Host " ClientAuthTrustMode=2, SendTrustedIssuerList=0" -ForegroundColor Green - -Write-Host "[10/17] 启用证书认证..." -ForegroundColor Yellow -Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true -Write-Host " 证书认证已启用" -ForegroundColor Green - -Write-Host "[11/17] 配置防火墙..." -ForegroundColor Yellow -$FirewallRuleName = "WinRM HTTPS (5986)" -$existingRule = Get-NetFirewallRule -DisplayName $FirewallRuleName -ErrorAction SilentlyContinue -if (-not $existingRule) { - New-NetFirewallRule -DisplayName $FirewallRuleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5986 -} else { - Enable-NetFirewallRule -DisplayName $FirewallRuleName -} -if ($Debug) { Write-Host " 规则: $FirewallRuleName" -ForegroundColor DarkGray } -Write-Host " 防火墙已配置" -ForegroundColor Green - -Write-Host "[12/17] 重启WinRM服务..." -ForegroundColor Yellow -Restart-Service WinRM -Force -Write-Host " WinRM服务已重启" -ForegroundColor Green - -Write-Host "[13/17] 通知服务器配置完成..." -ForegroundColor Yellow -$notifyBody = @{ token = $Token } | ConvertTo-Json -$notifyResp = $null -$notifyOk = $false -for ($i = 1; $i -le 3; $i++) { - try { - $notifyResp = Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/notify-complete/" -Method Post -Body $notifyBody -ContentType "application/json" - $notifyOk = $true - break - } catch { - if ($i -lt 3) { Start-Sleep -Seconds 5 } - } -} -$testDeferred = $false -if ($notifyOk) { - if ($notifyResp.test -eq "deferred") { - Write-Host " 已通知服务器(连接测试将在主机注册后执行)" -ForegroundColor Green - $testDeferred = $true - } else { - Write-Host " 已通知服务器" -ForegroundColor Green - } -} else { - Write-Host " 通知服务器失败,但本地配置已完成" -ForegroundColor Yellow - $testDeferred = $true -} - -if ($testDeferred) { - Write-Host "[14/17] 连接测试已延后,请在后台完成主机注册" -ForegroundColor Yellow -} else { - Write-Host "[14/17] 等待连接测试..." -ForegroundColor Yellow - $testResult = $null - $testWaited = 0 - $maxTestWait = 60 - while ($testWaited -lt $maxTestWait) { - Start-Sleep -Seconds 5 - $testWaited += 5 - try { - $testResp = Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/test-result/?token=$Token" -Method Get - if ($testResp.status -ne "testing") { - $testResult = $testResp - break - } - } catch { - continue - } - Write-Host " 测试中... ($testWaited/${maxTestWait}s)" -ForegroundColor DarkGray - } - if ($testResult -and $testResult.status -eq "success") { - Write-Host " 连接测试成功!" -ForegroundColor Green - } else { - Write-Host " 连接测试失败或超时" -ForegroundColor Yellow - } -} - -Write-Host "[15/17] 安全性提升选项" -ForegroundColor Yellow -$choice = Read-Host "是否禁用密码认证以提升安全性?(Y/N)" -if ($choice -eq "Y" -or $choice -eq "y") { - Write-Host "[16/17] 禁用密码认证..." -ForegroundColor Yellow - Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $false - Set-Item -Path WSMan:\localhost\Service\Auth\Digest -Value $false - Set-Item -Path WSMan:\localhost\Service\Auth\Kerberos -Value $false - Set-Item -Path WSMan:\localhost\Service\Auth\CredSSP -Value $false - Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $false - Restart-Service WinRM -Force - $disableBody = @{ token = $Token } | ConvertTo-Json - Invoke-RestMethod -Uri "$ServerUrl/bootstrap/api/cert-provision/disable-password-auth/" -Method Post -Body $disableBody -ContentType "application/json" - Write-Host " 已禁用密码认证,仅允许证书认证" -ForegroundColor Green -} else { - Write-Host " 保留密码认证" -ForegroundColor Yellow -} - -Write-Host "[17/17] 清理临时文件..." -ForegroundColor Yellow -Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue -Write-Host "`n=== 配置完成 ===" -ForegroundColor Cyan diff --git a/static/src/tailwind.css b/static/src/tailwind.css deleted file mode 100644 index da983d3..0000000 --- a/static/src/tailwind.css +++ /dev/null @@ -1,81 +0,0 @@ -@import "tailwindcss"; - -/* ============================================================================ - * 2c2a - Material Design 3 (MD3) Theme Configuration - * Tailwind CSS v4 CSS-first configuration - * - * Dark mode baseline: - * All MD3 colors use the `md-` namespace - * Purple-based Material You dark palette - * ============================================================================ */ - -@theme { - /* ---- MD3 Primary ---- */ - --color-md-primary: #D0BCFF; - --color-md-on-primary: #381E72; - --color-md-primary-container: #4F378B; - --color-md-on-primary-container: #EADDFF; - - /* ---- MD3 Secondary ---- */ - --color-md-secondary: #CCC2DC; - --color-md-on-secondary: #332D41; - --color-md-secondary-container: #4A4458; - --color-md-on-secondary-container: #EADDFF; - - /* ---- MD3 Tertiary ---- */ - --color-md-tertiary: #EFB8C8; - --color-md-on-tertiary: #492532; - --color-md-tertiary-container: #633B48; - --color-md-on-tertiary-container: #FFD8E4; - - /* ---- MD3 Surface ---- */ - --color-md-surface: #1C1B1F; - --color-md-on-surface: #E6E1E5; - --color-md-on-surface-variant: #CAC4D0; - --color-md-surface-variant: #49454F; - --color-md-surface-container: #211F26; - --color-md-surface-container-low: #1D1B20; - --color-md-surface-container-high: #2B2930; - - /* ---- MD3 Outline ---- */ - --color-md-outline: #938F99; - --color-md-outline-variant: #49454F; - - /* ---- MD3 Error ---- */ - --color-md-error: #F2B8B5; - --color-md-on-error: #601410; - --color-md-error-container: #8C1D18; - --color-md-on-error-container: #F9DEDC; - - /* ---- MD3 Border Radius ---- */ - --radius-md: 12px; - --radius-md-lg: 16px; - --radius-md-xl: 28px; -} - -/* ============================================================================ - * Source directives - scan all template directories for Tailwind classes - * ============================================================================ */ -@source "../../templates"; -@source "../../apps/*/templates"; -@source "../../static/js"; - -/* ============================================================================ - * Select Dropdown Dark Theme - * Forces all native -

    yo~17{e|>yrNSVVwj3m^Rxho=#-DV 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�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�fyyA=tCfkzRY^7__ z_s$E%t4jxna8=f{l;DHieFDJS&R@NQs?xisZ!3;aADQCZIW_SaYAd`i%n*L$#>nie z&1Jtc#{0P@Y!*Ka5lpOHBeoM%*lI6+4{$dUze;@!{;*$f#dIShO)V?Y>4;&Zv%n2KLohSnZSy*9CFr z-7VHvS%a!_looCFWPy- z=N$H+wPbh82>K~3d~tPZ$6XbYt1naWPSDU4XhDMPL6G3+@mK-Jqd<)y@Q`I(t0A|X z?1OfVgp@&6n#IWRy8wy^5Q|A5%M3NlGZSOurpAyjN6OugUBRvDJC=MjDNLV+&ryRj7>rDT6YAma&qkb0 zbfrFDGqE^R`z@3 zcB@DCVArOdtf%ZvtkI>rkn^GH8=eqqB5PMSn$lY9S~59I_7$J1^d2dP$ACTD=te?jF`!y^$ln;(R^uOHVo$DN#d7;T=A>aBY~HQyMwp9(M( zZw`%W6Q!IXQ(nZ2cvX-{W_EKOl|ev{1;d&~{kC*8nphCOwh}&NXV%4o8NOuRa?Ar;`M-hkni9J(cK!fsH9zRcXegtH)VnDe{3h=2W|Mm!wx z;d+cT%b;!qn5daoRDbHZcrIu2Lhr)cb`nYL@C@is;`lE8fp4m_v{zthZbDhXu)z3D zmr*)f%kQh9f#$DV*iGF1{?R#`l|p4Bj1TkgW6PBYfG_Yp2o_kBoz_ z^~7lvtZW(a63%31OsiRVkz7PU5A0sO864#gfr4nKf`Npj|j`~D~+J-T_-1bJC59-F_ z7EJ%}{N{d%XZna=TrVM9m4XC!AXqTe+p;pEsVNNNQDd25ZQ)Guc6MFb=UF_gJqFHi zV^AU}YyACezgQ|b9k|LrXa`E?v&mYr{OFA;xomiuwyLQxq&rOYisQab+hDi(<4_c|cod}>+z#iX>oVIyPMD7|XGm;o>8K&MuN&r}Dx zb|&MH?Jvar$opt;Z50F3(1__RSp^D#8b4g2V}>fC(3r4&MvcGVH2p}c0;|~T1hdtj z{#?kbPsfMP@OO0^!15oi-UxLptO-<~u>ROvVc~$q!dogf(3$Gn--)C06Gi?MVij0k z;4&QqKKM2Gb0)S`V=vmN2yd}QcUV1+K*a2D1mutGB#y)PGj2ugqf076E(g+VUG}WX zYUwM_#9HAC{vh{h_yz^~i>Vb{nz;rITn?f@=o`Z~K6F>KvCFok@>h7!~3P^7+QPV7l!TpT6senM`hXiObHyM6hRseVG)YklKB;CIM#qD zVnioRgu8sMyn5ys5t1&6PP(4)2ATePA#d#4_FbC5dFE$)7Pv)FXFYE!d{DXqi?)Ja z>r2#cwHl?FVd*m8M{&6DrM*7JhGTt94vrz;z)YKj4|xrGS~}S z7+Z5$noBN*wX;fb12AmNs_4x8TU6js79;NFcqq4=p^CuOM=WamAHytd%EyVHrHMOw zJ3C0*PO<7{$3uK{87=4`A$>?@lRA>ZAa8`yrWK~9;4#t?;i-fQ_QmbeaJJ} z#^$VDl2Zc)V_z!vxv8hDtm=zjY#297K5;%pRI|N1zj|Y;YEF~3)BGw#^^G=-MKTP9 zj~BLvBh(!c3^Lvw0~EU*ru~H8=j8nz-@3>->du)ybsnC++8l`{?K_{4HV7}}8w?X2 zV7F2|-_}UctXBTCD6=vX+M94@_Xb1v$K=s)#snJ*OS2x^;y85f-Wu)M%HFHh$2LDF z%VL;|PKfNO4Oo!+^Sf+G14*5(s+@%fZDiT)cuJoNy;by;XC=ioao&;%SdOhocz?kT zQD9otrl3W2))EL+@px^Q>4g!!OpzW4<86VZ$<>tgU zuhe5!)h`Fs-C~AnQR#B z&vkh9iYfPUpj>4CU99LN7@0>m=!?HTan`DB!OiIFb>Ej}h&g4!Pj}pJA(6MkqXy4; zxh;Ol=IE?fo=Dk^bocwVhH#@zW~*q@{Ii`8F4R>TYt|Sbebis{_pdZMONNHF#PlsJ zwCxgPOFDM$L9cEFQ!Ox^~ zNSufR6!;U|Ll5(BbpB+w=lAHZ;%Bk9*$=4eCbco69j0RhKdW@az3exG@yK#t%0SW`g-rfCZB<(W&d;M0 zjlye~=9!wB);2uF&*G&@IUlINVj@+ASzHKMnCJu*1ouw}um8pnEBjbLx`LOXpu>I4itw)Cn{+G<@zFMl|8cW|q*wj+=}Ixu_9!8_JBXkdIbYM2z3L z0HN9s;T-}3b!WV+&f4i5xt6ak9zVb9yfPJdQd;?q{Y;|+b*+3okcBmfmML*(vPs9Bs~C#o;e?gx++t7!CDmc>{2`J0C5_ zt0#DPE|2F|NOwisIoJ9$VRfKuzwU&QJ^%oP-QA6&a3&2~=xYyH>P;2-H}j``KwbS} zfM0IgXw+4PVyynIljW3ebK*loM-k0{#~12ot{(ww*E4^2{qTdvQQFM9kYr%#-~|v z$=91JGx=l-YZAy`gU?H3)Ft*+nd_6-S5`jd-8n<~^`c53B`)jb*^f`yhKj(>W6ZlO z=FP;LsWW8vQA#1BJc&x{T2&(t}v+^V46r0A#C zI9`#ND_np&Tshbs{L?02&?Qj!jc-%9r=H8()?)t`C3X{cm~!JGEMW zfs6}kp0DY*=FV+}g7h;l z{nJr2?DV}btLq65`wy2T?dSzo!foX8>w;e59k!Ar_~!_2 z0MOr(mhd=??dDxw0Hp5Zz|-SQaIbyM_d)+KpHWZEA#X!WJ>ZY<#kfAv*##;l_%zsS zU1t;S_|E37#@*X{_27M7{Ok~y_07LRhAEPEHD}u*?lnvE@|Osa|QsD1F)2jX=b&_xFs1}bC=1;@dZ0I>if3f zP{AFdPdS?-+czD<&t;jY0!gj-UzHzB`4$bjK0Z}LnlZIbPHt~CcC0lz3r^y>PV%}5 zYa|fN66Ee7A@>HcQyUcD$qgmJB+DHj%`axFL+ELnV+#Veg^;|=HSsD9HuT(OJrsp0 z6M9E;B*~S^4HlM3m3;@%WB_!TiqNw7@AgPR{K(s2gEs}HDHPFW1kwQZB@Hq&pA3cr zCF%f6Mf#Ve+yl`2TOJo;J^6S>GsmhOPb@|s*$mbbbl`jJMqm`sw-w!@NUpPTww;lJ zyW-d6w#nc?#Yf3I+6shwo}=jP1|rt%2ITF~VZM&jDUyH3YE0cVJ2NJbrTCkzTT(cv5@vrqT6zOf6*Jf>7 zO9(l8DOBk^TV^W@nf`b~QNl$p%U5q=vtdLAo zeSqwTQ~2s2nfbUo=Vn?FsgeRS(Ld(rBaY*f=%u%a3AAx8``A;~BBENHH`L#HO2#nu zCA77*cT2Ez7C?S^_*Ym0g*+|5!!cAgNhZtUVQ)1b*SB1j?PWItXF=@cF(L_%>|(Kg z)x$i{N-4(nhsrtqG$?adBE!uze6S<>dYn;AS#VeOlk4|O6F|-8Jn+LGaYN4I&mJ!3 zM``Y>cE(OPKCS#&l;)RHNLtjh`mJ~OM>glzDMn@(r^+hdNIk8?t=Kb2PIE%fN^fB# zlUMY)5?%8An>GjaI8XH)eGK{?bBDUPM6-GI4W*d%Rf|M{)O3hz^eX%%EluI{`=4k~ zX~D&3z;w%CY!2ErZdNN#n4Eb8|f8*Q08Z-rH=TNhg)=en(MK31|`(W znOk_)TJ}U~e0#i6e{34EdgkozWDgFp>yC(rFefdlra438-d1Q=aW3649@YBX*4EQY zRQf}6TkQr9j3~IW(%H>J+XAc&nYe=eln{OM=0xa$;)x7 zbMwUU59sO{Jk9xlLB;`d+0t;L@Y_QyH!YRpx)#yk8SIAtLrH)yHTlB?!W zZ`$XkrZ0#+8lnr#Sm)Xwtz;_A_@0WPtu_Gb+ff<+F^?!Tsw5{Al5hNh$EMS*rXw5+ zOGfSYp}LSWr`oWR*Ki-Lb&1cTB!&S$+03f2AEi9z@-qhUr__PhG zXNRx?gQaxdNWcXAXIgZr@U>moa(y&X8Zpzqsfl*y#5JsLXq0T>?&A} z%6sAH9+_BrA>ZvPtT{N$^-|&o+aTBMFhqJk5stb&E)GP3`oNugWavj%9 z+il!*Q;#+j)(*VlNN|Sc{x@`6hXrC&1im?E*23I8Kl2egyZXKGYE5-dn4+b&(jq({QsTtL{tn7aes!W4te`L^N0A z|LQ0SJDu+6N37)H>cbCThxxgM>iJG~R1r#8xdn#ih+*r+LZDY$5p3-r`H+|+`}3Xl z`y}QT>H;^5KZ!34%ZwK1gIlx-Cazj!yhk+l(;EArZpsjc#h5`A=>2-+k(x>Dcy|g!+ss&%DUz&lasZOpi?W+DSqPiY z>~+|@D(GLalXbo&@qou@XPW_X=^jk3D@shL&x>tVB(}@DuU=G#%Y%EjYR66vBs$Md z5wKLS>>^E$FtQ|Cy5p7Z4Yn7N5S~unPa-YOFt)rbMM4&x-6qO^uFrJBBGSr!t%!X6 z2_l^(P+syM2DJ&5+OydejsiJo0-pMpSJe~+VT|nqWsmBurlzK5abxZ=)QALdC8ThY zbAPdcnPLIi<=jZ-KWc8kd*exMKq~CI~2aRPqDfanKgsN&`Su{G3 zhW9CPGP={mbRN`wC1?>ypf%L)Zvof{Fr=~wS*Zr21Nnq_KLW1gmS}${VT<(R37Sc; zy9}zPn}i)byFgltnSKk!Vw_Z z!+bUAhtO+DlU^i;YbT*m`{kiw&(D?08wLVDs)x{=<=jawmjdf{o3c9exrEayFhBzN zpeQ~=JOS)UHB?kuC5p-lz+@D5*YJ=|3)Frrb1@Uu zF6OVjJKw1v6Jb?Y5&Npks7I-*qeX-|dvODrxT`!}(pD}ISe35ClQaY_ZAt?+ zDFJCBz4t^q3W5mIno~cqVYbd3uo0T#@2GmHh3PzXEw-b+ttyv!R6rK^K zGE8KTo4(lCMz6kjvA%&q>Y>;atHp)o&5}+CsT`aDC+>4XNSNvaAbF+XWH%YG8p2Fx z8eOj|9pTGw{yj35ig+S(^_a-mdb&?NzdLGE8;4g!JQIl?>A*eOllWb6^ndH#&x;+zfwIJ-h-swU-PxN)xKiV0C_Cs zPOP-XR6OAPyawW%SVrElkDFvOVy$5tV^ODTwB@+oN{x9yQNb&ln+!8cT@oGstTPI* z{RT6yIgM}h*$n3{K3-Z`Z0^Ei_YORWZ18Z*t>3CQ3|;5BzbH&(frsI_+fNT&!%llu zTH&po^M)~o8#Tw?Jnrnx@z2_k9+u>@v-EXk4QR=sKny3=Cxt~6qglR2@U)xP4$9$j7ln! z;so$5iH!}oGqkdgu5g!Lr~f#)vWb_AGwM1x;-=t|L3+l)G1Ye#U`gAw;WT*DhWHKs zF`%k!=Rvg;)N!IfA83d3-L0M_WXy+|Daz`5Y;G^Gtvfl13ne}^Cl3@?`)%&#P7oel zG%WhrC7bi!C^26RkW|<9T-sEk+7R{FmyU;)kX54m5O8VtEKTPe%}u(;Gt^X%1*q8R z3u{$IKLHnFxhKV+U`v63S|V3_Nc7JeqqGXiAl6ag94Q&pP14k7CfYF&!%~KjLD2v4>o}T zh#{YgGCj1TEU{xH@b|38qm>Xp=`atnPgIt*lXQ>^ zereTF)1@Zr*TJZS4IfMAeWyFllHsN<((!Aah^QM9R^Tj^pPEK?Bp~i+FsTpv-V>Vn z?%H4)Txrc#Ziew#=E5_Bjasw!^?NgvokxClD$NP zF#-O*0t4jK5HPuEupfu1q`)vxQTCkI9eVwEO|CKw!+T0jZ!z-))&APFT&^$08&PXN z*-<0?f)F1OjLAE|RR!?#13c7{AWUhZ90MV>K>C4147jHnjOC;hZF(UU9-=jvUIuCl zjw>;yHi9o|wek*I!C&tcIIELuI~|-Bl{rVZ7v}diQ3YAnfO5*)_lp>714%0g2`f9h zL4FsW3|UJ2QA&Sp)A%C{dz7jF)=v>CEn$L){|xV&f>BnXOJX67icfE)tI=rK%}{^b z=i#W(rq^|kRraf?kFfR}muu*7PuRu4KSXw4eC^L`yUX*D^~q`tXE(k`q%)?hvSPemYp~43@&{QlJ8Pzpc z;i*|R_PBn*p4HE-i*!r)DEs1quAe1QPI!|rp&Rcyml~%@*oGOcyW}V?4Jv)wnTSI>7w_yprf2Wtg}!0cC~ z0mdXq9=}08>!yh^-*>Oz9HnrjVytWemR@QhV)Y0ywAV$*61|5=zHoVi`w`$~dH|$X zLp__#g7Aw$rZ7^EQ8S@k)yL~0W3*KbSIFYR3SnQionc6y2%uU!$4l~By*9_a55#@P4qn24oSERqCOCxDnI*AC1eV%T-&qoY(Or4q+>5CL*d#FcM(cp@X(ENkUtaDDHn#h2}# z<2#RrEe}r)b@=k;@x_424vQ#xKCS0=?r~39fZzij0D04rFrGniDapUgPEg3|KXL>o zjw_t(5E(pgcC_&Q>aaM={QCX$3kfh`N;3Q~X zmeU$^$SicD!Q~=H=FqNpvk}qiBO-%1=BUu^pNmm$TVRA`f}RKnsFu+pTtilV?CdF^ zNA?4cS0>Wfh8I{A|; z*NDiPuTwKIjzJ;QioL%)_5Aajq)`I8%&{GuwW&~-<}8%y*@xuqyn}nVRqyg$9%s(- z`l^`uba@*zbT3d-O4lo#Kk5p_ZF}qy^^&ioljZrvj?iUFw>maBks!4XAPqx0JW2Nt z_DGw6Rr09J(bH$5H><|43-XOdsVZ&4ae85LZ< zKY?!t3u6b0%9_iol|qtGN&z7yh`Bt+UB9tgi8Ck3XvA4a&$LD zv?x*P@}(=;*CWs0qhY3|W1?A(h-zffr&0;Ebt;9H4UI5i^(ZW9aaon}k1chswMp>O zTP?R`Z?6q8u?Mna`*qhP%q^w=?e&t<|2;^ykv0BdvJUD<`!irNtU{1U#*%2gc$3HC zlI3?+FM$%$}DA#=ql_#b}D#7C8{Dn82y|@OGLKBIYsRJdD#{*1dHRx z=Xa0gq0t;Q@`?tsUHhv`P(!`a`c$K*d>7=U?&Qz2%b_(IT0&%)_oO5}9~$m(ri^xk zE5G}ya4Eo2acjo3%r!dy!C3!j)Y9^x*JoX1iJ$cQq(Fz~MB?PY^Z5W8pgg#S_1+>An8cc4a~ z^9Fa+##q};CU*e3H4No}#}9kcoGFdWl{Ur*jAn=VXO4l7t4G_t>+xE-!5+JqBBN3^ zb^f_R&)sW2Xw=Ku$3J-TB}zc-R#gdan)WU%t!Ley6?AR?u~|ejGqvmBxwAW%Mv(u2S=G{V^L49|^5=lS{Y62(H%z9lTDa^lBSvD-^Hyy*mTH^OS5mDZc~Y(W zs%8bvBQc`pk-O?{+|raW#m_GDNZ%P?88CIaoh3b#WzMH-@`F9)ne-(0;JjZA%5B~B z{#cf@q5|<=&YiIUs&82y5AbAJSz;%J=j6WKg7$o>7A?+^GdB5Q`>yGYnTU#xkPtSAp7ri6_dY+Uk#JJGiH*F!Uj$g0z7WuNh~(r zSs;}Byfn~KNkFfEA-TEr1M~XLB>mtY2Sjq6;pB8L^;TQB`feHuA6ElaHA|zlh)~#m zJ-u=!J8(yY9)oAkK&{Jv?myzoE67*1bDBg;+Zw(dYL@KJ-WZ$N{jpBz+2-%XE{oF@ z?jOuYs>bw&sNay?V)x1&bm&3_X_=h2YVwI;=9SzBv|@!I=>D;4A`Y_AK$_g!&)WkR zw6xgwnd+K4Z80)YUrW7Z-&u8-S{sL&_1UtrgrGZSUM+>GZ>Ie(ky zoYAY$PeMF0k$NLt?|==4p7Tzh#O0l=eMnGY;W#LuLbZ1N#c6@5o|C2A=~Fo(VD*=< zb-$jo{iJ=q+RhSKk1|>Zg-67r8?5`mu%ld<+PAswfSF+hJ&$Puk?ZN3{_i`d4Lg{? z7k28CRWH>c$!^DJS;24G@zGA_?|Z69$ipjBMO^borrK0j5-f9O)E;Erw20t8|5)c7 z=bAu7W$x_0!4cI`(X(j{JB%US`aFvgcPvxSP`_zo5W|D&(XItUA{pSw(&$o(?!1D(x!khm8j? z{5p5|!aG~ISazrO$laEUs`f&V=Ixh?i)9O!e{|+%Mr^~c!pGso68iV-WAkf#B9r$x zht+WEPQH6WE?D*H=NaZn_rH9s={QAOS}nDAnArrU^5(Y<=~r7JPMxcKRtP*f#7N2j zpG4|}tVkWH3-qxTVt30`5RZ)jM>}O7Ky*$MtGV~2M;}dLlx1mY`XiZsb{%jErkBoo zZ<4dVl+Wz(L=}5B`0o0Tffkh>TEqz6F5B0B<&PT|A`+Wk=@JzSUrSLDP1kw=|B#$# zl`BGsKwn~Ur8#-rBze(-+_;K?AYsS{@EO2R#~|CxS5Ubi7}-wh8N1ZjoxLYqanaCm zx{X|u*UX;ZS>{@Encl+Z9*VK-1v!UxX3}bnr`@^3YgX)*QIzowtY7_X*Fm7yi^I(A zk=3uQUL6?r%<9orPYo;8dTmV>L!##+_#?Q2XFIGtL(u1phTR9YiXU`E;tNgLD_I$d z)c`DK1L-h_Fc?TmgA>@vSwM0nc|bS}0iE^kZjfB=Mkgk)-V}bvi1_kaef^BVxi*e7 z5zSTC%_z1^?AehQ`{e{h`a!WDwBjYF`I)Fa&(sS|r}HHV`IONn#TamK?;&}pn5-&G zp0(xHPlxysosoOOMWodOgJ;b9f& zV8C|2UdGt5ne}dAb3pPA9ClMOS3LpSJ%?NDC%&IsbIuN^Q_TxMH;1p^Jv5qXa%nnq z?qYV~i_zc>e=m6`C-$AP@WetWn})}0p$})4H~niqbI^!JyGU1ThtF>2AA5iKZZ1dm zno1%QyG>!9i%3Y>^jxx_KXKC}V;=@Oshsup1l-Vb#T<6e@(3Rilp6DT%3vAX;DR(! zC7vBsLpQuf<}PEcJGr5r71&lRFhm)!E<$x*NAs~7s(*zvzT(tC`}{E0aA{^|=_Up* z3(azm&M5VqE`T!oZ!%7iZy_^iCl`M{kOwSgk#)nJ-p_oH1z%8}gZXE=(<(Q7k#*O% zyANu~YJWjanZ>pAy8z~b@ujY(pp(O7W$;2C$bSmhQx_g?4nB2kd$LF}9Udp~ou*zp z%^B@K7j%e$qx`LP^0T1ckj>Y>led4CYHfYQ)3K~v^tY5bH+#99Gx5?YG%FU>pLvy# z$CHr@cL+W{0Tr)4raclv7)W1=jo%nhymE|!_v-}?rdo$IiODNC;)tyI?O`!F3P1$L z4AL+=kK%i zitt512HrbY9pEhY4vE5xYXuaxIa&`+jy6W^aWurOr!A)%+uC#Ghn&^lEj;Va*wOsO zdMGRR!Rw_%ThF*J$U>oT%YXaX=;(~yq}Tpk(U3JUfTDP)Vq&GQT2lYXX3&k7IhT_n z!>d>18g=bDN94eEMo!9^ow_zbR&fLEiJVcrihyeAo%kcs?!pWvm$<>8WD}*U$$KWd z3)l9RuBLNoHEG|w!rwiBhT5!9K;PyYGvL|=%x3env|wW{m@oR?FYRgzr_XVWSDY?A zz1qyFrM5~p6~UXUY#aG^$EUP1g;l;((!OAw;42czHaQ&Ils*>M!JUSSsYN z!AW@d1_HbbKfFdEk-A8K3Ou--8kT_vG{$&Z7hTqrJy+ZNk?q_4%jX|XGxy%7Gwz`K zDZDKmH(L^nk5H^r&{UqkKiU!MasA={uKDN61Cd<(EGrcX57d$KzbgxkZOlkor^%Cs zkiC^}U<&v#%Af6EP(m~zPw$9yvB>7&$I6PT#mOQ*VTQDVQ&O8`N&GJ@A8DcF`;LPS zw>z-`_f-Pz0vRWw#rUFD-j)GZk zGFl=WZgae`@|S7&oNN=sFN^ix4J2uvMs-(Ylh0YiBtfR1lV1&(1m_-IiI%Qhd&Q@u zX=?h|k(%e8p^}?ov;w?!Ox)87v0ELU2R>kibV-qCAPB~Wz=2K?w!%ud#b^7Q&kEst zvNw^-jP9>|yu*2sSqtH)ra#Z0EARr?WHSe;HL~t6n9KP6Gl4eBmi&wR)ot|~J*lOxok)?2!}ZFGP3EV`sk@DO z>DJf-{{?I^l%s|n@ z0D^4ydic~-Sp`1h$5^Le7`aazjy&P9}Cl<5P=aIBGk7h+@L73iGY zHuQ68X}J`PLx!#F2A;qmC(MLHPXbQ?NTE9@+2{6$BaJOAH7&L|aSMNCs5x6mYZeJ4JTCyDo%nT=B4K z8JgDT|1_A{x%u+V$GG>e%Ce=i(vEQSIsE)Rck*+7}T=*sUM&m~???*5eL*o3Y6iCHNEN`F5spa(my#$&f@caHP?t`V_H zC?njd_e5ngI2k_+UPvyO#ihjD=-9zGRC&6PU6;JQ>?Hy9M#J?o(HhlQjrR?72XJ{z|BA?ESzQ?B3cGd?fsu*=VTYOKRU|3`Jrg&up&*---0euu2*T z^fXz7u;we8>BNFEq*TcQo=ag-In`}L;;{U+reR*C29qDveQqqw3@%#tjO?<0L`Blw z;A#y<;gK6WJDB05N28wrb-X>>(jPF-OW2z#lh-OrDXkTLs{|j3bRZ9{ReJlGmyh5% z(5cQXpGP+y2aEL$NHg5beVkMyuzi7#UZLTfCvn}7gyWwlOaf0#K_oM>pfDU4-Rhmc z(9$!uCBz-Rg(=+9j{oHF>A5+?csvczX3^jldqL5^Nh)=R8&favlZOWZl;_8G$XCy) zSK&#g?+#af1vU>Mc-XBs%Qm2T7UZhqz~NFrBZ=`ASP)ZCx;TV|2(6Gd)(D_7KuT&w zIdL&HpG>QdG@sH$z`IM%E^gyH5zSdCzE{uFFMZofka_mr#FSf?$=uRdMFrxNujDLo z-WRRn!4p{B0>c_Cie6#509Y1s5`13Pu096(w5vyDfQ5Sdub-B3s|_85o*xS#ZG1)x zBLfz|zzxW{qRP&3igG(l;_h|vg*=X~JI0y+JjG@YB5do!XKQg0lGYgJ@|_BZxi_i2 z9%$vy>UN)mSKI)b8IqGjai9fsT>s+QC+-4g;)(KD!L>Zn_i!1@WWoPh$;+;KjBZiW z-xcJN@o$(oEesP=a?!M!^gd&6xlmWu7OjUCRp_djP%LrU3taSL4egj)HtjeTDag?| zs9JbT0wUIpl{QLnv)j0(@%L;0tH?hs0SyGzYo9fA>rrYyDJx~I=#9ZV17$3L5m_=xOc5Wl=vbeJ;xMFoYCv{Yz>7 zvm#H7>b;|P<8_I_x_&jwJ#Lxw(w814b8JpUn9ZeV(8}{lzzO>)rGHeo&icwH4(|@N zRYjqlkix_@)MWu^M56XtosPwUmsyi=d|D-lJtRl9PB56Xpx+weIPj(vABI-|W$Y1_ zlg~umatq>QK=3(UfAn(dQmC#2%}Wbw_+u!HQr*IEL!py1QJG20QlJ7bU}fih^n$pcD<%xk79%0M+O0!Sp9V~W#a z{hK+@&GJ`BDiG)@@2qs3|w=NAdl_tCXI3$sghZTSczT|i0!@*na_(8QsK&il!g9P;~r%-8A|qvshy({Wbr$oHOKh2NwnV z2W^taQ0d5slV%#R&$k)}UQNi``URfti>5LXbJ+Ju+}nM)yU-bcs?nrx3EV=+!1CLA z7(Ls@824I7F|JZIZAyiDz0y~oZJhV$d3%9JE%HjmkPfr|#TDH}@1C8Md*uPvT9?-Y z?mX<+Nsft8m$v&_WP}ed2K>DZ_ABxmWe!Ht*K*dBV7l^wVr+(mbPdf(Otw$OzxhYZ zV^Jx-o2$n>A2Kd1=6JbhHXW}Fw-SFr_jiH#Tcp_ra0~!fC;axRB};v^7DR75LWZ-5 zQIyk*Cbmi2Z{+NPY>;Lbb~wI{nv?D9yFwBA|9g7Mr%!C|tSwVs3P1Nx6N+4}>sZL$ z4umn~TBCtjru={9p){f;)%R-g9l>JH^eShuYfv|;=vh@)CTib)!J*nu0=sVm_a32k z|1r3lwkDY0;+4b?vsp_}0B#I>hDA_-T$SHW}!Rj_F)LkD+PjbgN+!F68h! z;yhbjQDXgMx*G2vEs!#8&b4JrQp-u_L?YbVj4egxr83{#y(F)oaN~7ku58f(A65eb z?+@MI%z=LOKJfH9of9Vvc@l(v=c}OofD_K6P!=g0QR=&yuU=#zsd8=fw*9bKL5%9c zghEiQDwCE(C#4W~#72FpA6~UsXDo|vZNWK% z{vJn4YO#BddAhxXS(@YhxhB22HpY!;>R0IQ$OK-=gAEvP!*y?Q{S9yR;ZzgrSj;;c z`DZEdHy)kWUA%GGInr5c`V+To1ZV99jvaL=w%KgZ%~9W0|5$V_2_@cr zDf?5PMr~o;6$pxh0fW+!u!6P2UTRCVpG+JA_xz<}_%7*k%CJDMh|)bvJPK9wdEno{ z?RIgCifym@W4pxPYa%hW(gtf?_1>QB6*p(+=AIS({FI%@xBJFBP+nf1&+um1$L05C zlKTykWV!s{1AkI41eZbB0pl*+J2OX>i!z**T4ONk%eKwU(^TZ9``5{%#0F|;qeR!E zE)wKD8!mCIu%Qe^cCz-Ouhm5+kD6IhH%#A+t>>QCmvZ&0OhK%FViL~>H?2JcwrO|G zs%I!;;bq$hdxE;}_WTGsz{a?i>%mxD>7MnrCk)FhfKF6#=vdrexMV@czgmAko_p;k z1>ByOnP83(yYiav%&6RSg%I_R3iW5~&6Iwg)b*)rhskO(Z2sCHkzyT_j>VS1ZA=I| zQKbo{Eih96-Oa&IKH)G^g)FisbNNrfR=)=}K9!@bD@*!ttK}GKLRPm%19lc+--#cP zCx8v&=?sXJ>+RfDG4UOv7ao>>)CNJJ8;~cvL*xr_#?`|L{*o^MU}la^kvXQWMpC@q zzUCv?L^;}kDqSQE!?Ng=tl0b29sfduiK9z9?N%6o5s6>3kyVrE`gM1%3%D;m*)&ZF z1d#i>R^6Qha|B=L4_#)I1k2ObIM|ijKr#die9@{^jGgC(hXRr9AcyK>;J1G0$kBLE z;9QeZpSeQ3XGxRF45xEF61(?+IMW7R_p-*AmBLpr3VPuLkH=W5dr#ipdUyTa6QD44 ztKhb*abfn4OD1mzo5T$IjwXdv00ic{Yj`V(LPU#O@F3DGf2F&@1~KfZHoP$^#4C?g z6n^vZOeB?UCm$QF)0K4OYkqN>S%FI_c}+AQ>C2HpPE{^#S?)5~!Qb#x9o`e7Ej2I` z5bA+#T6=uOWV2^sRp_ah81mzh@p_~34sON4u!fq(!)Tv|MT<)jx>&kV<94RsyKMVg z<^6(6bC`BW)#s}Wd{0>7I~pta*cez;A0_E~QuS~W!ol3&PJVpQ~@5x<87W=c# z4F^j+y!qh~p^m*v#Met3&NuYq-qvt1buy)$_xLeoX?hygCYqj8YbAI`RDqRg4mViO z(ga^93ZAg0{yxW+UkUI;$f+0`%cf>bLjt}*>xw+657q^Ai37F4f>z*g0e^S$!Iy%d zTsXy52!7`pbQ~?W2Qfv0g*Q$tJbDSPX87!?>cBD50RMoh7u@4mb@wUxn-_U3HwHOL zKnQ2TsWt0HS!6Gn+d4bP(Zql7(1tO8tpX=`?;^mOzD$iUe)yfc1{(D1RPU9taGD-M zhEe@pNW5MCZQG!V1{FDMTWR2M>+H2D@O*i|Q(t^lQT@&VS#i6&Xt&OEvY;-U8tEn+ z)U%so=AS#`n03h$;NPkMTz`0te4?h#zpuE_GJ39$`;&Si4innVq8sbW)u>7>ZY$tM zZdd)1GJL926!_xDdr{g`(*jNN-W2|OOa}JFhllT zHrWE}UIY9s772+di(9DWYdHnKUKGGgNq`r5Feq0(q@z6PaDqaBvq$n+cMoS^h`kHeknUEu8#s8--yaRBPxAyxAv>4*V;x#FgXFZj6p(u zw2rA)J=QTM=HqY^N&QxWihj02 zsA_bF^BP{%wy8&gOO8Up2dsK~gLX6KM$ll5v#PHh#mQ?QhyykcW_YxAq0Po* z6UTn!+Ef%ZHXuToBKR)RRTs51M7@xkoNvF0(Xf|)W8U`jm7|?)ZPT8S^Z!tpc&OJW z=j_^rJ3AVJ@h|Hoc=tDjA7&^-EHDhX-hD7ORn4fIY#ZTV^dQUUHuc`uwzknzG~zTA zQSFQQB2HLasw^!dkU`;dDHOil

    - {% if form.email.errors %} -

    {{ form.email.errors.0 }}

    - {% endif %} - - -
    -
    - mail - - {% if CAPTCHA_PROVIDER == 'tianai' %} - - {% else %} - - {% endif %} -
    -
    - -
    -
    - lock - - -
    -

    至少8个字符

    - {% if form.new_password1.errors %} -

    {{ form.new_password1.errors.0 }}

    - {% endif %} -
    - -
    -
    - lock - - -
    - {% if form.new_password2.errors %} -

    {{ form.new_password2.errors.0 }}

    - {% endif %} -
    - - - -
    - {% if CAPTCHA_PROVIDER == 'tianai' %} - - {% else %} - - {% endif %} -
    - - -
    - - - - - - - - - - {% if CAPTCHA_PROVIDER == 'tianai' %} - {% load static %} - - - - {% endif %} - - - - - diff --git a/templates/accounts/login.html b/templates/accounts/login.html deleted file mode 100644 index ab55492..0000000 --- a/templates/accounts/login.html +++ /dev/null @@ -1,234 +0,0 @@ -{% load static cotton %} - - - - - - 登录 - {{ site_name }} - - - - - - -
    -
    - -
    -
    -
    -
    - {{ site_name }} -
    -

    欢迎回来

    -

    登录您的账户以继续

    -
    - {% if messages %} - {% for message in messages %} - {{ message }} - {% endfor %} - {% endif %} - -
    -

    - 没有账户? 立即注册 -

    -

    - description查看文档 -

    -
    -
    -
    - - - - - {% if CAPTCHA_PROVIDER == 'tianai' %} - {% load static %} - - - - {% endif %} - - - - diff --git a/templates/accounts/migrate.html b/templates/accounts/migrate.html deleted file mode 100644 index 9022589..0000000 --- a/templates/accounts/migrate.html +++ /dev/null @@ -1,196 +0,0 @@ -{% load static cotton %} - - - - - - 站点迁移 - {{ site_name }} - - - - - - -
    -
    -
    -
    -
    -
    -
    - sync_alt -
    -
    -

    站点迁移

    -

    您正在访问站点组「{{ site_group_name }}」

    -
    - - {% if messages %} - {% for message in messages %} - {{ message }} - {% endfor %} - {% endif %} - -
    -
    -
    - info -
    -

    - 您的账户 {{ username }} 尚未加入该站点组。 -

    -

    - 迁移后,该站点组的管理员可以查看并管理您的账户数据,但不影响您在其他站点的使用。 -

    -
    -
    -
    - - {% if email_not_compliant %} -
    -
    - warning -
    -

    邮箱后缀不符合要求

    -

    该站点组要求邮箱后缀在白名单中,您当前的邮箱不满足条件。请先绑定符合条件的邮箱。

    -
    -
    -
    - -
    -

    绑定合规邮箱

    -
    - {% csrf_token %} -
    - - -
    -
    - -
    - -
    -
    - {% endif %} - -
    - {% csrf_token %} -
    - {% if not email_not_compliant %} - - {% endif %} - - logout - 暂不迁移,退出登录 - -
    -
    -
    -
    -
    - - - - - - diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html deleted file mode 100755 index 51f1ec7..0000000 --- a/templates/accounts/profile.html +++ /dev/null @@ -1,513 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}账户设置 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -

    账户设置

    -

    管理您的个人资料、安全设置和通知偏好

    -
    - -
    - -
    - - - - -
    - - -
    - -
    -
    -

    基本资料

    -

    更新您的个人信息和联系方式

    -
    - -
    - {% csrf_token %} - -
    -
    - - - 用户名不可更改 -
    - -
    - - - {% if form.email.errors %} -

    {{ form.email.errors }}

    - {% endif %} -
    -
    - -
    -
    - - -
    - -
    - - -
    -
    - -
    -
    - - -
    - -
    - - -
    -
    - -
    - - -
    - -
    - -
    -
    -
    - - - - - - - - - -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/accounts/register.html b/templates/accounts/register.html deleted file mode 100644 index d9f4c5a..0000000 --- a/templates/accounts/register.html +++ /dev/null @@ -1,253 +0,0 @@ -{% load static cotton %} - - - - - - 注册 - {{ site_name }} - - - - - - -
    - - - -
    -
    -
    -
    - {{ site_name }} -
    -

    {% if reglink %}邀请注册{% else %}创建账户{% endif %}

    -

    {% if reglink %}您已被邀请加入 2c2a{% else %}加入我们,开启云电脑之旅{% endif %}

    -
    - - {% if target_group %} -
    -
    - group -
    -

    注册后将加入用户组

    -

    {{ target_group.name }}

    -
    -
    -
    - {% endif %} - - {% if messages %} - {% for message in messages %} - {{ message }} - {% endfor %} - {% endif %} - -
    - {% csrf_token %} - - -
    -
    - person - -
    - {% if form.username.errors %} -

    {{ form.username.errors.0 }}

    - {% endif %} -
    - -
    -
    - email - -
    - {% if form.email.errors %} -

    {{ form.email.errors.0 }}

    - {% endif %} -
    - -
    -
    - mail - - {% if CAPTCHA_PROVIDER == 'tianai' %} - - {% else %} - - {% endif %} -
    -
    - -
    -
    - lock - - -
    - {% if form.password1.errors %} -

    {{ form.password1.errors.0 }}

    - {% endif %} -
    - -
    -
    - lock - - -
    - {% if form.password2.errors %} -

    {{ form.password2.errors.0 }}

    - {% endif %} -
    - -
    - - -
    - -
    - -
    -
    - -
    -

    已有账户? 立即登录

    -
    -
    -
    - - - - - - - - {% if CAPTCHA_PROVIDER == 'tianai' %} - {% load static %} - - - - {% endif %} - - - - - diff --git a/templates/admin/base.html b/templates/admin/base.html deleted file mode 100755 index b2a37b9..0000000 --- a/templates/admin/base.html +++ /dev/null @@ -1,127 +0,0 @@ -{% load i18n static %} -{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} - - - -{% block title %}{% endblock %} - -{% block dark-mode-vars %} - - -{% endblock %} -{% if not is_popup and is_nav_sidebar_enabled %} - - -{% endif %} -{% block extrastyle %}{% endblock %} -{% if LANGUAGE_BIDI %}{% endif %} -{% block extrahead %}{% endblock %} -{% block responsive %} - - - {% if LANGUAGE_BIDI %}{% endif %} -{% endblock %} -{% block blockbots %}{% endblock %} -{% block page-icon %}{% endblock %} - - - -{% translate 'Skip to main content' %} - -
    - - {% if not is_popup %} - - {% block header %} - - {% endblock %} - - {% block nav-breadcrumbs %} - - {% endblock %} - {% endif %} - -
    - {% if not is_popup and is_nav_sidebar_enabled %} - {% block nav-sidebar %} - {% include "admin/nav_sidebar.html" %} - {% endblock %} - {% endif %} -
    - {% block messages %} - {% if messages %} -
      {% for message in messages %} - {{ message|capfirst }} - {% endfor %}
    - {% endif %} - {% endblock messages %} - -
    - {% block pretitle %}{% endblock %} - {% block content_title %}{% if title %}

    {{ title }}

    {% endif %}{% endblock %} - {% block content_subtitle %}{% if subtitle %}

    {{ subtitle }}

    {% endif %}{% endblock %} - {% block content %} - {% block object-tools %}{% endblock %} - {{ content }} - {% endblock %} - {% block sidebar %}{% endblock %} -
    -
    - - {% block footer %}{% endblock %} -
    -
    -
    - - - - - - - - - - - diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html deleted file mode 100755 index 38086f0..0000000 --- a/templates/admin/base_site.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "admin/base.html" %} -{% load static %} - -{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} - -{% block branding %} -

    - - - -

    -{% endblock %} - -{% block nav-global %}{% endblock %} diff --git a/templates/admin/hosts/host/manage_permissions.html b/templates/admin/hosts/host/manage_permissions.html deleted file mode 100644 index fcf0c03..0000000 --- a/templates/admin/hosts/host/manage_permissions.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends "admin/change_form.html" %} -{% load i18n admin_urls static %} - -{% block title %}{{ title }}{% endblock %} - -{% block breadcrumbs %} - -{% endblock %} - -{% block content %} -
    -

    {{ host.name }} - 权限管理

    - -
    - 说明:在此页面您可以管理主机上用户的管理员权限。只有关联到此主机的产品用户才会显示在这里。 -
    - - {% if products %} - {% for product in products %} -
    -
    -

    {{ product.display_name }}

    -

    {{ product.display_description|default:"无描述" }}

    -
    -
    - {% with product_users=users|dictsort:"username" %} - {% if product_users %} -
    - - - - - - - - - - - - - {% for user in product_users %} - {% if user.product.id == product.id %} - - - - - - - - - {% endif %} - {% endfor %} - -
    用户名用户姓名邮箱状态管理员权限操作
    {{ user.username }}{{ user.fullname|default:"-" }}{{ user.email|default:"-" }} - - {{ user.get_status_display }} - - - {% if user.is_admin %} - 管理员 - {% else %} - 普通用户 - {% endif %} - - {% if user.is_admin %} - - 撤销管理员 - - {% else %} - - 授予管理员 - - {% endif %} -
    -
    - {% else %} -

    此产品暂无用户

    - {% endif %} - {% endwith %} -
    -
    - {% endfor %} - {% else %} -
    -

    暂无关联产品

    -

    此主机尚未关联任何产品,因此没有可管理的用户权限。

    -
    - {% endif %} - - -
    -{% endblock %} \ No newline at end of file diff --git a/templates/admin_base/audit/auditlog_detail.html b/templates/admin_base/audit/auditlog_detail.html deleted file mode 100644 index 595544c..0000000 --- a/templates/admin_base/audit/auditlog_detail.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 审计日志详情{% endblock %} - -{% block breadcrumb %} -审计日志 -chevron_right -日志详情 #{{ log.pk }} -{% endblock %} - -{% block content %} -
    -
    -

    审计日志详情

    -

    此页面为只读

    -
    - - arrow_back - 返回列表 - -
    - -
    - -
    -
    - 操作类型 - -
    -
    - 操作用户 - - {% if log.user %}{{ log.user.username }}{% else %}-{% endif %} - -
    -
    - IP 地址 - - {% if log.ip_address %}{{ log.ip_address }}{% else %}-{% endif %} - -
    -
    - 操作时间 - {{ log.timestamp|date:"Y-m-d H:i:s" }} -
    -
    - 是否成功 - {% if log.success %} - - {% else %} - - {% endif %} -
    -
    -
    - - -
    -
    - 关联主机 - - {% if log.host %}{{ log.host.name }} ({{ log.host.hostname }}){% else %}-{% endif %} - -
    - {% if log.content_type %} -
    - 关联对象 - - {{ log.content_type }} #{{ log.object_id }} - -
    - {% endif %} - {% if log.result %} -
    - 操作结果 - {{ log.result }} -
    - {% endif %} -
    -
    -
    - -{% if log.details %} - -
    {{ log.details|pprint }}
    -
    -{% endif %} -{% endblock %} diff --git a/templates/admin_base/audit/auditlog_list.html b/templates/admin_base/audit/auditlog_list.html deleted file mode 100644 index e4e3255..0000000 --- a/templates/admin_base/audit/auditlog_list.html +++ /dev/null @@ -1,158 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 审计日志{% endblock %} - -{% block breadcrumb %} -审计日志 -chevron_right -日志列表 -{% endblock %} - -{% block content %} -
    -
    -

    审计日志

    -

    查看系统操作记录,此页面为只读

    -
    -
    - - -
    -
    -
    - search - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - 筛选 - -
    -
    - -{% if page_obj %} -
    - - -
    - {% for log in page_obj %} - -
    -
    -
    - {% if log.action == 'create' %} - add_circle - {% elif log.action == 'update' %} - edit - {% elif log.action == 'delete' %} - delete - {% elif log.action == 'login' %} - login - {% elif log.action == 'logout' %} - logout - {% else %} - history - {% endif %} -
    -
    - -
    -
    - - {% if log.success %} - - {% else %} - - {% endif %} - {{ log.timestamp|date:"Y-m-d H:i:s" }} -
    -
    - {% if log.user %} - - person - {{ log.user.username }} - - {% else %} - 系统 - {% endif %} - {% if log.host %} - - · - - dns - {{ log.host.name }} - - - {% endif %} - {% if log.ip_address %} - - · - {{ log.ip_address }} - - {% endif %} -
    - {% if log.description %} -

    {{ log.description|truncatechars:120 }}

    - {% endif %} -
    - - -
    -
    - {% endfor %} -
    -
    - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/base.html b/templates/admin_base/base.html deleted file mode 100644 index 6fb6857..0000000 --- a/templates/admin_base/base.html +++ /dev/null @@ -1,447 +0,0 @@ -{% load static %} -{% load plugin_extensions %} - - - - - - - - {% block title %} - {% block page_title %}{{ site_name }}{% endblock %} - {% endblock %} - - - - - - {% block extra_css %}{% endblock %} - - -
    - Background -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    -
    -
    - {% if messages %} -
    - {% for message in messages %} -
    -
    - {% if message.tags == 'success' %} - check_circle - {% elif message.tags == 'error' %} - error - {% elif message.tags == 'warning' %} - warning - {% else %} - info - {% endif %} - {{ message }} -
    - -
    - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    -
    - - {% block extra_js %}{% endblock %} - - diff --git a/templates/admin_base/dashboard.html b/templates/admin_base/dashboard.html deleted file mode 100644 index 72868eb..0000000 --- a/templates/admin_base/dashboard.html +++ /dev/null @@ -1,177 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 指挥中心{% endblock %} - -{% block breadcrumb %} -超管后台 -chevron_right -指挥中心 -{% endblock %} - -{% block content %} - -
    -

    指挥中心

    -

    欢迎回来,{{ user.username }},以下是您需要关注的事项和系统状态

    -
    - - - - - -
    - -
    - - {% if attention_items %} -
    - {% for item in attention_items %} -
    -
    -
    - {{ item.icon }} -
    - {{ item.description }} -
    - - {{ item.action_label }} - arrow_forward - -
    - {% endfor %} -
    - {% else %} -
    -
    - check_circle -
    -

    一切正常

    -

    当前没有需要关注的事项

    -
    - {% endif %} -
    -
    - - -
    - -
    - -
    -
    - dns - 主机 -
    -
    - - - {{ system_health.online_hosts }} 在线 - - / - - - {{ system_health.offline_hosts }} 离线 - -
    -
    - - -
    -
    - link - 隧道 -
    -
    - - - {{ system_health.active_tunnels }} 活跃 - - {% if system_health.inactive_tunnels > 0 %} - / - - - {{ system_health.inactive_tunnels }} 异常 - - {% endif %} -
    -
    - -
    - - -
    -
    - person - 活跃用户 -
    - {{ system_health.active_users }} -
    - - -
    -
    - cloud - 产品数 -
    - {{ system_health.active_products }} -
    -
    -
    -
    -
    - - - - {% if recent_activities %} -
    - {% for activity in recent_activities %} -
    - -
    - {{ activity.icon }} -
    - -
    -
    -

    {{ activity.description }}

    - {{ activity.actor }} -
    -

    {{ activity.timestamp|date:"m-d H:i" }}

    -
    -
    - {% endfor %} -
    - {% else %} -
    - event_note -

    暂无最近动态

    -
    - {% endif %} - - {% if recent_activities %} - - {% endif %} -
    -{% endblock %} diff --git a/templates/admin_base/dashboard/systemconfig_edit.html b/templates/admin_base/dashboard/systemconfig_edit.html deleted file mode 100644 index 7b3aedc..0000000 --- a/templates/admin_base/dashboard/systemconfig_edit.html +++ /dev/null @@ -1,315 +0,0 @@ -{% extends "admin_base/base.html" %} -{% load plugin_extensions %} -{% block title %}{{ site_name }} 超级管理员 - 系统配置{% endblock %} -{% block breadcrumb %}系统配置{% endblock %} -{% block content %} - -
    -
    -

    系统配置

    -

    管理系统全局配置(单例模式)

    -
    -
    - - -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %}

    {{ error }}

    {% endfor %} -
    - {% endif %} - -
    -

    - info - 基本信息 -

    -
    - {% with field=form.site_name %}{% endwith %} -
    - -
    - - -
    - {% if form.enable_registration.help_text %} -

    {{ form.enable_registration.help_text }}

    - {% endif %} -
    -
    -
    - -
    -

    - gavel - 备案信息 -

    -
    - {% with field=form.icp_number %} - - {% endwith %} - {% with field=form.police_number %} - - {% endwith %} -
    -
    - -
    -

    - mail - SMTP 配置 -

    -
    - {% with field=form.smtp_host %} - - {% endwith %} - {% with field=form.smtp_port %} - - {% endwith %} - {% with field=form.smtp_encryption %} - - {% for value, label in form.fields.smtp_encryption.choices %} - - {% endfor %} - - {% endwith %} - {% with field=form.smtp_username %}{% endwith %} - {% with field=form.smtp_password %} - - {% endwith %} - {% with field=form.smtp_from_email %} - - {% endwith %} - {% with field=form.smtp_from_name %}{% endwith %} -
    -
    - -
    -

    - verified_user - 验证码设置 -

    -
    - {% with field=form.captcha_provider %} - - {% for value, label in form.fields.captcha_provider.choices %} - - {% endfor %} - - {% endwith %} - {% with field=form.captcha_type %} - - {% for value, label in form.fields.captcha_type.choices %} - - {% endfor %} - - {% endwith %} -
    -

    场景级别类型留空则使用默认类型

    -
    - {% with field=form.login_captcha_type %} - - - {% for value, label in form.fields.login_captcha_type.choices %} - - {% endfor %} - - {% endwith %} - {% with field=form.register_captcha_type %} - - - {% for value, label in form.fields.register_captcha_type.choices %} - - {% endfor %} - - {% endwith %} - {% with field=form.email_captcha_type %} - - - {% for value, label in form.fields.email_captcha_type.choices %} - - {% endfor %} - - {% endwith %} -
    -
    - -
    -

    - alternate_email - 邮箱后缀配置 -

    -

    白名单和黑名单同时生效时,白名单优先;两者都留空则不限制邮箱后缀。

    -
    -
    - - - {% if form.email_suffix_whitelist.help_text %} -

    {{ form.email_suffix_whitelist.help_text }}

    - {% endif %} - {% if form.email_suffix_whitelist.errors %} -
    - {% for error in form.email_suffix_whitelist.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - - {% if form.email_suffix_blacklist.help_text %} -

    {{ form.email_suffix_blacklist.help_text }}

    - {% endif %} - {% if form.email_suffix_blacklist.errors %} -
    - {% for error in form.email_suffix_blacklist.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - -
    -

    - lock - 安全设置 -

    -
    - -
    - - -
    - {% if form.local_access_locked.help_text %} -

    {{ form.local_access_locked.help_text }}

    - {% endif %} -
    -
    - -
    -

    - language - 主机名品牌绑定 -

    -

    为不同的访问主机名绑定专用的站点名称和图标。未配置的主机名将使用上方"站点名称"的全局默认值和默认图标。

    -
    - - {{ form.hostname_branding }} - {% if form.hostname_branding.help_text %} -

    {{ form.hostname_branding.help_text }}

    - {% endif %} - {% if form.hostname_branding.errors %} -
    - {% for error in form.hostname_branding.errors %}

    {{ error }}

    {% endfor %} -
    - {% endif %} -
    -
    - {% plugin_extensions "system_config_after_sections" %} - -
    - -
    - - - -
    - - 保存配置 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/dashboard/test_email_progress.html b/templates/admin_base/dashboard/test_email_progress.html deleted file mode 100644 index ce7e694..0000000 --- a/templates/admin_base/dashboard/test_email_progress.html +++ /dev/null @@ -1,171 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} - 测试邮件发送中{% endblock %} - -{% block breadcrumb %} -系统配置 -chevron_right -测试邮件 -{% endblock %} - -{% block content %} -
    - - -
    - - - - - - - - - - - -
    - - -
    -
    - terminal - 调试日志 -
    -
    - - -
    -
    -
    -
    - - -{% endblock %} diff --git a/templates/admin_base/dashboard/widget_confirm_delete.html b/templates/admin_base/dashboard/widget_confirm_delete.html deleted file mode 100644 index 037bf15..0000000 --- a/templates/admin_base/dashboard/widget_confirm_delete.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除仪表盘组件{% endblock %} - -{% block breadcrumb %} -仪表盘组件 -chevron_right -删除组件 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下仪表盘组件吗?

    -
    -
    - 标题 - {{ widget.title }} -
    -
    - 组件类型 - -
    -
    - 显示顺序 - {{ widget.display_order }} -
    -
    -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/dashboard/widget_form.html b/templates/admin_base/dashboard/widget_form.html deleted file mode 100644 index 589b9d1..0000000 --- a/templates/admin_base/dashboard/widget_form.html +++ /dev/null @@ -1,160 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}仪表盘组件{% endblock %} - -{% block breadcrumb %} -仪表盘组件 -chevron_right -{% if is_create %}创建组件{% else %}编辑组件{% endif %} -{% endblock %} - -{% block content %} -
    -
    -

    {% if is_create %}创建仪表盘组件{% else %}编辑仪表盘组件{% endif %}

    -

    - {% if is_create %}填写以下信息创建新组件{% else %}修改组件「{{ widget.title }}」的信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    -
    - - - {% if form.widget_type.errors %} -
    - {% for error in form.widget_type.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.title.errors %} -
    - {% for error in form.title.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.display_order.errors %} -
    - {% for error in form.display_order.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - -
    - {% if form.is_enabled.help_text %} -

    {{ form.is_enabled.help_text }}

    - {% endif %} -
    -
    - -
    - - - {% if form.widget_config.help_text %} -

    {{ form.widget_config.help_text }}

    - {% endif %} - {% if form.widget_config.errors %} -
    - {% for error in form.widget_config.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/dashboard/widget_list.html b/templates/admin_base/dashboard/widget_list.html deleted file mode 100644 index 3c63584..0000000 --- a/templates/admin_base/dashboard/widget_list.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 仪表盘组件{% endblock %} - -{% block breadcrumb %} -仪表盘组件 -chevron_right -组件列表 -{% endblock %} - -{% block content %} -
    -
    -

    仪表盘组件

    -

    管理系统仪表盘上的组件配置

    -
    - - 添加组件 - -
    - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - -{% if page_obj %} - - {% for widget in page_obj %} - - - {{ widget.title }} - - - - - {{ widget.display_order }} - - {% if widget.is_enabled %} - - {% else %} - - {% endif %} - - {{ widget.created_at|date:"Y-m-d H:i" }} - - - - - {% endfor %} - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 添加组件 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/groups/group_confirm_delete.html b/templates/admin_base/groups/group_confirm_delete.html deleted file mode 100644 index f2baae8..0000000 --- a/templates/admin_base/groups/group_confirm_delete.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除用户组{% endblock %} - -{% block breadcrumb %} -用户组管理 -chevron_right -删除用户组 -{% endblock %} - -{% block content %} -
    - -
    -
    - warning -
    -

    确认删除用户组

    -

    - 您确定要删除用户组「{{ group_profile.group.name }}」吗?此操作不可撤销。 -

    - - {% if group_profile.group.user_set.exists %} -
    -
    - group - 该用户组下仍有成员 -
    -

    - 删除后,以下 {{ group_profile.group.user_set.count }} 名用户将从该组移除: -

    -
    - {% for user in group_profile.group.user_set.all %} - - {{ user.username }} - - {% endfor %} -
    -
    - {% endif %} - -
    - {% csrf_token %} - - 取消 - - - 确认删除 - -
    -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/groups/group_form.html b/templates/admin_base/groups/group_form.html deleted file mode 100644 index 282011c..0000000 --- a/templates/admin_base/groups/group_form.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}用户组{% endblock %} - -{% block breadcrumb %} -用户组管理 -chevron_right -{% if is_create %}创建用户组{% else %}编辑用户组{% endif %} -{% endblock %} - -{% block content %} -
    -
    -

    {% if is_create %}创建用户组{% else %}编辑用户组{% endif %}

    -

    - {% if is_create %}填写以下信息创建新的用户组{% else %}修改用户组「{{ group_profile.group.name }}」的信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - -
    - {% csrf_token %} - -
    -

    - group - 基本信息 -

    -
    -
    - - -
    -
    - - -

    数值越小排序越靠前,默认为0

    -
    -
    -
    - -
    -

    - description - 描述 -

    -
    - - -
    -
    - -
    -

    - badge - 员工身份 -

    - -
    - - {% if not is_create and group_profile.is_default %} -
    -
    - info - 此用户组为系统默认组,不可删除 -
    -
    - {% endif %} - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/groups/group_list.html b/templates/admin_base/groups/group_list.html deleted file mode 100644 index 1bb5922..0000000 --- a/templates/admin_base/groups/group_list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 用户组管理{% endblock %} - -{% block breadcrumb %} -用户组管理 -chevron_right -用户组列表 -{% endblock %} - -{% block content %} -
    -
    -

    用户组管理

    -

    管理系统中的用户组与权限角色

    -
    - - 创建用户组 - -
    - -{% if group_profiles %} -
    - {% for gp in group_profiles %} - -
    -
    -
    - {% if gp.group.name == '超管' %} - shield - {% elif gp.group.name == '主机提供商' %} - dns - {% elif gp.group.name == '云电脑审批' %} - how_to_reg - {% elif gp.group.name == '工单技术客服' %} - support_agent - {% elif gp.group.name == '普通用户' %} - person - {% else %} - group - {% endif %} -
    -
    -
    - {{ gp.group.name }} - {% if gp.is_default %} - - {% endif %} - {% if gp.auto_staff %} - - {% endif %} - - {{ gp.group.user_set.count }} 名成员 - -
    - {% if gp.description %} -

    {{ gp.description }}

    - {% endif %} -
    -
    - -
    - - edit - - {% if not gp.is_default %} - - delete - - {% else %} - - lock - - {% endif %} -
    -
    -
    - {% endfor %} -
    -{% else %} - - - 创建用户组 - - -{% endif %} - -{% if unprofiled_groups %} -
    -

    未配置的用户组

    -
    - {% for group in unprofiled_groups %} - -
    -
    -
    - group -
    -
    - {{ group.name }} - {{ group.user_set.count }} 名成员 -
    -
    - 未配置描述 -
    -
    - {% endfor %} -
    -
    -{% endif %} -{% endblock %} diff --git a/templates/admin_base/hosts/host_confirm_delete.html b/templates/admin_base/hosts/host_confirm_delete.html deleted file mode 100644 index af08c9c..0000000 --- a/templates/admin_base/hosts/host_confirm_delete.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除主机{% endblock %} - -{% block breadcrumb %} -主机管理 -chevron_right -删除主机 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下主机吗?

    -
    -
    - 主机名称 - {{ host.name }} -
    -
    - 主机地址 - {{ host.hostname }}:{{ host.port }} -
    -
    - 连接类型 - -
    -
    - 状态 - {% if host.status == 'online' %} - - {% elif host.status == 'error' %} - - {% else %} - - {% endif %} -
    - {% if product_count > 0 %} -
    - 关联产品 - -
    - {% endif %} - {% if host.providers.exists %} -
    - 提供商 -
    - {% for provider in host.providers.all %} - - {% endfor %} -
    -
    - {% endif %} -
    - {% if product_count > 0 %} -
    -
    - error -

    该主机关联了 {{ product_count }} 个产品,删除后关联产品也将被一并删除!

    -
    -
    - {% endif %} -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/hosts/host_detail.html b/templates/admin_base/hosts/host_detail.html deleted file mode 100644 index de2d817..0000000 --- a/templates/admin_base/hosts/host_detail.html +++ /dev/null @@ -1,497 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {{ host.name }}{% endblock %} - -{% block breadcrumb %} -主机管理 -chevron_right -{{ host.name }} -{% endblock %} - -{% block content %} -
    - - - - -
    -
    -

    {{ host.name }}

    -

    {{ host.hostname }}:{{ host.port }}

    -
    - -
    - - -{% if generated_password %} - -
    - key -
    -

    自动生成的密码

    -

    {{ generated_password }}

    -

    请妥善保存,此密码仅显示一次

    -
    -
    -
    -{% endif %} - - - -
    -
    -

    主机名称

    -

    {{ host.name }}

    -
    -
    -

    主机地址

    -

    {{ host.hostname }}

    -
    -
    -

    连接类型

    - -
    -
    -

    连接端口

    -

    {{ host.port }}

    -
    -
    -

    RDP端口

    -

    {{ host.rdp_port }}

    -
    -
    -

    使用SSL

    - {% if host.use_ssl %} - - {% else %} - - {% endif %} -
    -
    -

    用户名

    -

    {{ host.username }}

    -
    -
    -

    操作系统

    -

    {{ host.os_version|default:"-" }}

    -
    -
    -

    状态

    - - - -
    -
    -

    创建者

    -

    {{ host.created_by|default:"-" }}

    -
    -
    -

    创建时间

    -

    {{ host.created_at|date:"Y-m-d H:i" }}

    -
    -
    -

    更新时间

    -

    {{ host.updated_at|date:"Y-m-d H:i" }}

    -
    -
    - - {% if host.description %} -
    -

    描述

    -

    {{ host.description }}

    -
    - {% endif %} -
    - - -{% if host.connection_type == 'tunnel' %} - -
    -
    -

    隧道状态

    - {% if host.tunnel_status == 'online' %} - - {% elif host.tunnel_status == 'error' %} - - {% elif host.tunnel_status == 'offline' %} - - {% else %} - - {% endif %} -
    -
    -

    客户端版本

    -

    {{ host.tunnel_client_version|default:"-" }}

    -
    -
    -

    客户端IP

    -

    {{ host.tunnel_client_ip|default:"-" }}

    -
    -
    -

    连接时间

    -

    {{ host.tunnel_connected_at|date:"Y-m-d H:i"|default:"-" }}

    -
    -
    -

    最后心跳

    -

    {{ host.tunnel_last_seen_at|date:"Y-m-d H:i"|default:"-" }}

    -
    -
    -
    -{% endif %} - - - - {% if host.providers.all %} -
    - {% for provider in host.providers.all %} -
    -
    - person -
    -
    -

    {{ provider.username }}

    -

    {{ provider.email|default:"-" }}

    -
    -
    - {% endfor %} -
    - {% else %} -

    暂未分配提供商

    - {% endif %} -
    - - - - {% if host.administrators.all %} -
    - {% for admin in host.administrators.all %} -
    -
    - person -
    -
    -

    {{ admin.username }}

    -

    {{ admin.email|default:"-" }}

    -
    -
    - {% endfor %} -
    - {% else %} -

    暂未分配管理员

    - {% endif %} -
    - - - - {% if products %} -
    - - - - - - - - - {% for product in products %} - - - - - {% endfor %} - -
    产品名称用户数
    {{ product.name }}{{ product.user_count|default:0 }}
    -
    - {% else %} -

    暂无关联产品

    - {% endif %} -
    - - -{% if host.auth_method == 'certificate' %} - -
    -

    - 在目标主机上以管理员权限运行以下命令,自动完成 H 端初始化和证书配置。 -

    - - - - - - -
    -
    -{% endif %} -
    - - -{% endblock %} diff --git a/templates/admin_base/hosts/host_form.html b/templates/admin_base/hosts/host_form.html deleted file mode 100644 index f32b1f6..0000000 --- a/templates/admin_base/hosts/host_form.html +++ /dev/null @@ -1,738 +0,0 @@ -{% extends "admin_base/base.html" %} -{% load plugin_extensions %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}主机{% endblock %} - -{% block breadcrumb %} -主机管理 -chevron_right -{% if is_create %}创建主机{% else %}编辑主机{% endif %} -{% endblock %} - -{% block content %} -
    -
    -
    -

    {% if is_create %}创建主机{% else %}编辑主机{% endif %}

    -

    - {% if is_create %}填写以下信息创建新主机{% else %}修改主机「{{ host.name }}」的信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    -

    - dns - 基本信息 -

    -
    -
    - - - {% if form.name.errors %} -
    - {% for error in form.name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - expand_more -
    - {% if form.os_type.errors %} -
    - {% for error in form.os_type.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.hostname.errors %} -
    - {% for error in form.hostname.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - -
    - - {% if form.connection_type.errors %} -
    - {% for error in form.connection_type.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - -
    -

    - settings_ethernet - 连接配置 -

    -
    -
    -
    - - -

    - {% if form.port.errors %} -
    - {% for error in form.port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.rdp_port.errors %} -
    - {% for error in form.rdp_port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - -
    - - - (端口5986通常需要SSL) -
    - -
    - -
    - - -
    - -
    - -
    - -
    - -
    -
    - - - {% if form.username.errors %} -
    - {% for error in form.username.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - -
    - {% if host %}

    留空则不修改密码

    {% endif %} - {% if form.password.errors %} -
    - {% for error in form.password.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - -
    -
    - -
    - - -
    -
    - -
    - -
    -

    1. 复制下方PowerShell脚本

    -

    2. 在目标主机上以管理员权限运行

    -

    3. 脚本将自动配置WinRM证书认证并导出证书文件

    -

    4. 将导出的证书文件上传到下方

    -
    -
    -
    -
    - - -
    -
    -
    - terminal - PowerShell -
    -
    - -
    -
    -
    -
    - -
    - -
    -

    请上传PEM格式的客户端证书和私钥文件

    -

    证书文件需包含公钥,私钥文件需包含完整的私钥数据

    -
    -
    -
    - -
    -
    - - -

    - {% if host and host.cert_pem_path %} -

    当前已存储证书文件

    - {% endif %} - {% if form.cert_pem.errors %} -
    - {% for error in form.cert_pem.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - -

    - {% if host and host.cert_key_path %} -

    当前已存储私钥文件

    - {% endif %} - {% if form.cert_key.errors %} -
    - {% for error in form.cert_key.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    -
    -
    - - {% plugin_extensions "host_form_after_auth" %} - -
    -

    - group - 提供商分配 -

    - -
    -
    - - -
    -
    - - - -
    - -
    - -
    - - - - - - - - - - - - - - -
    类型名称操作
    - person_off -

    暂未分配提供商,请使用上方表单添加

    -
    -
    - - -
    - - {% plugin_extensions "host_form_after_providers" %} - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/admin_base/hosts/host_list.html b/templates/admin_base/hosts/host_list.html deleted file mode 100644 index 6648008..0000000 --- a/templates/admin_base/hosts/host_list.html +++ /dev/null @@ -1,176 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 主机管理{% endblock %} - -{% block breadcrumb %} -主机管理 -chevron_right -主机列表 -{% endblock %} - -{% block content %} - -
    -
    -

    主机管理

    -

    管理所有主机,无数据隔离

    -
    - -
    - - - -
    -
    -
    - search - -
    -
    -
    - -
    -
    - -
    - - 筛选 - -
    -
    - - -{% if connection_type_filter or status_filter %} - -{% endif %} - - -{% if page_obj %} -
    - {% for host in page_obj %} - -
    - -
    - -
    - {% if host.status == 'online' %} - dns - {% elif host.status == 'error' %} - dns - {% else %} - dns - {% endif %} -
    - -
    -
    - - {{ host.name }} - - {% if host.status == 'online' %} - - {% elif host.status == 'error' %} - - {% else %} - - {% endif %} -
    -

    - {{ host.hostname }}·{{ host.get_connection_type_display }}·:{{ host.port }} - {% if host.use_ssl %}·SSL{% endif %} -

    - -
    - {% for provider in host.providers.all %} - - person - {{ provider.username }} - - {% empty %} - {% if not host.providers.exists %} - 未分配提供商 - [分配] - {% endif %} - {% endfor %} -
    -
    -
    - - - -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 添加主机 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/hosts/host_wizard.html b/templates/admin_base/hosts/host_wizard.html deleted file mode 100644 index 7beb8f8..0000000 --- a/templates/admin_base/hosts/host_wizard.html +++ /dev/null @@ -1,1100 +0,0 @@ -{% extends "admin_base/base.html" %} -{% load plugin_extensions %} - -{% block title %}{{ site_name }} 超级管理员 - 添加主机向导{% endblock %} - -{% block breadcrumb %} -主机管理 -chevron_right -添加主机 -{% endblock %} - -{% block content %} -
    - -
    -
    -

    添加主机

    -

    按照步骤引导创建新主机

    -
    - - arrow_back - 返回列表 - -
    - -
    - -
    - -
    -
    - info
    基本信息 -
    -
    - settings_ethernet
    连接配置 -
    -
    - admin_panel_settings
    分配提供商 -
    -
    - -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - - {% for error in form.non_field_errors %}{{ error }}{% endfor %} - -
    - {% endif %} - - -
    - -
    -
    - - - {% if form.name.errors %} -
    - {% for error in form.name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - expand_more -
    - {% if form.os_type.errors %} -
    - {% for error in form.os_type.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.hostname.errors %} -
    - {% for error in form.hostname.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - -
    - - {% if form.connection_type.errors %} -
    - {% for error in form.connection_type.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    -
    - - -
    - -
    -
    -
    - - -

    - {% if form.port.errors %} -
    - {% for error in form.port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.rdp_port.errors %} -
    - {% for error in form.rdp_port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - -
    - - - (端口5986通常需要SSL) -
    - -
    - -
    - - -
    -
    - -
    -
    - - - - -
    -
    - - - {% if form.username.errors %} -
    - {% for error in form.username.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - - -
    - {% if form.password.errors %} -
    - {% for error in form.password.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -
    - -
    - - -
    - -
    - - -
    - -
    -

    点击下方按钮生成初始化命令,在目标主机上以管理员权限运行即可。

    -
    -
    - - - - - - -
    - - -
    - -
    -

    请上传PEM格式的客户端证书和私钥文件

    -
    -
    -
    - - -

    - {% if form.cert_pem.errors %} -
    - {% for error in form.cert_pem.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -

    - {% if form.cert_key.errors %} -
    - {% for error in form.cert_key.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - -
    - -
    - - - -
    -
    - - - -
    -
    - - -
    -
    - progress_activity - 等待目标主机连接... -
    -
    - progress_activity - H 端已连接,正在上传证书并测试连接... -
    -
    - -
    - check_circle - 证书上传成功!保存主机后将完成配置 -
    -
    -
    -
    - -
    - check_circle - H 端初始化成功!目标主机已完成配置 -
    -
    -
    -
    - - 等待超时,目标主机未在有效时间内完成初始化。请确认命令已在目标主机上运行。 - -
    -
    - - - -
    -
    -
    -
    -
    - - -
    - -
    -
    - -

    选择可以管理此主机的提供商用户

    - - {% if providers_with_count %} -
    - {% for provider in providers_with_count %} - - {% endfor %} -
    - {% else %} -
    - person_off -

    暂无可用的提供商用户

    -

    请先在用户管理中创建提供商用户

    -
    - {% endif %} - - {% if form.providers.errors %} -
    - {% for error in form.providers.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - -
    - -
    -

    - summarize - 创建预览 -

    -
    -
    - 主机名称 - -
    -
    - 主机系统 - -
    -
    - 主机地址 - -
    -
    - 连接类型 - -
    -
    - 端口 - -
    -
    - SSL - -
    -
    - 连接方式 - -
    - - -
    - 提供商 - -
    -
    -
    -
    -
    -
    - - -
    - -
    - - - - -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/templates/admin_base/hosts/hostgroup_confirm_delete.html b/templates/admin_base/hosts/hostgroup_confirm_delete.html deleted file mode 100644 index 203bcb8..0000000 --- a/templates/admin_base/hosts/hostgroup_confirm_delete.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除主机组{% endblock %} - -{% block breadcrumb %} -主机组管理 -chevron_right -删除主机组 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下主机组吗?

    -
    -
    - 组名称 - {{ hostgroup.name }} -
    -
    - 描述 - {{ hostgroup.description|default:"-" }} -
    -
    - 主机数量 - -
    - {% if hostgroup.providers.exists %} -
    - 提供商 -
    - {% for provider in hostgroup.providers.all %} - - {% endfor %} -
    -
    - {% endif %} -
    - {% if host_count > 0 %} -
    -
    - error -

    该主机组包含 {{ host_count }} 台主机,删除主机组不会删除其中的主机。

    -
    -
    - {% endif %} -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/hosts/hostgroup_form.html b/templates/admin_base/hosts/hostgroup_form.html deleted file mode 100644 index ff8b85c..0000000 --- a/templates/admin_base/hosts/hostgroup_form.html +++ /dev/null @@ -1,137 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}主机组{% endblock %} - -{% block breadcrumb %} -主机组管理 -chevron_right -{% if is_create %}创建主机组{% else %}编辑主机组{% endif %} -{% endblock %} - -{% block content %} - -
    -
    -

    {% if is_create %}创建主机组{% else %}编辑主机组{% endif %}

    -

    - {% if is_create %}填写以下信息创建新主机组{% else %}修改主机组「{{ hostgroup.name }}」的信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - -
    -

    - folder - 基本信息 -

    -
    - {% with field=form.name %} - - {% endwith %} - -
    - {% with field=form.description %} - - {% endwith %} -
    -
    -
    - - -
    -

    - group - 成员分配 -

    -
    - -
    - - - {% if form.hosts.help_text %} -

    {{ form.hosts.help_text }}

    - {% endif %} - {% if form.hosts.errors %} -
    - {% for error in form.hosts.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - - {% if form.providers.help_text %} -

    {{ form.providers.help_text }}

    - {% endif %} - {% if form.providers.errors %} -
    - {% for error in form.providers.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/hosts/hostgroup_list.html b/templates/admin_base/hosts/hostgroup_list.html deleted file mode 100644 index 46f8b92..0000000 --- a/templates/admin_base/hosts/hostgroup_list.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 主机组管理{% endblock %} - -{% block breadcrumb %} -主机组管理 -chevron_right -主机组列表 -{% endblock %} - -{% block content %} - -
    -
    -

    主机组管理

    -

    管理所有主机组,无数据隔离

    -
    - -
    - - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - - -{% if page_obj %} - - {% for hostgroup in page_obj %} - - - {{ hostgroup.name }} - - - {{ hostgroup.description|default:"-" }} - - - - - -
    - {% for provider in hostgroup.providers.all %} - - {% empty %} - - - {% endfor %} -
    - - - - - - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 创建主机组 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/grant_list.html b/templates/admin_base/operations/grant_list.html deleted file mode 100644 index a0f06df..0000000 --- a/templates/admin_base/operations/grant_list.html +++ /dev/null @@ -1,131 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 访问授权{% endblock %} - -{% block breadcrumb %} -{% if is_provider %} -访问授权 -{% else %} -访问授权 -{% endif %} -chevron_right -授权列表 -{% endblock %} - -{% block content %} - -
    -
    -

    访问授权

    -

    查看所有产品访问授权记录

    -
    -
    - - - -
    -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - - -{% if page_obj %} -
    - {% for grant in page_obj %} - -
    - -
    - -
    - {% if grant.is_revoked %} - shield_off - {% elif grant.is_expired %} - shield - {% else %} - shield - {% endif %} -
    - -
    -
    - {{ grant.user.username }} - - → - {% if grant.product %} - {{ grant.product.display_name }} - {% elif grant.product_group %} - {{ grant.product_group.name }} - {% else %} - -- - {% endif %} - - {% if grant.is_revoked %} - - {% elif grant.is_expired %} - - {% else %} - - {% endif %} -
    -

    - schedule - 授权时间: {{ grant.granted_at|date:"Y-m-d H:i" }} - · - 过期时间: - {% if grant.expires_at %} - {% if grant.is_expired %} - {{ grant.expires_at|date:"Y-m-d H:i" }} - {% else %} - {{ grant.expires_at|date:"Y-m-d H:i" }} - {% endif %} - {% else %} - 永久 - {% endif %} -

    - {% if grant.granted_by_token %} -

    - key - 来源令牌: {{ grant.granted_by_token.token|truncatechars:12 }} -

    - {% endif %} - {% if grant.product_group %} -
    - - folder - {{ grant.product_group.name }} - -
    - {% endif %} -
    -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/product_confirm_delete.html b/templates/admin_base/operations/product_confirm_delete.html deleted file mode 100644 index f5732b4..0000000 --- a/templates/admin_base/operations/product_confirm_delete.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除产品 {{ product.display_name }}{% endblock %} - -{% block breadcrumb %} -产品管理 -chevron_right -删除产品 -{% endblock %} - -{% block content %} - -
    -

    删除产品

    -

    此操作不可撤销,请谨慎确认

    -
    - - - - 您即将删除产品 {{ product.display_name }}。 - {% if user_count > 0 %} -
    该产品下还有 {{ user_count }} 个关联用户,删除产品将同时删除这些用户及关联数据。 - {% endif %} -
    此操作不可撤销。 -
    - - - -
    -
    -

    产品名称

    -

    {{ product.display_name }}

    -
    -
    -

    关联主机

    -

    {{ product.host.name }}

    -
    -
    -

    产品组

    -

    {{ product.product_group.name|default:"未分组" }}

    -
    -
    -

    创建者

    -

    - {% if product.created_by %}{{ product.created_by.username }}{% else %}-{% endif %} -

    -
    -
    -

    关联用户数

    -

    {{ user_count }}

    -
    -
    -

    可见性

    -

    {{ product.get_visibility_display }}

    -
    -
    -

    创建时间

    -

    {{ product.created_at|date:"Y-m-d H:i:s" }}

    -
    -
    -
    - - -
    - {% csrf_token %} -
    - - 返回列表 - - 确认删除 -
    -
    -{% endblock %} diff --git a/templates/admin_base/operations/product_form.html b/templates/admin_base/operations/product_form.html deleted file mode 100644 index 9f0bd74..0000000 --- a/templates/admin_base/operations/product_form.html +++ /dev/null @@ -1,493 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建产品{% else %}编辑产品{% endif %}{% endblock %} - -{% block breadcrumb %} -产品管理 -chevron_right -{% if is_create %}创建产品{% else %}编辑产品{% endif %} -{% endblock %} - -{% block content %} -
    -
    -

    {% if is_create %}创建产品{% else %}编辑产品{% endif %}

    -

    - {% if is_create %}填写以下信息创建新的云电脑产品{% else %}修改产品 {{ product.display_name }} 的信息{% endif %} -

    -
    - - -
    - {% csrf_token %} - - -
    -

    - info - 基本信息 -

    -
    -
    - - {{ form.display_name }} - {% if form.display_name.errors %} -
    - {% for error in form.display_name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - {{ form.product_group }} - expand_more -
    - {% if form.product_group.errors %} -
    - {% for error in form.product_group.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - -
    - - {{ form.display_description }} - {% if form.display_description.errors %} -
    - {% for error in form.display_description.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -

    - dns - 主机关联与状态 -

    -
    -
    - -
    - - expand_more -
    - {% if form.host.help_text %} -

    {{ form.host.help_text }}

    - {% endif %} - {% if form.host.errors %} -
    - {% for error in form.host.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - -
    - {{ form.visibility }} - expand_more -
    - {% if form.visibility.help_text %} -

    {{ form.visibility.help_text }}

    - {% endif %} - {% if form.visibility.errors %} -
    - {% for error in form.visibility.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.is_available.errors %} -
    - {% for error in form.is_available.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.auto_approval.errors %} -
    - {% for error in form.auto_approval.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - -
    -

    - shield - 主机保护 -

    -
    - - {% if form.enable_host_protection.errors %} -
    - {% for error in form.enable_host_protection.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -

    - monitor - 显示配置 -

    -
    -
    - - {{ form.display_hostname }} - {% if form.display_hostname.errors %} -
    - {% for error in form.display_hostname.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - {{ form.rdp_port }} - {% if form.rdp_port.errors %} -
    - {% for error in form.rdp_port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - -
    -

    - hard_disk - 磁盘配额管理 -

    - -
    - - {% if form.enable_disk_quota.errors %} -
    - {% for error in form.enable_disk_quota.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    -
    - - 请先选择关联主机 -
    - - -
    -

    -
    - - -
    - - - - - - - - - - - - - -
    磁盘总容量配额 (MB)允许额外申请操作
    -

    配额为 0 表示不限制该磁盘容量

    -
    - - -
    - hard_disk -

    点击"扫描主机磁盘"获取磁盘列表

    -
    - - - - -
    -
    - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} -
    - {% endif %} - - -
    - - 取消 - - - {% if is_create %}创建产品{% else %}保存修改{% endif %} - -
    -
    -
    -
    - - -{% endblock %} diff --git a/templates/admin_base/operations/product_list.html b/templates/admin_base/operations/product_list.html deleted file mode 100644 index ff3330a..0000000 --- a/templates/admin_base/operations/product_list.html +++ /dev/null @@ -1,370 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 产品管理{% endblock %} - -{% block breadcrumb %} -产品管理 -chevron_right -产品列表 -{% endblock %} - -{% block content %} - -
    - - - -
    -
    -

    产品管理

    -

    管理所有云电脑产品

    -
    - - 创建产品 - -
    - - - -
    -
    -
    - search - -
    -
    -
    - -
    -
    - -
    - - 筛选 - -
    -
    - - -{% if available_filter or visibility_filter %} - -{% endif %} - - -{% if page_obj %} -
    - {% for product in page_obj %} - -
    - -
    - -
    - {% if product.is_available %} - inventory_2 - {% else %} - inventory_2 - {% endif %} -
    - -
    -
    - {{ product.display_name }} - {% if product.is_available %} - - {% else %} - - {% endif %} - {% if product.visibility == 'public' %} - - {% else %} - - {% endif %} -
    -

    - dns - {{ product.host.name }} - · - person - {% if product.created_by %}{{ product.created_by.username }}{% else %}-{% endif %} -

    - -
    - {% if product.product_group %} - - folder - {{ product.product_group.name }} - - {% endif %} - {% if product.enable_disk_quota %} - - hard_disk - 磁盘配额 - - {% endif %} - {% if product.enable_host_protection %} - - shield - 主机保护 - - {% endif %} - {% if product.auto_approval %} - - bolt - 自动审核 - - {% endif %} -
    -
    -
    - - -
    - {% if product.visibility == 'invite_only' %} - - {% endif %} - - edit - - - delete - -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 创建产品 - - -{% endif %} -
    - - -{% endblock %} diff --git a/templates/admin_base/operations/product_wizard.html b/templates/admin_base/operations/product_wizard.html deleted file mode 100644 index 72b0fbd..0000000 --- a/templates/admin_base/operations/product_wizard.html +++ /dev/null @@ -1,741 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 创建产品向导{% endblock %} - -{% block breadcrumb %} -产品管理 -chevron_right -创建产品 -{% endblock %} - -{% block content %} -
    - - -
    -
    -

    创建产品

    -

    按照步骤引导创建新的云电脑产品

    -
    - - arrow_back - 返回列表 - -
    - - -
    - -
    - - -
    -
    - info
    基本信息 -
    -
    - dns
    主机关联 -
    -
    - tune
    高级设置 -
    -
    - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - - {% for error in form.non_field_errors %}{{ error }}{% endfor %} - -
    - {% endif %} - - -
    - -
    - -
    - - - {% if form.display_name.errors %} -
    - {% for error in form.display_name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - - {% if form.display_description.errors %} -
    - {% for error in form.display_description.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - -
    - - expand_more -
    - {% if form.product_group.errors %} -
    - {% for error in form.product_group.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    -
    - - -
    - -
    - -
    - -
    - - expand_more -
    -

    此产品运行所在的主机

    - {% if form.host.errors %} -
    - {% for error in form.host.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - -
    -
    - dns - - -
    -
    -
    地址:
    -
    连接类型:
    -
    -
    -
    - - -
    -
    - - -

    用户连接时看到的地址

    - {% if form.display_hostname.errors %} -
    - {% for error in form.display_hostname.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - {% if form.rdp_port.errors %} -
    - {% for error in form.rdp_port.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    - -
    - - expand_more -
    -

    公开对所有用户可见,邀请访问仅对已授权用户可见

    - {% if form.visibility.errors %} -
    - {% for error in form.visibility.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - - -
    - -
    - -
    - - {% if form.enable_host_protection.errors %} -
    - {% for error in form.enable_host_protection.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - {% if form.enable_disk_quota.errors %} -
    - {% for error in form.enable_disk_quota.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - -
    -
    - - 请先选择关联主机 -
    - - -
    -

    -
    - - -
    - - - - - - - - - - - - - -
    磁盘总容量配额 (MB)允许额外申请操作
    -

    配额为 0 表示不限制该磁盘容量

    -
    - - -
    - hard_disk -

    点击"扫描主机磁盘"获取磁盘列表

    -
    - - - - -
    -
    - - -
    -

    - summarize - 创建预览 -

    -
    -
    - 显示名称 - -
    -
    - 产品组 - -
    -
    - 关联主机 - -
    -
    - 显示地址 - -
    -
    - RDP端口 - -
    -
    - 可见性 - -
    -
    - 是否可用 - -
    -
    - 自动审核 - -
    -
    - 主机保护 - -
    -
    - 磁盘配额 - -
    - -
    -
    -
    -
    -
    - - -
    - -
    - - - - -
    -
    -
    - - -{% endblock %} diff --git a/templates/admin_base/operations/productgroup_confirm_delete.html b/templates/admin_base/operations/productgroup_confirm_delete.html deleted file mode 100644 index 069b60b..0000000 --- a/templates/admin_base/operations/productgroup_confirm_delete.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除产品组 {{ productgroup.name }}{% endblock %} - -{% block breadcrumb %} -产品组管理 -chevron_right -删除产品组 -{% endblock %} - -{% block content %} - -
    -

    删除产品组

    -

    此操作不可撤销,请谨慎确认

    -
    - - - - 您即将删除产品组 {{ productgroup.name }}。 - {% if product_count > 0 %} -
    该产品组下还有 {{ product_count }} 个关联产品。 - {% endif %} -
    此操作不可撤销。 -
    - - - -
    -
    -

    产品组名称

    -

    {{ productgroup.name }}

    -
    -
    -

    显示顺序

    -

    {{ productgroup.display_order }}

    -
    -
    -

    可见性

    -

    {{ productgroup.get_visibility_display }}

    -
    -
    -

    创建者

    -

    - {% if productgroup.created_by %}{{ productgroup.created_by.username }}{% else %}-{% endif %} -

    -
    -
    -

    关联产品数

    -

    {{ product_count }}

    -
    -
    -

    创建时间

    -

    {{ productgroup.created_at|date:"Y-m-d H:i:s" }}

    -
    -
    -
    - - -
    - {% csrf_token %} -
    - - 返回列表 - - 确认删除 -
    -
    -{% endblock %} diff --git a/templates/admin_base/operations/productgroup_form.html b/templates/admin_base/operations/productgroup_form.html deleted file mode 100644 index 63e062e..0000000 --- a/templates/admin_base/operations/productgroup_form.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建产品组{% else %}编辑产品组{% endif %}{% endblock %} - -{% block breadcrumb %} -产品组管理 -chevron_right -{% if is_create %}创建产品组{% else %}编辑产品组{% endif %} -{% endblock %} - -{% block content %} - -
    -

    {% if is_create %}创建产品组{% else %}编辑产品组{% endif %}

    -

    - {% if is_create %}填写以下信息创建新的产品组{% else %}修改产品组 {{ productgroup.name }} 的信息{% endif %} -

    -
    - - - -
    - {% csrf_token %} - -
    - -
    - - {{ form.name }} - {% if form.name.errors %} -
    - {% for error in form.name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - {{ form.display_order }} - {% if form.display_order.help_text %} -

    {{ form.display_order.help_text }}

    - {% endif %} - {% if form.display_order.errors %} -
    - {% for error in form.display_order.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - -
    - {{ form.visibility }} - expand_more -
    - {% if form.visibility.help_text %} -

    {{ form.visibility.help_text }}

    - {% endif %} - {% if form.visibility.errors %} -
    - {% for error in form.visibility.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - - {% if form.is_active.errors %} -
    - {% for error in form.is_active.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    - - {{ form.description }} - {% if form.description.errors %} -
    - {% for error in form.description.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} -
    - {% endif %} - - -
    - - 取消 - - - {% if is_create %}创建产品组{% else %}保存修改{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/operations/productgroup_list.html b/templates/admin_base/operations/productgroup_list.html deleted file mode 100644 index ff64fb5..0000000 --- a/templates/admin_base/operations/productgroup_list.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 产品组管理{% endblock %} - -{% block breadcrumb %} -产品组管理 -chevron_right -产品组列表 -{% endblock %} - -{% block content %} - -
    -
    -

    产品组管理

    -

    管理所有产品分组

    -
    - - 添加产品组 - -
    - - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - - -{% if productgroups %} - - {% for pg in productgroups %} - - - {{ pg.name }} - - {{ pg.description|truncatechars:40|default:"-" }} - {{ pg.display_order }} - - {% if pg.visibility == 'public' %} - - {% else %} - - {% endif %} - - - {% if pg.is_active %} - - {% else %} - - {% endif %} - - - {% if pg.created_by %}{{ pg.created_by.username }}{% else %}-{% endif %} - - {{ pg.created_at|date:"Y-m-d H:i" }} - - - - - {% endfor %} - - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -
    - folder_open -

    暂无产品组

    -

    点击下方按钮添加第一个产品组

    - - 添加产品组 - -
    -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/request_detail.html b/templates/admin_base/operations/request_detail.html deleted file mode 100644 index 31846cf..0000000 --- a/templates/admin_base/operations/request_detail.html +++ /dev/null @@ -1,334 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 申请详情{% endblock %} - -{% block breadcrumb %} -开户申请 -chevron_right -{{ request_obj.username }} -{% endblock %} - -{% block content %} - -
    -
    - - arrow_back - -
    -

    申请详情

    -

    {{ request_obj.username }} - {{ request_obj.target_product.display_name }}

    -
    -
    -
    - {% if request_obj.status == 'pending' %} -
    - {% csrf_token %} - 批准 -
    - - - -
    - {% csrf_token %} -
    - - -
    - -
    -
    - {% elif request_obj.status == 'failed' %} - - -
    - {% csrf_token %} -
    -

    - 确定要重试此失败的开户申请吗?系统将重新执行用户创建流程。 -

    - {% if request_obj.retry_count > 0 %} -

    - 此申请已重试过 {{ request_obj.retry_count }} 次。 -

    - {% endif %} -
    - -
    -
    - {% endif %} -
    -
    - -
    - -
    - - - -
    - {% if request_obj.status == 'pending' %} -
    - schedule -
    - {% elif request_obj.status == 'approved' %} -
    - check_circle -
    - {% elif request_obj.status == 'rejected' %} -
    - cancel -
    - {% elif request_obj.status == 'processing' %} -
    - sync -
    - {% elif request_obj.status == 'completed' %} -
    - task_alt -
    - {% elif request_obj.status == 'failed' %} -
    - error -
    - {% endif %} -
    -

    {{ request_obj.get_status_display }}

    -

    提交于 {{ request_obj.created_at|date:"Y-m-d H:i" }}

    - {% if request_obj.retry_count > 0 %} -

    已重试 {{ request_obj.retry_count }} 次

    - {% endif %} -
    -
    -
    - - - -
    -
    -

    申请人

    -

    {{ request_obj.applicant.username }}

    -
    -
    -

    联系邮箱

    -

    {{ request_obj.contact_email }}

    -
    - {% if request_obj.contact_phone %} -
    -

    联系电话

    -

    {{ request_obj.contact_phone }}

    -
    - {% endif %} -
    -
    - - - -
    -
    -

    用户名

    -

    {{ request_obj.username }}

    -
    -
    -

    用户姓名

    -

    {{ request_obj.user_fullname }}

    -
    -
    -

    用户邮箱

    -

    {{ request_obj.user_email }}

    -
    -
    -

    目标产品

    -

    {{ request_obj.target_product.display_name }}

    -
    - {% if request_obj.user_description %} -
    -

    用户描述

    -

    {{ request_obj.user_description }}

    -
    - {% endif %} - {% if request_obj.requested_disk_capacity %} -
    -

    需求磁盘容量

    -
    - {% for disk, capacity in request_obj.requested_disk_capacity.items %} - - {{ disk }}: {{ capacity }} MB - - {% endfor %} -
    -
    - {% endif %} -
    -
    - - - {% if request_obj.approved_by %} - -
    -
    -

    审核人

    -

    {{ request_obj.approved_by.username }}

    -
    -
    -

    审核时间

    -

    {{ request_obj.approval_date|date:"Y-m-d H:i" }}

    -
    - {% if request_obj.approval_notes %} -
    -

    审核备注

    -

    {{ request_obj.approval_notes }}

    -
    - {% endif %} -
    -
    - {% endif %} - - - {% if request_obj.result_message %} - -
    - {% if request_obj.cloud_user_id %} -
    -

    云电脑用户ID

    -

    {{ request_obj.cloud_user_id }}

    -
    - {% endif %} -
    -

    结果信息

    -

    {{ request_obj.result_message }}

    -
    -
    -
    - {% endif %} -
    - - -
    - -
    - {% for step in timeline %} -
    -
    -
    - - {% if step.done %}check_circle{% else %}radio_button_unchecked{% endif %} - -
    - {% if not forloop.last %} -
    - {% endif %} -
    -
    -

    {{ step.label }}

    - {% if step.time %} -

    {{ step.time|date:"Y-m-d H:i" }}

    - {% endif %} - {% if step.detail %} -

    {{ step.detail }}

    - {% endif %} -
    -
    - {% endfor %} -
    -
    - - - -
    - {% if request_obj.status == 'pending' %} -
    - {% csrf_token %} - -
    - - - -
    - {% csrf_token %} -
    - - -
    - -
    -
    - {% else %} - {% if request_obj.status == 'failed' %} - - -
    - {% csrf_token %} -
    -

    - 确定要重试此失败的开户申请吗?系统将重新执行用户创建流程。 -

    - {% if request_obj.retry_count > 0 %} -

    - 此申请已重试过 {{ request_obj.retry_count }} 次。 -

    - {% endif %} -
    - -
    -
    - {% else %} -

    当前状态无可用操作

    - {% endif %} - {% endif %} -
    -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/operations/request_list.html b/templates/admin_base/operations/request_list.html deleted file mode 100644 index 073017c..0000000 --- a/templates/admin_base/operations/request_list.html +++ /dev/null @@ -1,200 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 开户申请{% endblock %} - -{% block breadcrumb %} -开户申请 -chevron_right -申请列表 -{% endblock %} - -{% block content %} - -
    -
    -

    开户申请

    -

    审核和管理所有开户申请

    -
    -
    - - -
    - - 全部 - - {{ total_count }} - - - {% for value, label, count in status_choices_with_counts %} - - {{ label }} - - {{ count }} - - - {% endfor %} -
    - - - -
    - {% if current_status %} - - {% endif %} -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - - -{% if page_obj %} -
    - {% for req in page_obj %} - -
    - -
    - -
    - {% if req.status == 'pending' %} - mark_email_unread - {% elif req.status == 'approved' %} - mark_email_read - {% elif req.status == 'rejected' %} - mail - {% elif req.status == 'processing' %} - pending - {% elif req.status == 'completed' %} - task_alt - {% elif req.status == 'failed' %} - error - {% else %} - mail - {% endif %} -
    - -
    -
    - - {{ req.username }} - - - → {{ req.target_product.display_name }} - - {% if req.status == 'pending' %} - - {% elif req.status == 'approved' %} - - {% elif req.status == 'rejected' %} - - {% elif req.status == 'processing' %} - - {% elif req.status == 'completed' %} - - {% elif req.status == 'failed' %} - - {% endif %} -
    -

    - person - {{ req.user_fullname }} - · - schedule - {{ req.created_at|date:"Y-m-d H:i" }} - · - 申请人: {{ req.applicant.username }} -

    -
    -
    - - -
    - - visibility - - {% if req.status == 'pending' %} -
    - {% csrf_token %} - -
    -
    - {% csrf_token %} - - -
    - {% elif req.status == 'approved' %} - - check_circle - 已批准 - - {% elif req.status == 'rejected' %} - - cancel - 已驳回 - - {% elif req.status == 'failed' %} -
    - {% csrf_token %} - -
    - {% endif %} -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/route_list.html b/templates/admin_base/operations/route_list.html deleted file mode 100644 index bfafd4f..0000000 --- a/templates/admin_base/operations/route_list.html +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 域名路由{% endblock %} - -{% block breadcrumb %} -域名路由 -chevron_right -路由列表 -{% endblock %} - -{% block content %} - -
    -
    -

    域名路由

    -

    查看所有 RDP 域名路由(只读)

    -
    -
    - - - -
    -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - - -{% if page_obj %} -
    - {% for route in page_obj %} - -
    - -
    - -
    - {% if route.is_active %} - language - {% else %} - language - {% endif %} -
    - -
    -
    - {{ route.domain }} - {% if route.is_active %} - - {% else %} - - {% endif %} -
    -

    - inventory_2 - {{ route.product.display_name }} - · - person - {{ route.assigned_to.username }} -

    -
    - {% if route.is_protected %} - - shield - 主机保护 - - {% endif %} - - schedule - 过期: {{ route.expires_at|date:"m-d H:i" }} - -
    -
    -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/task_list.html b/templates/admin_base/operations/task_list.html deleted file mode 100644 index 88f6bfd..0000000 --- a/templates/admin_base/operations/task_list.html +++ /dev/null @@ -1,157 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 系统任务{% endblock %} - -{% block breadcrumb %} -系统任务 -chevron_right -任务列表 -{% endblock %} - -{% block content %} -
    -
    -

    系统任务

    -

    查看所有 Celery 异步任务执行状态(只读)

    -
    -
    - - -
    -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - -{% if page_obj %} -
    - {% for task in page_obj %} - -
    -
    -
    - {% if task.status == 'pending' %} - hourglass_empty - {% elif task.status == 'running' %} - progress_activity - {% elif task.status == 'success' %} - task_alt - {% elif task.status == 'failed' %} - error - {% elif task.status == 'cancelled' %} - block - {% else %} - task - {% endif %} -
    -
    -
    - {{ task.name }} - {% if task.status == 'pending' %} - - {% elif task.status == 'running' %} - - {% elif task.status == 'success' %} - - {% elif task.status == 'failed' %} - - {% elif task.status == 'cancelled' %} - - {% endif %} -
    -

    - {% if task.target_content_type %} - {{ task.target_content_type }} - · - {% endif %} - schedule - {{ task.created_at|date:"Y-m-d H:i" }} - {% if task.created_by %} - · - person - {{ task.created_by.username }} - {% endif %} - {% if task.duration %} - · - timer - {{ task.duration }} - {% endif %} -

    - {% if task.status == 'running' or task.status == 'pending' %} -
    -
    -
    -
    - {{ task.progress }}% -
    - {% endif %} - {% if task.result %} -

    - description - {% if task.result is string %} - {{ task.result|truncatechars:80 }} - {% else %} - {{ task.result|default_if_none:"" }} - {% endif %} -

    - {% endif %} - {% if task.status == 'failed' and task.error_message %} -

    - warning - {{ task.error_message|truncatechars:80 }} -

    - {% endif %} - {% if task.progress_updates.exists %} -
    - - expand_more - 进度详情 - -
    - {% for p in task.progress_updates.all %} -

    - {{ p.progress }}% - · - {{ p.message|default:"" }} - {{ p.timestamp|date:"H:i:s" }} -

    - {% endfor %} -
    -
    - {% endif %} -
    -
    -
    - {{ task.task_id|truncatechars:16 }} -
    -
    -
    - {% endfor %} -
    - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/token_detail.html b/templates/admin_base/operations/token_detail.html deleted file mode 100644 index 1ff331a..0000000 --- a/templates/admin_base/operations/token_detail.html +++ /dev/null @@ -1,229 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} - 邀请令牌详情{% endblock %} - -{% block breadcrumb %} -{% if is_provider %} -邀请令牌 -{% else %} -邀请令牌 -{% endif %} -chevron_right -令牌详情 -{% endblock %} - -{% block content %} - -
    -
    -

    邀请令牌详情

    -

    查看令牌信息和使用该令牌的用户列表

    -
    -
    - {% if is_provider %} - - arrow_back - 返回列表 - - {% else %} - - arrow_back - 返回列表 - - {% endif %} -
    -
    - - - -
    -
    -
    - {% if token_obj.is_valid %} - key - {% elif token_obj.is_expired %} - key_off - {% else %} - key - {% endif %} -
    -
    -
    - {{ token_obj.token }} -
    - -
    - {% if not token_obj.is_active %} - - {% elif token_obj.is_expired %} - - {% elif token_obj.is_exhausted %} - - {% else %} - - {% endif %} -
    -
    -
    - inventory_2 - 关联产品: - {% if token_obj.product %} - {{ token_obj.product.display_name }} - {% elif token_obj.product_group %} - {{ token_obj.product_group.name }} - {% else %} - -- - {% endif %} -
    -
    - counter_1 - 使用次数: - {{ token_obj.used_count }}{% if token_obj.max_uses > 0 %}/{{ token_obj.max_uses }}{% else %}/∞{% endif %} -
    -
    - schedule - 过期时间: - {% if token_obj.expires_at %} - {% if token_obj.is_expired %} - {{ token_obj.expires_at|date:"Y-m-d H:i" }} - {% else %} - {{ token_obj.expires_at|date:"Y-m-d H:i" }} - {% endif %} - {% else %} - 永不过期 - {% endif %} -
    -
    - person - 创建者: - {% if token_obj.created_by %}{{ token_obj.created_by.username }}{% else %}-{% endif %} -
    -
    - event - 创建时间: - {{ token_obj.created_at|date:"Y-m-d H:i" }} -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - group -
    -
    -

    {{ grant_count }}

    -

    总使用人数

    -
    -
    -
    -
    -
    -
    - verified_user -
    -
    -

    {{ effective_grant_count }}

    -

    有效授权

    -
    -
    -
    -
    -
    -
    - counter_1 -
    -
    -

    {{ token_obj.used_count }}{% if token_obj.max_uses > 0 %}/{{ token_obj.max_uses }}{% else %}/∞{% endif %}

    -

    使用次数

    -
    -
    -
    -
    - - - -
    - person_add -

    使用记录

    - ({{ grant_count }}人) -
    - - {% if grants %} -
    - {% for grant in grants %} -
    -
    - {% if grant.is_revoked %} - person_off - {% elif grant.is_expired %} - person - {% else %} - person - {% endif %} -
    -
    -
    - {{ grant.user.username }} - {% if grant.user.email %} - {{ grant.user.email }} - {% endif %} - {% if grant.is_revoked %} - - {% elif grant.is_expired %} - - {% else %} - - {% endif %} -
    -

    - schedule - 授权时间: {{ grant.granted_at|date:"Y-m-d H:i" }} - · - 过期时间: - {% if grant.expires_at %} - {% if grant.is_expired %} - {{ grant.expires_at|date:"Y-m-d H:i" }} - {% else %} - {{ grant.expires_at|date:"Y-m-d H:i" }} - {% endif %} - {% else %} - 永久 - {% endif %} -

    - {% if grant.product_group %} -

    - folder - 产品组: {{ grant.product_group.name }} -

    - {% endif %} -
    -
    - {% endfor %} -
    - - {% if grants.has_other_pages %} -
    - -
    - {% endif %} - - {% else %} - - {% endif %} -
    -{% endblock %} diff --git a/templates/admin_base/operations/token_list.html b/templates/admin_base/operations/token_list.html deleted file mode 100644 index f924635..0000000 --- a/templates/admin_base/operations/token_list.html +++ /dev/null @@ -1,149 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} - 邀请令牌{% endblock %} - -{% block breadcrumb %} -{% if is_provider %} -邀请令牌 -{% else %} -邀请令牌 -{% endif %} -chevron_right -令牌列表 -{% endblock %} - -{% block content %} - -
    -
    -

    邀请令牌

    -

    查看所有产品邀请令牌

    -
    -
    - - - -
    -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - - -{% if page_obj %} -
    - {% for token in page_obj %} - -
    - -
    - -
    - {% if token.is_valid %} - key - {% elif token.is_expired %} - key_off - {% else %} - key - {% endif %} -
    - -
    -
    - - {{ token.token|truncatechars:16 }} -
    - -
    - {% if not token.is_active %} - - {% elif token.is_expired %} - - {% elif token.is_exhausted %} - - {% else %} - - {% endif %} -
    -

    - inventory_2 - {% if token.product %} - {{ token.product.display_name }} - {% elif token.product_group %} - {{ token.product_group.name }} - {% else %} - -- - {% endif %} - · - counter_1 - {{ token.used_count }}{% if token.max_uses > 0 %}/{{ token.max_uses }}{% else %}/∞{% endif %} -

    -

    - schedule - {% if token.expires_at %} - {% if token.is_expired %} - {{ token.expires_at|date:"Y-m-d H:i" }} - {% else %} - {{ token.expires_at|date:"Y-m-d H:i" }} - {% endif %} - {% else %} - 永不过期 - {% endif %} - · - person - {% if token.created_by %}{{ token.created_by.username }}{% else %}-{% endif %} -

    -
    -
    - -
    - {% if is_provider %} - - visibility - 查看详情 - - {% else %} - - visibility - 查看详情 - - {% endif %} -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/operations/user_detail.html b/templates/admin_base/operations/user_detail.html deleted file mode 100644 index 3c40d21..0000000 --- a/templates/admin_base/operations/user_detail.html +++ /dev/null @@ -1,500 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {{ cloud_user.username }}{% endblock %} - -{% block breadcrumb %} -云电脑用户 -chevron_right -{{ cloud_user.username }} -{% endblock %} - -{% block content %} -
    - - - - - -
    -
    - - arrow_back - -
    -

    -

    {{ cloud_user.product.display_name }}

    -
    -
    -
    - - - - -
    -
    - - - - - -
    - - - -
    -
    - 用户名 - -
    -
    - 姓名 - {{ cloud_user.fullname|default:"-" }} -
    -
    - 邮箱 - {{ cloud_user.email|default:"-" }} -
    -
    - 描述 - {{ cloud_user.description|default:"-" }} -
    -
    - 用户组 - {{ cloud_user.groups|default:"-" }} -
    -
    -
    - - - -
    -
    - 所属产品 - {{ cloud_user.product.display_name }} -
    -
    - 关联主机 - {{ cloud_user.product.host.name }} -
    -
    - 管理员权限 - - -
    -
    - 所有者 - - {% if cloud_user.owner %}{{ cloud_user.owner.username }}{% else %}-{% endif %} - -
    -
    - 创建时间 - {{ cloud_user.created_at|date:"Y-m-d H:i:s" }} -
    -
    - 更新时间 - {{ cloud_user.updated_at|date:"Y-m-d H:i:s" }} -
    -
    -
    -
    - - - -
    - - - - - - -
    -
    - - -{% if cloud_user.created_from_request %} - -
    - -
    - 申请人 - - {% if cloud_user.created_from_request.applicant %}{{ cloud_user.created_from_request.applicant.username }}{% else %}-{% endif %} - -
    -
    -
    -{% endif %} - - - - - - - -
    - - -{% endblock %} diff --git a/templates/admin_base/operations/user_list.html b/templates/admin_base/operations/user_list.html deleted file mode 100644 index 033cb75..0000000 --- a/templates/admin_base/operations/user_list.html +++ /dev/null @@ -1,139 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 云电脑用户{% endblock %} - -{% block breadcrumb %} -云电脑用户 -chevron_right -用户列表 -{% endblock %} - -{% block content %} - -
    -
    -

    云电脑用户

    -

    管理所有云电脑用户

    -
    -
    - - - -
    -
    -
    - search - -
    -
    -
    - -
    -
    - -
    - - 筛选 - -
    -
    - - -{% if page_obj %} -
    - {% for user in page_obj %} - -
    - -
    - -
    - {% if user.status == 'active' %} - person - {% elif user.status == 'disabled' %} - person_off - {% elif user.status == 'deleted' %} - person_remove - {% else %} - person - {% endif %} -
    - -
    -
    - - {{ user.username }} - - {% if user.status == 'active' %} - - {% elif user.status == 'inactive' %} - - {% elif user.status == 'disabled' %} - - {% elif user.status == 'deleted' %} - - {% endif %} - {% if user.is_admin %} - - {% endif %} -
    -

    - inventory_2 - {{ user.product.display_name }} - · - dns - {{ user.product.host.name }} -

    - - {% if user.disk_quota %} -
    - {% for disk, quota in user.disk_quota.items %} - - hard_disk - {{ disk }} {{ quota }}MB - - {% endfor %} -
    - {% endif %} -
    -
    - - - -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/provider/coming_soon.html b/templates/admin_base/provider/coming_soon.html deleted file mode 100644 index 72a3cff..0000000 --- a/templates/admin_base/provider/coming_soon.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 提供商后台 - {{ page_title }}{% endblock %} - -{% block breadcrumb %} -首页 -chevron_right -{{ page_title }} -{% endblock %} - -{% block content %} - -
    -
    -
    - {{ feature_icon }} -
    -

    {{ feature_name }}

    -

    该功能正在开发中,敬请期待

    - - arrow_back - 返回仪表盘 - -
    -
    -{% endblock %} diff --git a/templates/admin_base/provider/dashboard.html b/templates/admin_base/provider/dashboard.html deleted file mode 100644 index 87aa1f2..0000000 --- a/templates/admin_base/provider/dashboard.html +++ /dev/null @@ -1,108 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 提供商后台 - 仪表盘{% endblock %} - -{% block breadcrumb %} -仪表盘 -{% endblock %} - -{% block content %} - -
    -

    仪表盘

    -

    欢迎回来,{{ user.username }}

    -
    - - -
    - - -
    -

    {{ stats.host_count }}

    - 查看全部 -
    -
    - - - -
    -

    {{ stats.product_count }}

    - 查看全部 -
    -
    - - - -
    -

    {{ stats.pending_request_count }}

    - 去处理 -
    -
    - - - -
    -

    {{ stats.active_user_count }}

    - 查看全部 -
    -
    -
    - - -
    - -
    -
    - workspaces -
    -

    {{ stats.hostgroup_count }}

    -

    主机组

    -
    -
    -
    - - -
    -
    - category -
    -

    {{ stats.productgroup_count }}

    -

    产品组

    -
    -
    -
    - - -
    -
    - link -
    -

    {{ stats.invitation_token_count }}

    -

    活跃邀请码

    -
    -
    -
    - - -
    -
    - key -
    -

    {{ stats.access_grant_count }}

    -

    有效授权

    -
    -
    -
    - - -
    -
    - language -
    -

    {{ stats.rdp_route_count }}

    -

    域名路由

    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/providers/host_list.html b/templates/admin_base/providers/host_list.html deleted file mode 100644 index 4d6f84a..0000000 --- a/templates/admin_base/providers/host_list.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 主机提供商分配{% endblock %} - -{% block breadcrumb %} -超管后台 -chevron_right -主机提供商分配 -{% endblock %} - -{% block content %} -
    -
    -

    主机提供商分配

    -

    管理所有主机的提供商分配,控制哪些提供商可以管理哪些主机

    -
    -
    - - -
    -
    -
    - search - -
    -
    -
    - -
    -
    - -
    - - 筛选 - -
    -
    - -{% if hosts %} - - {% for host in hosts %} - - - {{ host.name }} - - {{ host.hostname }}:{{ host.port }} - - {% if host.connection_type == 'winrm' %} - - {% elif host.connection_type == 'ssh' %} - - {% elif host.connection_type == 'tunnel' %} - - {% elif host.connection_type == 'localwinserver' %} - - {% else %} - - {% endif %} - - - {% if host.status == 'online' %} - - {% elif host.status == 'offline' %} - - {% elif host.status == 'error' %} - - {% else %} - - {% endif %} - - - {% if host.providers.all %} -
    - {% for provider in host.providers.all %} - - {% endfor %} -
    - {% else %} - 未分配 - {% endif %} - - - {% if host.created_by %} - {{ host.created_by.username }} - {% else %} - - - {% endif %} - - - - group_add - 分配提供商 - - - - {% endfor %} -
    - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/providers/host_provider_assign.html b/templates/admin_base/providers/host_provider_assign.html deleted file mode 100644 index 7c3adac..0000000 --- a/templates/admin_base/providers/host_provider_assign.html +++ /dev/null @@ -1,226 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 分配提供商给主机{% endblock %} - -{% block breadcrumb %} -超管后台 -chevron_right -主机提供商分配 -chevron_right -{{ host.name }} -{% endblock %} - -{% block content %} -
    -
    -

    分配提供商 - {{ host.name }}

    -

    选择可以管理此主机的提供商用户

    -
    - - - 返回列表 - - -
    - - -
    -
    -

    主机名称

    -

    {{ host.name }}

    -
    -
    -

    主机地址

    -

    {{ host.hostname }}:{{ host.port }}

    -
    -
    -

    连接类型

    -

    {{ host.get_connection_type_display }}

    -
    -
    -

    状态

    - {% if host.status == 'online' %} - - {% elif host.status == 'offline' %} - - {% elif host.status == 'error' %} - - {% else %} - - {% endif %} -
    -
    -
    - - -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    -
    -
    - - -
    -
    - - - -
    - -
    - -
    - - - - - - - - - - - - - - -
    类型名称操作
    - person_off -

    暂未分配提供商,请使用上方表单添加

    -
    -
    - - -
    - -
    - - 取消 - - - 保存分配 - -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/admin_base/providers/hostgroup_list.html b/templates/admin_base/providers/hostgroup_list.html deleted file mode 100644 index b138706..0000000 --- a/templates/admin_base/providers/hostgroup_list.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 主机组提供商分配{% endblock %} - -{% block breadcrumb %} -超管后台 -chevron_right -主机组提供商分配 -{% endblock %} - -{% block content %} -
    -
    -

    主机组提供商分配

    -

    管理所有主机组的提供商分配,控制哪些提供商可以管理哪些主机组

    -
    -
    - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - -{% if hostgroups %} - - {% for group in hostgroups %} - - - {{ group.name }} - - - {% if group.description %} - {{ group.description|truncatechars:40 }} - {% else %} - - - {% endif %} - - - - - - {% if group.providers.all %} -
    - {% for provider in group.providers.all %} - - {% endfor %} -
    - {% else %} - 未分配 - {% endif %} - - - {% if group.created_by %} - {{ group.created_by.username }} - {% else %} - - - {% endif %} - - - - group_add - 分配提供商 - - - - {% endfor %} -
    - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/providers/hostgroup_provider_assign.html b/templates/admin_base/providers/hostgroup_provider_assign.html deleted file mode 100644 index 7e9c5e6..0000000 --- a/templates/admin_base/providers/hostgroup_provider_assign.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 分配提供商给主机组{% endblock %} - -{% block breadcrumb %} -超管后台 -chevron_right -主机组提供商分配 -chevron_right -{{ hostgroup.name }} -{% endblock %} - -{% block content %} -
    -
    -

    分配提供商 - {{ hostgroup.name }}

    -

    选择可以管理此主机组的提供商用户

    -
    - - - 返回列表 - - -
    - - -
    -
    -

    组名称

    -

    {{ hostgroup.name }}

    -
    -
    -

    描述

    -

    - {% if hostgroup.description %} - {{ hostgroup.description }} - {% else %} - - - {% endif %} -

    -
    -
    -

    包含主机数

    -

    {{ hostgroup.hosts.count }} 台

    -
    -
    - - {% if hostgroup.hosts.all %} -
    -

    包含的主机:

    -
    - {% for host in hostgroup.hosts.all %} - - {% endfor %} -
    -
    - {% endif %} -
    - - - {% if current_providers %} -
    - {% for provider in current_providers %} -
    - person - {{ provider.username }} - {% if provider.email %} - ({{ provider.email }}) - {% endif %} -
    - {% endfor %} -
    - {% else %} -
    - info - 尚未分配任何提供商 -
    - {% endif %} -
    - - - - 选择可以管理此主机组的提供商用户。提供商可以查看和管理分配给他们的主机组及其包含的主机资源。按住 Ctrl/Cmd 可多选。 - - -
    - {% csrf_token %} -
    - - {{ form.providers }} - {% if form.providers.help_text %} -

    {{ form.providers.help_text }}

    - {% endif %} - {% if form.providers.errors %} -
    - {% for error in form.providers.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - -
    - - - 取消 - - - - 保存分配 - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/reglinks/reglink_confirm_delete.html b/templates/admin_base/reglinks/reglink_confirm_delete.html deleted file mode 100644 index 1a888a5..0000000 --- a/templates/admin_base/reglinks/reglink_confirm_delete.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除注册链接{% endblock %} - -{% block breadcrumb %} -注册链接管理 -chevron_right -删除注册链接 -{% endblock %} - -{% block content %} -
    -
    -

    删除注册链接

    -

    确认删除此注册链接

    -
    - - arrow_back - 返回列表 - -
    - - -
    -
    - warning -
    -

    确认删除

    -

    确定要删除此注册链接吗?此操作不可撤销。

    - -
    -
    - 用户组 - {{ reglink.group.name }} -
    -
    - 使用次数 - - {% if reglink.max_uses == 0 %} - 已用 {{ reglink.used_count }}次 / 不限 - {% else %} - 已用 {{ reglink.used_count }}/{{ reglink.max_uses }}次 - {% endif %} - -
    -
    - 创建者 - {{ reglink.created_by.username|default:"未知" }} -
    -
    - 创建时间 - {{ reglink.created_at|date:"Y-m-d H:i" }} -
    - {% if reglink.note %} -
    - 备注 - {{ reglink.note }} -
    - {% endif %} -
    - -
    - {% csrf_token %} - - 取消 - - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/reglinks/reglink_form.html b/templates/admin_base/reglinks/reglink_form.html deleted file mode 100644 index 9111a4c..0000000 --- a/templates/admin_base/reglinks/reglink_form.html +++ /dev/null @@ -1,121 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 创建注册链接{% endblock %} - -{% block breadcrumb %} -注册链接管理 -chevron_right -创建注册链接 -{% endblock %} - -{% block content %} -
    -
    -

    创建注册链接

    -

    创建注册链接,注册后将自动加入指定用户组

    -
    - - arrow_back - 返回列表 - -
    - - -
    - {% csrf_token %} - -
    -

    - group - 用户组设置 -

    -
    - - -

    通过此链接注册的用户将自动加入所选用户组

    -
    -
    - -
    -

    - counter_1 - 使用次数设置 -

    -
    - - -

    设置为0表示不限制使用次数,设置为1则为一次性链接

    -
    -
    - -
    -

    - schedule - 有效期设置 -

    -
    - - -

    留空表示永不过期

    -
    -
    - -
    -

    - description - 备注 -

    -
    - - -

    仅后台可见,方便管理

    -
    -
    - -
    -
    - info - 注册链接达到最大使用次数后将自动失效,不受系统注册开关影响 -
    -
    - -
    - - 取消 - - 创建链接 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/reglinks/reglink_list.html b/templates/admin_base/reglinks/reglink_list.html deleted file mode 100644 index 4496215..0000000 --- a/templates/admin_base/reglinks/reglink_list.html +++ /dev/null @@ -1,208 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 注册链接管理{% endblock %} - -{% block breadcrumb %} -注册链接管理 -chevron_right -注册链接列表 -{% endblock %} - -{% block content %} -
    -
    -

    注册链接管理

    -

    创建注册链接,指定注册后加入的用户组

    -
    - - 创建注册链接 - -
    - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    - 筛选 -
    -
    - -{% if page_obj %} -
    - {% for link in page_obj %} - -
    -
    -
    - - {% if link.is_exhausted %}link_off{% elif link.is_expired %}timer_off{% else %}link{% endif %} - -
    -
    -
    - {{ link.group.name }} - {% if link.is_exhausted %} - - {% elif link.is_expired %} - - {% else %} - - {% endif %} - {% if link.max_uses == 0 %} - - {% endif %} -
    - {% if link.note %} -

    {{ link.note }}

    - {% endif %} -
    - - counter_1 - {% if link.max_uses == 0 %} - 已用 {{ link.used_count }}次 / 不限 - {% else %} - 已用 {{ link.used_count }}/{{ link.max_uses }}次 - {% endif %} - - - person - 创建者: {{ link.created_by.username|default:"未知" }} - - - schedule - 创建: {{ link.created_at|date:"Y-m-d H:i" }} - - {% if link.expires_at %} - - event_busy - 过期: {{ link.expires_at|date:"Y-m-d H:i" }} - - {% else %} - - all_inclusive - 永不过期 - - {% endif %} - {% if link.used_count > 0 %} - - how_to_reg - 最后使用者: {{ link.used_by.username|default:"未知" }} - - - done_all - 最后使用: {{ link.used_at|date:"Y-m-d H:i" }} - - {% endif %} -
    - {% if not link.is_exhausted and not link.is_expired %} -
    - - {% url 'accounts:register_by_link' token=link.token as link_path %}{{ request.scheme }}://{{ request.get_host }}{{ link_path }} - - -
    - {% endif %} -
    -
    - -
    - {% if not link.is_exhausted %} - - delete - - {% else %} - - lock - - {% endif %} -
    -
    -
    - {% endfor %} -
    - -{% if page_obj.has_other_pages %} - -{% endif %} - -{% else %} - - - 创建注册链接 - - -{% endif %} -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/admin_base/themes/pagecontent_confirm_delete.html b/templates/admin_base/themes/pagecontent_confirm_delete.html deleted file mode 100644 index 58c34b0..0000000 --- a/templates/admin_base/themes/pagecontent_confirm_delete.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除页面内容{% endblock %} - -{% block breadcrumb %} -页面内容 -chevron_right -删除内容 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下页面内容吗?

    -
    -
    - 位置 - -
    -
    - 标题 - - {% if page.title %}{{ page.title }}{% else %}-{% endif %} - -
    -
    -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/themes/pagecontent_form.html b/templates/admin_base/themes/pagecontent_form.html deleted file mode 100644 index 715b4b8..0000000 --- a/templates/admin_base/themes/pagecontent_form.html +++ /dev/null @@ -1,131 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}页面内容{% endblock %} - -{% block breadcrumb %} -页面内容 -chevron_right -{% if is_create %}创建内容{% else %}编辑内容{% endif %} -{% endblock %} - -{% block content %} - -
    -
    -

    {% if is_create %}创建页面内容{% else %}编辑页面内容{% endif %}

    -

    - {% if is_create %}填写以下信息创建新内容{% else %}修改页面内容信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    - {% with field=form.position %} - - {% for value, label in form.fields.position.choices %} - - {% endfor %} - - {% endwith %} - - {% with field=form.title %} - - {% endwith %} -
    - - -
    - - - {% if form.content.help_text %} -

    {{ form.content.help_text }}

    - {% endif %} - {% if form.content.errors %} -
    - {% for error in form.content.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - -
    - - -
    -
    - - -
    - - - {% if form.metadata.help_text %} -

    {{ form.metadata.help_text }}

    - {% endif %} - {% if form.metadata.errors %} -
    - {% for error in form.metadata.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/themes/pagecontent_list.html b/templates/admin_base/themes/pagecontent_list.html deleted file mode 100644 index 6421163..0000000 --- a/templates/admin_base/themes/pagecontent_list.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 页面内容{% endblock %} - -{% block breadcrumb %} -页面内容 -chevron_right -内容列表 -{% endblock %} - -{% block content %} - -
    -
    -

    页面内容

    -

    管理系统中各页面的可编辑内容

    -
    - - 添加内容 - -
    - - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - - -{% if page_obj %} - - {% for page in page_obj %} - - - - - - {% if page.title %}{{ page.title }}{% else %}-{% endif %} - - - {% if page.is_enabled %} - - {% else %} - - {% endif %} - - {{ page.updated_at|date:"Y-m-d H:i" }} - - - - - {% endfor %} - - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 添加内容 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/themes/themeconfig_edit.html b/templates/admin_base/themes/themeconfig_edit.html deleted file mode 100644 index 6076681..0000000 --- a/templates/admin_base/themes/themeconfig_edit.html +++ /dev/null @@ -1,167 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 主题配置{% endblock %} - -{% block breadcrumb %} -主题配置 -{% endblock %} - -{% block content %} - -
    -
    -

    主题配置

    -

    管理系统主题风格和品牌设置(单例模式)

    -
    - -
    - {% csrf_token %} - 清除缓存 -
    -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - -
    -

    - palette - 主题选择 -

    -
    - {% with field=form.active_theme %} - - {% for value, label in form.fields.active_theme.choices %} - - {% endfor %} - - {% endwith %} -
    -
    - - -
    -

    - branding_watermark - 品牌资源 -

    -
    - - - {% if form.branding.help_text %} -

    {{ form.branding.help_text }}

    - {% endif %} - {% if form.branding.errors %} -
    - {% for error in form.branding.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -

    - colorize - 自定义颜色 -

    -
    - - - {% if form.custom_colors.help_text %} -

    {{ form.custom_colors.help_text }}

    - {% endif %} - {% if form.custom_colors.errors %} -
    - {% for error in form.custom_colors.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -

    - tune - 高级设置 -

    -
    -
    - - - {% if form.css_overrides.help_text %} -

    {{ form.css_overrides.help_text }}

    - {% endif %} - {% if form.css_overrides.errors %} -
    - {% for error in form.css_overrides.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - -
    - - -
    -
    -
    -
    - - -
    - 保存配置 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/themes/widgetlayout_confirm_delete.html b/templates/admin_base/themes/widgetlayout_confirm_delete.html deleted file mode 100644 index 01893b9..0000000 --- a/templates/admin_base/themes/widgetlayout_confirm_delete.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除组件布局{% endblock %} - -{% block breadcrumb %} -组件布局 -chevron_right -删除布局 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下组件布局吗?

    -
    -
    - 组件类型 - {{ layout.widget_type }} -
    -
    - 显示顺序 - {{ layout.display_order }} -
    -
    - 列跨度 - -
    -
    -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/themes/widgetlayout_form.html b/templates/admin_base/themes/widgetlayout_form.html deleted file mode 100644 index badf78a..0000000 --- a/templates/admin_base/themes/widgetlayout_form.html +++ /dev/null @@ -1,115 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}组件布局{% endblock %} - -{% block breadcrumb %} -组件布局 -chevron_right -{% if is_create %}创建布局{% else %}编辑布局{% endif %} -{% endblock %} - -{% block content %} - -
    -
    -

    {% if is_create %}创建组件布局{% else %}编辑组件布局{% endif %}

    -

    - {% if is_create %}填写以下信息创建新布局{% else %}修改组件布局信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    - {% with field=form.widget_type %} - - {% endwith %} - {% with field=form.display_order %} - - {% endwith %} - {% with field=form.column_span %} - - {% for value, label in form.fields.column_span.choices %} - - {% endfor %} - - {% endwith %} - {% with field=form.row_span %} - - {% for value, label in form.fields.row_span.choices %} - - {% endfor %} - - {% endwith %} -
    - - -
    - -
    - - -
    -
    - - -
    - - - {% if form.responsive.help_text %} -

    {{ form.responsive.help_text }}

    - {% endif %} - {% if form.responsive.errors %} -
    - {% for error in form.responsive.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/themes/widgetlayout_list.html b/templates/admin_base/themes/widgetlayout_list.html deleted file mode 100644 index 79012f8..0000000 --- a/templates/admin_base/themes/widgetlayout_list.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 组件布局{% endblock %} - -{% block breadcrumb %} -组件布局 -chevron_right -布局列表 -{% endblock %} - -{% block content %} - -
    -
    -

    组件布局

    -

    管理仪表盘组件的布局配置

    -
    - - 添加布局 - -
    - - - -
    -
    -
    - search - -
    -
    - - 筛选 - -
    -
    - - -{% if page_obj %} - - {% for layout in page_obj %} - - - {{ layout.widget_type }} - - {{ layout.display_order }} - - - - - - - - {% if layout.is_visible %} - - {% else %} - - {% endif %} - - - - - - {% endfor %} - - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 添加布局 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/tickets/activity_list.html b/templates/admin_base/tickets/activity_list.html deleted file mode 100644 index 4ff1b3b..0000000 --- a/templates/admin_base/tickets/activity_list.html +++ /dev/null @@ -1,129 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 活动日志{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -活动日志 -{% endblock %} - -{% block content %} - -
    -
    -

    活动日志

    -

    查看系统中所有工单操作记录

    -
    -
    - - - -
    -
    -
    - search - -
    -
    -
    - -
    - - 筛选 - -
    -
    - - -{% if activities %} -
    - - - -
    - {% for activity in activities %} - -
    - -
    -
    - {% if activity.action == 'create' %} - add_circle - {% elif activity.action == 'status_change' %} - swap_horiz - {% elif activity.action == 'comment' %} - chat - {% elif activity.action == 'assign' %} - person_add - {% elif activity.action == 'close' %} - lock - {% elif activity.action == 'update' %} - edit - {% else %} - history - {% endif %} -
    -
    - - -
    -
    - {% if activity.action == 'create' %} - - {% elif activity.action == 'status_change' %} - - {% elif activity.action == 'comment' %} - - {% elif activity.action == 'assign' %} - - {% elif activity.action == 'close' %} - - {% elif activity.action == 'update' %} - - {% else %} - - {% endif %} - {{ activity.created_at|date:"Y-m-d H:i" }} -
    -
    - {{ activity.actor.username|default:"系统" }} - {{ activity.description|truncatechars:100 }} -
    - {% if activity.ticket %} - - {% endif %} -
    -
    -
    - {% endfor %} -
    -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/tickets/category_confirm_delete.html b/templates/admin_base/tickets/category_confirm_delete.html deleted file mode 100644 index 395265b..0000000 --- a/templates/admin_base/tickets/category_confirm_delete.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 删除分类 {{ category.name }}{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -工单分类 -chevron_right -删除 -{% endblock %} - -{% block content %} - -
    -

    删除工单分类

    -

    此操作不可撤销,请谨慎确认

    -
    - - - - 您即将删除工单分类 {{ category.name }}。 - {% if ticket_count > 0 %} -
    该分类下还有 {{ ticket_count }} 个关联工单,删除分类后这些工单的分类将变为空。 - {% endif %} -
    此操作不可撤销。 -
    - - - -
    -
    -

    分类名称

    -

    {{ category.name }}

    -
    -
    -

    图标

    -

    - {{ category.icon|default:"help_outline" }} - {{ category.icon|default:"help_outline" }} -

    -
    -
    -

    默认优先级

    -

    {{ category.get_default_priority_display }}

    -
    -
    -

    SLA时限

    -

    {{ category.sla_hours }}小时

    -
    -
    -

    自动分配给

    -

    {{ category.auto_assign_to.username|default:"-" }}

    -
    -
    -

    关联工单数

    -

    {{ ticket_count }}

    -
    -
    -
    - - -
    - {% csrf_token %} -
    - - - 返回列表 - - - - 确认删除 - -
    -
    -{% endblock %} diff --git a/templates/admin_base/tickets/category_form.html b/templates/admin_base/tickets/category_form.html deleted file mode 100644 index b969bec..0000000 --- a/templates/admin_base/tickets/category_form.html +++ /dev/null @@ -1,247 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - {% if is_create %}创建工单分类{% else %}编辑工单分类{% endif %}{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -工单分类 -chevron_right -{% if is_create %}创建分类{% else %}编辑分类{% endif %} -{% endblock %} - -{% block content %} - -
    -

    {% if is_create %}创建工单分类{% else %}编辑工单分类{% endif %}

    -

    - {% if is_create %}填写以下信息创建新的工单分类{% else %}修改分类 {{ category.name }} 的信息{% endif %} -

    -
    - - - -
    - {% csrf_token %} - - -
    -

    - info - 基本信息 -

    -
    - -
    - - {{ form.name }} - {% if form.name.errors %} -
    - {% for error in form.name.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - {{ form.icon }} - {% if form.icon.errors %} -
    - {% for error in form.icon.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    - - {{ form.description }} - {% if form.description.errors %} -
    - {% for error in form.description.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    -

    - settings - 配置 -

    -
    - -
    - -
    - {{ form.default_priority }} - expand_more -
    - {% if form.default_priority.errors %} -
    - {% for error in form.default_priority.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - {{ form.sla_hours }} - {% if form.sla_hours.errors %} -
    - {% for error in form.sla_hours.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - -
    - {{ form.auto_assign_to }} - expand_more -
    - {% if form.auto_assign_to.help_text %} -

    {{ form.auto_assign_to.help_text }}

    - {% endif %} - {% if form.auto_assign_to.errors %} -
    - {% for error in form.auto_assign_to.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - -
    - {{ form.auto_assign_to_group }} - expand_more -
    - {% if form.auto_assign_to_group.help_text %} -

    {{ form.auto_assign_to_group.help_text }}

    - {% endif %} - {% if form.auto_assign_to_group.errors %} -
    - {% for error in form.auto_assign_to_group.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - -
    -

    - tune - 显示设置 -

    -
    - -
    - - - {% if form.is_active.errors %} -
    - {% for error in form.is_active.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - - {% if form.allow_banned_users.errors %} -
    - {% for error in form.allow_banned_users.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    - - -
    - - {{ form.display_order }} - {% if form.display_order.errors %} -
    - {% for error in form.display_order.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    -
    - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} -
    - {% endif %} - - -
    - - - 取消 - - - - {% if is_create %}创建分类{% else %}保存修改{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/tickets/category_list.html b/templates/admin_base/tickets/category_list.html deleted file mode 100644 index 18865cd..0000000 --- a/templates/admin_base/tickets/category_list.html +++ /dev/null @@ -1,136 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 工单分类{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -工单分类 -{% endblock %} - -{% block content %} - -
    -
    -

    工单分类

    -

    管理系统中所有工单分类

    -
    - - - 创建分类 - - -
    - - - -
    -
    -
    - search - -
    -
    - - 搜索 - -
    -
    - - -{% if categories %} -
    - {% for category in categories %} - -
    - -
    -
    -
    - {{ category.icon|default:"help_outline" }} -
    -
    - - {{ category.name }} - - {% if category.description %} -

    {{ category.description }}

    - {% endif %} -
    -
    - {% if category.is_active %} - - {% else %} - - {% endif %} -
    - - -
    -
    - confirmation_number - 工单数: {{ category.tickets.count|default:"0" }} -
    -
    - flag - 默认优先级: - {% if category.default_priority == 'urgent' %} - - {% elif category.default_priority == 'high' %} - - {% elif category.default_priority == 'medium' %} - - {% else %} - - {% endif %} -
    -
    - schedule - SLA: {{ category.sla_hours }}小时 -
    - {% if category.auto_assign_to %} -
    - person - 自动分配: {{ category.auto_assign_to.username }} -
    - {% endif %} -
    - - - -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - - 创建分类 - - -{% endif %} -{% endblock %} diff --git a/templates/admin_base/tickets/ticket_detail.html b/templates/admin_base/tickets/ticket_detail.html deleted file mode 100644 index c095181..0000000 --- a/templates/admin_base/tickets/ticket_detail.html +++ /dev/null @@ -1,267 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 工单 {{ ticket.ticket_no }}{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -工单管理 -chevron_right -{{ ticket.ticket_no }} -{% endblock %} - -{% block content %} - -
    -
    -
    -

    {{ ticket.ticket_no }}

    - {% if ticket.status == 'pending' %} - - {% elif ticket.status == 'processing' %} - - {% elif ticket.status == 'waiting_feedback' %} - - {% elif ticket.status == 'resolved' %} - - {% elif ticket.status == 'closed' %} - - {% elif ticket.status == 'rejected' %} - - {% endif %} - {% if ticket.is_overdue %} - - {% endif %} -
    -

    {{ ticket.title }}

    -
    - - - 返回列表 - - -
    - -
    - -
    - - - -
    -
    -

    工单编号

    -

    {{ ticket.ticket_no }}

    -
    -
    -

    分类

    -

    {{ ticket.category.name|default:"未分类" }}

    -
    -
    -

    优先级

    -

    - {% if ticket.priority == 'urgent' %} - - {% elif ticket.priority == 'high' %} - - {% elif ticket.priority == 'medium' %} - - {% else %} - - {% endif %} -

    -
    -
    -

    来源

    -

    {{ ticket.get_source_display }}

    -
    -
    -

    创建者

    -

    {{ ticket.creator.username }}

    -
    -
    -

    处理人

    -

    - {% if ticket.assignee or ticket.assigned_group %} - {% if ticket.assignee %}{{ ticket.assignee.username }}{% endif %}{% if ticket.assignee and ticket.assigned_group %} / {% endif %}{% if ticket.assigned_group %}{{ ticket.assigned_group.name }}(组){% endif %} - {% else %} - 未分配 - {% endif %} -

    -
    - {% if ticket.related_cloud_computer %} -
    -

    关联云电脑

    -

    {{ ticket.related_cloud_computer.username }}@{{ ticket.related_cloud_computer.product.display_name }}

    -
    - {% endif %} - {% if ticket.related_request %} -
    -

    关联申请

    -

    {{ ticket.related_request }} ({{ ticket.related_request.get_status_display }})

    -
    - {% endif %} -
    -

    创建时间

    -

    {{ ticket.created_at|date:"Y-m-d H:i:s" }}

    -
    - {% if ticket.due_at %} -
    -

    截止时间

    -

    - {{ ticket.due_at|date:"Y-m-d H:i:s" }} -

    -
    - {% endif %} - {% if ticket.resolved_at %} -
    -

    解决时间

    -

    {{ ticket.resolved_at|date:"Y-m-d H:i:s" }}

    -
    - {% endif %} - {% if ticket.closed_at %} -
    -

    关闭时间

    -

    {{ ticket.closed_at|date:"Y-m-d H:i:s" }}

    -
    - {% endif %} - {% if ticket.satisfaction %} -
    -

    满意度评分

    -

    {{ ticket.satisfaction }}/5

    -
    - {% endif %} -
    - - -
    -

    详细描述

    -
    {{ ticket.description }}
    -
    -
    - - - - {% if comments %} -
    - {% for comment in comments %} -
    -
    -
    -
    - person -
    - {{ comment.author.username }} - {{ comment.created_at|date:"Y-m-d H:i" }} - {% if comment.is_internal %} - - {% endif %} -
    -
    -
    {{ comment.content }}
    -
    - {% endfor %} -
    - {% else %} -

    暂无评论

    - {% endif %} - - -
    -

    添加评论

    -
    - {% csrf_token %} -
    - {{ comment_form.content }} - {% if comment_form.content.errors %} -
    - {% for error in comment_form.content.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - - 提交评论 - -
    -
    -
    -
    -
    - - -
    - - - - {% if attachments %} -
    - {% for attachment in attachments %} -
    -
    - description -
    -

    {{ attachment.filename|truncatechars:30 }}

    -

    {{ attachment.uploaded_by.username }} - {{ attachment.created_at|date:"Y-m-d H:i" }}

    -
    -
    -
    - {% endfor %} -
    - {% else %} -

    暂无附件

    - {% endif %} -
    - - - - {% if activities %} -
    - {% for activity in activities %} -
    -
    - {% if activity.action == 'status_change' %} - swap_horiz - {% elif activity.action == 'assign' %} - person_add - {% elif activity.action == 'comment' %} - chat - {% elif activity.action == 'create' %} - add_circle - {% elif activity.action == 'close' %} - lock - {% else %} - info - {% endif %} -
    -
    -

    {{ activity.description }}

    -

    - {{ activity.actor.username|default:"系统" }} - {{ activity.created_at|date:"Y-m-d H:i" }} -

    -
    -
    - {% endfor %} -
    - {% else %} -

    暂无活动记录

    - {% endif %} - - {% if activities %} - - {% endif %} -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/tickets/ticket_list.html b/templates/admin_base/tickets/ticket_list.html deleted file mode 100644 index e340ebf..0000000 --- a/templates/admin_base/tickets/ticket_list.html +++ /dev/null @@ -1,213 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超管后台 - 工单管理{% endblock %} - -{% block breadcrumb %} -超级管理员 -chevron_right -工单管理 -{% endblock %} - -{% block content %} - -
    -
    -

    工单管理

    -

    查看和管理系统中所有工单

    -
    - - 管理分类 - -
    - - - - - - -
    - {% if status_filter %} - - {% endif %} -
    -
    - search - -
    -
    -
    - -
    - - 筛选 - -
    -
    - - -
    -
    - {% csrf_token %} -
    - - - - -
    - - - {% if tickets %} -
    - {% for ticket in tickets %} - -
    - -
    - - - -
    - {% if ticket.priority == 'urgent' %} - - {% elif ticket.priority == 'high' %} - - {% elif ticket.priority == 'medium' %} - - {% else %} - - {% endif %} -
    - -
    -
    - - {{ ticket.ticket_no }} - - · - - {{ ticket.title|truncatechars:50 }} - - {% if ticket.status == 'pending' %} - - {% elif ticket.status == 'processing' %} - - {% elif ticket.status == 'waiting_feedback' %} - - {% elif ticket.status == 'resolved' %} - - {% elif ticket.status == 'closed' %} - - {% elif ticket.status == 'rejected' %} - - {% endif %} - {% if ticket.priority == 'urgent' %} - - {% elif ticket.priority == 'high' %} - - {% elif ticket.priority == 'medium' %} - - {% else %} - - {% endif %} -
    -

    - {{ ticket.category.name|default:"未分类" }} - · - {{ ticket.creator.username }} - · - {{ ticket.created_at|date:"Y-m-d H:i" }} - {% if ticket.assignee or ticket.assigned_group %} - · - 处理人: {% if ticket.assignee %}{{ ticket.assignee.username }}{% endif %}{% if ticket.assignee and ticket.assigned_group %} / {% endif %}{% if ticket.assigned_group %}{{ ticket.assigned_group.name }}(组){% endif %} - {% endif %} -

    -
    -
    - - - -
    -
    - {% endfor %} -
    - - - {% if page_obj.has_other_pages %} -
    - -
    - {% endif %} - - {% else %} - - - {% endif %} -
    -
    -{% endblock %} diff --git a/templates/admin_base/users/user_confirm_delete.html b/templates/admin_base/users/user_confirm_delete.html deleted file mode 100644 index 741a0a7..0000000 --- a/templates/admin_base/users/user_confirm_delete.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 删除用户{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -删除用户 -{% endblock %} - -{% block content %} - -
    -

    确认删除

    -

    此操作不可撤销

    -
    - - - -
    -

    确定要删除以下用户吗?删除后该用户的所有数据将无法恢复。

    -
    -
    - 用户名 - {{ target_user.username }} -
    -
    - 邮箱 - {{ target_user.email|default:"-" }} -
    -
    - 姓名 - {{ target_user.get_full_name|default:"-" }} -
    -
    - 员工状态 - {% if target_user.is_staff %} - - {% else %} - - {% endif %} -
    -
    - 用户组 - {% if target_user.groups.exists %} -
    - {% for group in target_user.groups.all %} - - {% endfor %} -
    - {% else %} - - - {% endif %} -
    -
    - 创建时间 - {{ target_user.created_at|date:"Y-m-d H:i" }} -
    -
    -
    - -
    - - 取消 - -
    - {% csrf_token %} - 确认删除 -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/users/user_form.html b/templates/admin_base/users/user_form.html deleted file mode 100644 index 4ce00ca..0000000 --- a/templates/admin_base/users/user_form.html +++ /dev/null @@ -1,116 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {% if is_create %}创建{% else %}编辑{% endif %}用户{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -{% if is_create %}创建用户{% else %}编辑用户{% endif %} -{% endblock %} - -{% block content %} - -
    -
    -

    {% if is_create %}创建用户{% else %}编辑用户{% endif %}

    -

    - {% if is_create %}填写以下信息创建新用户{% else %}修改用户「{{ target_user.username }}」的信息{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - -
    -

    - person - 基本信息 -

    -
    - - - - -
    -
    - - - {% if is_create %} -
    -

    - key - 密码 -

    -
    - - -
    -
    - {% else %} -
    -

    - key - 密码 -

    - - 编辑用户时不支持修改密码,请使用 - 重置密码 - 功能 - -
    - {% endif %} - - -
    -

    - group - 用户组 -

    -
    - -
    - {{ form.groups }} - expand_more -
    - {% if form.groups.errors %} -
    - {% for error in form.groups.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} -
    -
    - - -
    - - 取消 - - - {% if is_create %}创建{% else %}保存{% endif %} - -
    -
    -
    -{% endblock %} diff --git a/templates/admin_base/users/user_list.html b/templates/admin_base/users/user_list.html deleted file mode 100644 index b206f2d..0000000 --- a/templates/admin_base/users/user_list.html +++ /dev/null @@ -1,216 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 用户管理{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -用户列表 -{% endblock %} - -{% block content %} - -
    -
    -

    用户管理

    -

    管理系统中的所有用户账号

    -
    - - 创建用户 - -
    - - - -
    -
    -
    - search - -
    -
    -
    -
    - - expand_more -
    -
    - - expand_more -
    - - 筛选 - -
    -
    -
    - - -{% if page_obj %} -
    - {% for user in page_obj %} - -
    - -
    - -
    - {{ user.username|first|upper }} -
    - -
    -
    - {{ user.username }} - {% if user.is_superuser %} - - {% endif %} - {% if user.is_staff %} - - {% endif %} - {% if user.is_active %} - - {% else %} - - {% endif %} - {% if user.active_ban %} - - {% endif %} -
    -

    - {{ user.email|default:"未设置邮箱" }} - {% if user.get_full_name %} - · - {{ user.get_full_name }} - {% endif %} -

    - - {% if user.groups.exists %} -
    - {% for group in user.groups.all %} - - {{ group.name }} - - {% endfor %} -
    - {% endif %} -
    -
    - - -
    - - edit - - - key - - -
    - {% csrf_token %} - {% if user.active_ban %} - - {% else %} - - {% endif %} -
    - - - delete - -
    -
    -
    - {% endfor %} -
    - - -{% if page_obj.has_other_pages %} -
    - -
    -{% endif %} - -{% else %} - - - 创建用户 - - -{% endif %} -{% endblock %} - -{% block extra_js %} - - - - -{% endblock %} diff --git a/templates/admin_base/users/user_reset_password.html b/templates/admin_base/users/user_reset_password.html deleted file mode 100644 index 3a5eb46..0000000 --- a/templates/admin_base/users/user_reset_password.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 重置密码{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -{{ target_user.username }} -chevron_right -重置密码 -{% endblock %} - -{% block content %} - -
    -
    -

    重置密码

    -

    为用户「{{ target_user.username }}」设置新密码

    -
    - - arrow_back - 返回列表 - -
    - - - -
    -
    -

    用户名

    -

    {{ target_user.username }}

    -
    -
    -

    邮箱

    -

    {{ target_user.email|default:"-" }}

    -
    -
    -

    姓名

    -

    {{ target_user.get_full_name|default:"-" }}

    -
    -
    -
    - - - -
    - {% csrf_token %} - - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - - - 重置密码后,用户需要使用新密码登录。此操作不可撤销。 - - -
    - {% with field=form.new_password1 %} - - {% endwith %} - {% with field=form.new_password2 %} - - {% endwith %} -
    - - -
    - - 取消 - - - 重置密码 - -
    -
    -
    -{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100755 index 9dc6db8..0000000 --- a/templates/base.html +++ /dev/null @@ -1,389 +0,0 @@ -{% load static %} - - - - - - - - {% block title %}{{ site_name }}{% endblock %} - - - - - - - - - - - - - - {% block extra_css %}{% endblock %} - - - -
    - - - - - - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} - - -
    - {% block content %}{% endblock %} -
    - - - - - - - {% block extra_js %}{% endblock %} - - - - diff --git a/templates/components/alert.html b/templates/components/alert.html deleted file mode 100644 index b95e6a4..0000000 --- a/templates/components/alert.html +++ /dev/null @@ -1,67 +0,0 @@ -{# Material Design 3 警告框组件 #} - -使用示例: -{% include 'components/alert.html' with - type="info" # info, success, warning, error - title="提示标题" # 警告框标题(可选) - dismissible=true # 是否可关闭 - icon=true # 是否显示图标 - classes="custom-class" # 自定义CSS类(可选) -%} -警告内容 -{% endinclude %} - - -{# 设置默认值 #} -{% with type=type|default:"info" dismissible=dismissible|default:False icon=icon|default:True classes=classes|default:"" %} - - - -{% endwith %} - - diff --git a/templates/components/button.html b/templates/components/button.html deleted file mode 100644 index 733e9b0..0000000 --- a/templates/components/button.html +++ /dev/null @@ -1,113 +0,0 @@ -{# Material Design 3 按钮组件 #} - -使用示例: -{% include 'components/button.html' with - variant="filled" # filled, outlined, text, elevated, tonal - size="medium" # small, medium, large - color="primary" # primary, secondary, tertiary, error - icon="check" # 图标名称(可选) - icon_position="start" # start, end(图标位置) - disabled=false # 是否禁用 - href="#" # 链接地址(可选,用于链接按钮) - type="button" # button, submit, reset - text="点击我" # 按钮文本 - classes="custom-class" # 自定义CSS类(可选) -%} - - -{% with variant=variant|default:"filled" size=size|default:"medium" color=color|default:"primary" icon_position=icon_position|default:"start" disabled=disabled|default:False type=type|default:"button" classes=classes|default:"" attributes=attributes|default:"" %} - -{% if variant == "tonal" %} - {% if color == "secondary" %} - - {% elif color == "error" %} - - {% else %} - - {% endif %} - -{% elif variant == "outlined" %} - {% if color == "secondary" %} - - {% elif color == "error" %} - - {% else %} - - {% endif %} - -{% elif variant == "text" %} - {% if color == "secondary" %} - - {% elif color == "error" %} - - {% else %} - - {% endif %} - -{% elif variant == "elevated" %} - - -{% else %} - {# variant == "filled" (default) #} - {% if color == "secondary" %} - - {% elif color == "error" %} - - {% else %} - - {% endif %} -{% endif %} - -{% endwith %} diff --git a/templates/cotton/x_admin_alert.html b/templates/cotton/x_admin_alert.html deleted file mode 100644 index 63976db..0000000 --- a/templates/cotton/x_admin_alert.html +++ /dev/null @@ -1,85 +0,0 @@ -{% firstof attrs.type attrs.variant "info" as alert_type %} -{% if alert_type == "success" %} -
    - check_circle -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% elif alert_type == "error" %} -
    - error -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% elif alert_type == "warning" %} -
    - warning -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% else %} -
    - info -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    -{% endif %} diff --git a/templates/cotton/x_admin_badge.html b/templates/cotton/x_admin_badge.html deleted file mode 100644 index 8c7b03e..0000000 --- a/templates/cotton/x_admin_badge.html +++ /dev/null @@ -1,30 +0,0 @@ -{% if attrs.color == "success" %} - - {% if attrs.icon %}{{ attrs.icon }}{% endif %} - {{ attrs.text }} - - -{% elif attrs.color == "error" %} - - {% if attrs.icon %}{{ attrs.icon }}{% endif %} - {{ attrs.text }} - - -{% elif attrs.color == "warning" %} - - {% if attrs.icon %}{{ attrs.icon }}{% endif %} - {{ attrs.text }} - - -{% elif attrs.color == "info" %} - - {% if attrs.icon %}{{ attrs.icon }}{% endif %} - {{ attrs.text }} - - -{% else %} - - {% if attrs.icon %}{{ attrs.icon }}{% endif %} - {{ attrs.text }} - -{% endif %} diff --git a/templates/cotton/x_admin_button.html b/templates/cotton/x_admin_button.html deleted file mode 100644 index ded2d48..0000000 --- a/templates/cotton/x_admin_button.html +++ /dev/null @@ -1,36 +0,0 @@ -{% if attrs.variant == "outlined" %} - - -{% elif attrs.variant == "text" %} - - -{% elif attrs.color == "error" %} - - -{% else %} - -{% endif %} diff --git a/templates/cotton/x_admin_card.html b/templates/cotton/x_admin_card.html deleted file mode 100644 index 3c03d61..0000000 --- a/templates/cotton/x_admin_card.html +++ /dev/null @@ -1,22 +0,0 @@ -
    - {% if attrs.title or attrs.icon %} -
    - {% if attrs.icon %} - {{ attrs.icon }} - {% endif %} - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {% endif %} - -
    - {{ slot }} -
    - - {% if attrs.footer %} -
    - {{ attrs.footer }} -
    - {% endif %} -
    diff --git a/templates/cotton/x_admin_empty.html b/templates/cotton/x_admin_empty.html deleted file mode 100644 index a1e5472..0000000 --- a/templates/cotton/x_admin_empty.html +++ /dev/null @@ -1,12 +0,0 @@ -
    -
    - {{ attrs.icon|default:"info" }} -
    -

    {{ attrs.title }}

    -

    {{ attrs.description }}

    - {% if slot %} -
    - {{ slot }} -
    - {% endif %} -
    diff --git a/templates/cotton/x_admin_input.html b/templates/cotton/x_admin_input.html deleted file mode 100644 index 5cf5859..0000000 --- a/templates/cotton/x_admin_input.html +++ /dev/null @@ -1,46 +0,0 @@ -
    - {% with f=attrs.field|default:field %} - {% if attrs.label or f %} - - {% endif %} - - {% if attrs.type == 'select' %} - - {% else %} - - {% endif %} - - {% if attrs.help_text|default:f.help_text and not attrs.errors and not f.errors %} -

    {{ attrs.help_text|default:f.help_text }}

    - {% endif %} - - {% if attrs.errors or f.errors %} -
    - {% for error in attrs.errors %} -

    {{ error }}

    - {% endfor %} - {% for error in f.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - {% endwith %} -
    diff --git a/templates/cotton/x_admin_modal.html b/templates/cotton/x_admin_modal.html deleted file mode 100644 index cab52c8..0000000 --- a/templates/cotton/x_admin_modal.html +++ /dev/null @@ -1,46 +0,0 @@ -
    - {{ trigger }} - -
    -
    - -
    -
    -

    {{ attrs.title }}

    - -
    - -
    - {{ slot }} -
    - - {% if footer %} -
    - {{ footer }} -
    - {% endif %} -
    -
    -
    diff --git a/templates/cotton/x_admin_pagination.html b/templates/cotton/x_admin_pagination.html deleted file mode 100644 index ff0f4b7..0000000 --- a/templates/cotton/x_admin_pagination.html +++ /dev/null @@ -1,57 +0,0 @@ -
    -
    - 显示 {{ page_obj.start_index }} - {{ page_obj.end_index }} 条,共 {{ page_obj.paginator.count }} 条 -
    -
    - {% if page_obj.has_previous %} - - first_page - - - chevron_left - - {% else %} - - first_page - - - chevron_left - - {% endif %} - -
    - {% for num in page_obj.paginator.page_range %} - {% if num == page_obj.number %} - - {{ num }} - - {% elif num > page_obj.number|add:"-3" and num < page_obj.number|add:3 %} - - {{ num }} - - {% endif %} - {% endfor %} -
    - - {% if page_obj.has_next %} - - chevron_right - - - last_page - - {% else %} - - chevron_right - - - last_page - - {% endif %} -
    -
    diff --git a/templates/cotton/x_admin_table.html b/templates/cotton/x_admin_table.html deleted file mode 100644 index 9b0e6bf..0000000 --- a/templates/cotton/x_admin_table.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load theme_tags %} -
    - - - - {% if attrs.headers %} - {% for header in attrs.headers|split:"," %} - - {% endfor %} - {% else %} - {{ header }} - {% endif %} - - - - {{ slot }} - -
    {{ header|trim }}
    -
    diff --git a/templates/cotton/x_md_alert.html b/templates/cotton/x_md_alert.html deleted file mode 100644 index c8ca588..0000000 --- a/templates/cotton/x_md_alert.html +++ /dev/null @@ -1,105 +0,0 @@ -{% if attrs.variant == "success" %} -
    - check_circle -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% elif attrs.variant == "warning" %} -
    - warning -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% elif attrs.variant == "error" %} -
    - error -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% elif attrs.variant == "info" %} -
    - info -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    - -{% else %} -
    - info -
    - {% if attrs.title %} -

    {{ attrs.title }}

    - {% endif %} -
    - {{ slot }} -
    -
    - {% if attrs.dismissible == "true" %} - - {% endif %} -
    -{% endif %} diff --git a/templates/dashboard/base.html b/templates/dashboard/base.html deleted file mode 100755 index 22f7545..0000000 --- a/templates/dashboard/base.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load static %} - - - - - - - {% block title %}{{ site_name }} 仪表盘{% endblock %} - - {% block extra_css %}{% endblock %} - - - - -
    - {% block content %}{% endblock %} -
    - - - {% block extra_js %}{% endblock %} - - \ No newline at end of file diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html deleted file mode 100755 index 8223846..0000000 --- a/templates/dashboard/index.html +++ /dev/null @@ -1,164 +0,0 @@ -{% extends 'base.html' %} - -{% load markdown_extras %} - -{% block title %}仪表盘 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -

    云电脑产品

    -

    浏览可用的云电脑产品并申请开户

    -
    - -
    -
    -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - - 重置 - -
    -
    -
    -
    - - {% if grouped_products %} - {% for group_data in grouped_products %} - {% if group_data.group %} -
    - folder -
    -

    {{ group_data.group.name }}

    - {% if group_data.group.description %} -

    {{ group_data.group.description }}

    - {% endif %} -
    -
    - {% else %} -
    - widgets -

    其他产品

    -
    - {% endif %} - -
    - {% for product in group_data.products %} -
    -
    -
    -

    {{ product.display_name }}

    - - {% if product.status == 'online' %}在线 - {% elif product.status == 'offline' %}离线 - {% elif product.status == 'error' %}错误 - {% else %}未知{% endif %} - -
    -
    - -
    -
    - 自动审核 -

    - {% if product.auto_approval %} - 已启用 - {% else %} - 未启用 - {% endif %} -

    -
    - - {% if product.display_description %} -
    - {{ product.display_description|markdown_filter }} -
    - {% endif %} - -
    - {% if existing_cloud_users|get_item:product.id %} - - desktop_windows - 查看已有账户 - - {% elif pending_request_ids|get_item:product.id %} - - schedule - 查看申请详细 - - {% else %} - - cloud - 立即申请 - - {% endif %} -
    -
    -
    - {% endfor %} -
    - {% endfor %} - {% else %} -
    - desktop_windows -

    暂无可用的云电脑主机

    -

    当前没有可申请的云电脑产品

    -
    - {% endif %} -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/dashboard/sitegroup_config.html b/templates/dashboard/sitegroup_config.html deleted file mode 100644 index 7508362..0000000 --- a/templates/dashboard/sitegroup_config.html +++ /dev/null @@ -1,189 +0,0 @@ -{% extends "admin_base/base.html" %} -{% block title %}{{ site_name }} - 站点组配置 - {{ sitegroup.name }}{% endblock %} -{% block breadcrumb %} - {% if user.is_superuser %} - 站点组管理 - chevron_right - {{ sitegroup.name }} - chevron_right - {% endif %} - 配置覆盖 -{% endblock %} -{% block content %} -
    -
    -

    {{ sitegroup.name }} - 配置覆盖

    -

    留空的字段将使用全局默认配置

    -
    - {% if user.is_superuser %} - - arrow_back - 返回详情 - - {% endif %} -
    -
    - {% csrf_token %} - -
    -
    - palette -

    站点外观

    -
    -
    -
    -
    - - {{ form.site_name }} -

    全局默认: {{ global_config.site_name|default:"2c2a" }}

    -
    -
    - - {{ form.site_icon }} -

    全局默认: {{ global_config.site_icon_for_hostname|default:"/static/img/favicon.svg" }}

    -
    -
    -
    -
    - - {{ form.icp_number }} -

    全局默认: {{ global_config.icp_number|default:"未配置" }}

    -
    -
    - - {{ form.police_number }} -

    全局默认: {{ global_config.police_number|default:"未配置" }}

    -
    -
    -
    -
    - -
    -
    - how_to_reg -

    注册与邮箱

    -
    -
    -
    - - {{ form.enable_registration }} -

    - 全局默认: - {% if global_config.enable_registration %} - 启用 - {% else %} - 禁用 - {% endif %} -

    -
    -
    - - {{ form.email_suffix_whitelist }} -

    - 全局默认: {{ global_config.email_suffix_whitelist|default:"不限制"|truncatewords:5 }} -

    -
    -
    - - {{ form.email_suffix_blacklist }} -

    - 全局默认: {{ global_config.email_suffix_blacklist|default:"不限制"|truncatewords:5 }} -

    -
    -
    -
    - -
    -
    - mail -

    SMTP 邮件配置

    -
    -
    -
    -
    - - {{ form.smtp_host }} -

    全局: {{ global_config.smtp_host|default:"未配置" }}

    -
    -
    - - {{ form.smtp_port }} -

    全局: {{ global_config.smtp_port|default:"未配置" }}

    -
    -
    -
    -
    - - {{ form.smtp_encryption }} -

    全局: {{ global_config.get_smtp_encryption_display|default:"未配置" }}

    -
    -
    - - {{ form.smtp_username }} -

    全局: {{ global_config.smtp_username|default:"未配置" }}

    -
    -
    -
    - - {{ form.smtp_password }} -

    留空保持原值不变

    -
    -
    -
    - - {{ form.smtp_from_email }} -

    全局: {{ global_config.smtp_from_email|default:"未配置" }}

    -
    -
    - - {{ form.smtp_from_name }} -

    全局: {{ global_config.smtp_from_name|default:"未配置" }}

    -
    -
    -
    -
    - -
    -
    - verified_user -

    验证码配置

    -
    -
    -
    - - {{ form.captcha_provider }} -

    全局: {{ global_config.get_captcha_provider_display|default:"未配置" }}

    -
    -
    -
    - - {{ form.captcha_type }} -
    -
    - - {{ form.login_captcha_type }} -
    -
    -
    -
    - - {{ form.register_captcha_type }} -
    -
    - - {{ form.email_captcha_type }} -
    -
    -
    -
    -
    - -
    -
    -{% endblock %} diff --git a/templates/dashboard/sitegroup_detail.html b/templates/dashboard/sitegroup_detail.html deleted file mode 100644 index 479908b..0000000 --- a/templates/dashboard/sitegroup_detail.html +++ /dev/null @@ -1,189 +0,0 @@ -{% extends "admin_base/base.html" %} -{% block title %}{{ site_name }} 超级管理员 - 站点组详情 - {{ sitegroup.name }}{% endblock %} -{% block breadcrumb %} - 站点组管理 - chevron_right - {{ sitegroup.name }} -{% endblock %} -{% block content %} - -
    -
    -
    - info -

    基本信息

    -
    -
    -
    - 名称 - {{ sitegroup.name }} -
    -
    - 标识符 - {{ sitegroup.slug }} -
    -
    - 描述 - {{ sitegroup.description|default:"-" }} -
    -
    - 站点名称 - {{ sitegroup.site_name|default:"-" }} -
    -
    - 站点图标 - {{ sitegroup.site_icon|default:"-" }} -
    -
    - 状态 - {% if sitegroup.is_active %} - - - 启用 - - {% else %} - - - 禁用 - - {% endif %} -
    -
    -
    -
    -
    - language -

    绑定主机名

    -
    -
    -
    - {% csrf_token %} - - -
    - {% if hostnames %} -
    - {% for hostname in hostnames %} -
    - {{ hostname.hostname }} -
    - {% csrf_token %} - -
    -
    - {% endfor %} -
    - {% else %} -

    暂无绑定主机名

    - {% endif %} -
    -
    -
    -
    -
    - admin_panel_settings -

    站点组管理员

    -
    -
    -
    - {% csrf_token %} - - -
    - {% if admins %} -
    - {% for admin in admins %} -
    -
    -
    - {{ admin.username|first|upper }} -
    - {{ admin.username }} -
    -
    - {% csrf_token %} - -
    -
    - {% endfor %} -
    - {% else %} -

    暂无站点组管理员

    - {% endif %} -
    -
    -
    -
    -
    - dangerous -
    -

    危险操作

    -

    删除站点组将同时删除所有关联的主机名绑定,此操作不可逆

    -
    -
    -
    - {% csrf_token %} - -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/sitegroup_form.html b/templates/dashboard/sitegroup_form.html deleted file mode 100644 index 5bf3ff2..0000000 --- a/templates/dashboard/sitegroup_form.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - {{ title }}{% endblock %} - -{% block breadcrumb %} -站点组管理 -chevron_right -{{ title }} -{% endblock %} - -{% block content %} -
    -
    -

    {{ title }}

    -

    - {% if sitegroup %}修改站点组「{{ sitegroup.name }}」的信息{% else %}填写以下信息创建新的站点组{% endif %} -

    -
    - - arrow_back - 返回列表 - -
    - -
    -
    -
    - {% csrf_token %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - -
    -

    - domain - 基本信息 -

    -
    - {% for field in form %} - {% if field.name == 'is_active' %} -
    - {{ field }} - - {% if field.help_text %} - - {{ field.help_text }} - {% endif %} -
    - {% else %} -
    - - {{ field }} - {% if field.help_text %} -

    {{ field.help_text }}

    - {% endif %} - {% for error in field.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endif %} - {% endfor %} -
    -
    - -
    - 取消 - -
    -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/sitegroup_list.html b/templates/dashboard/sitegroup_list.html deleted file mode 100644 index bb19aaf..0000000 --- a/templates/dashboard/sitegroup_list.html +++ /dev/null @@ -1,89 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} 超级管理员 - 站点组管理{% endblock %} - -{% block breadcrumb %} -站点组管理 -chevron_right -站点组列表 -{% endblock %} - -{% block content %} -
    -
    -

    站点组管理

    -

    管理系统中的所有站点组及其绑定主机名和管理员

    -
    - - - -
    - -
    -
    - - - - - - - - - - - - - - {% for sitegroup in sitegroups %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
    名称标识符站点名称状态主机名管理员操作
    - {{ sitegroup.name }} - - {{ sitegroup.slug }} - {{ sitegroup.site_name|default:"-" }} - {% if sitegroup.is_active %} - - - 启用 - - {% else %} - - - 禁用 - - {% endif %} - {{ sitegroup.hostnames.count }}{{ sitegroup.admins.count }} - -
    - domain - 暂无站点组,点击上方按钮创建 -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/sitegroup_user_list.html b/templates/dashboard/sitegroup_user_list.html deleted file mode 100644 index f77cb1c..0000000 --- a/templates/dashboard/sitegroup_user_list.html +++ /dev/null @@ -1,203 +0,0 @@ -{% extends "admin_base/base.html" %} -{% block title %}{{ site_name }} - {{ site_group.name }} - 用户管理{% endblock %} -{% block breadcrumb %} - {{ site_group.name }} - chevron_right - 用户管理 -{% endblock %} -{% block content %} -
    -
    -

    用户管理

    -

    管理站点组「{{ site_group.name }}」下的用户

    -
    -
    - -
    -
    -
    -
    - search - -
    -
    -
    -
    - - expand_more -
    - -
    -
    -
    - - {% if page_obj %} -
    - {% for user in page_obj %} -
    -
    - -
    -
    - {{ user.username|first|upper }} -
    -
    -
    - {{ user.username }} - {% if user.is_superuser %} - 超管 - {% elif user.pk in admin_ids %} - 站点管理员 - {% endif %} - {% if user.is_staff %} - 员工 - {% endif %} - {% if user.active_ban %} - 封禁 - {% elif user.is_active %} - 活跃 - {% else %} - 禁用 - {% endif %} -
    -

    - {{ user.email|default:"未设置邮箱" }} - {% if user.get_full_name %} - · - {{ user.get_full_name }} - {% endif %} -

    - {% if user.groups.exists %} -
    - {% for group in user.groups.all %} - - {{ group.name }} - - {% endfor %} -
    - {% endif %} -
    -
    - - {% if not user.is_superuser %} -
    - - key - - -
    - {% csrf_token %} - {% if user.active_ban %} - - {% else %} - - {% endif %} -
    - - - remove_circle_outline - -
    - {% endif %} -
    -
    - {% endfor %} -
    - - {% if page_obj.has_other_pages %} -
    - -
    - {% endif %} - {% else %} -
    - person_off -

    暂无用户

    -

    该站点组下还没有用户

    -
    - {% endif %} -{% endblock %} - -{% block extra_js %} - - - - -{% endblock %} diff --git a/templates/dashboard/sitegroup_user_remove.html b/templates/dashboard/sitegroup_user_remove.html deleted file mode 100644 index 95394a4..0000000 --- a/templates/dashboard/sitegroup_user_remove.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} - 移出用户 - {{ target_user.username }}{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -移出用户 -{% endblock %} - -{% block content %} -
    -
    -
    - remove_circle_outline -

    移出站点组

    -
    -
    -
    -
    - {{ target_user.username|first|upper }} -
    -
    -

    {{ target_user.username }}

    -

    {{ target_user.email|default:"未设置邮箱" }}

    -
    -
    -

    - 确定将用户「{{ target_user.username }}」从站点组「{{ site_group.name }}」移出吗?移出后该用户将无法通过该站点组登录,但其账户数据不会被删除。 -

    -
    - {% csrf_token %} - - - 取消 - -
    -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/sitegroup_user_reset_password.html b/templates/dashboard/sitegroup_user_reset_password.html deleted file mode 100644 index 9c53bd0..0000000 --- a/templates/dashboard/sitegroup_user_reset_password.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "admin_base/base.html" %} - -{% block title %}{{ site_name }} - 重置密码 - {{ target_user.username }}{% endblock %} - -{% block breadcrumb %} -用户管理 -chevron_right -重置密码 - {{ target_user.username }} -{% endblock %} - -{% block content %} -
    -
    -
    - key -

    重置用户密码

    -
    -
    -
    -
    - {{ target_user.username|first|upper }} -
    -
    -

    {{ target_user.username }}

    -

    {{ target_user.email|default:"未设置邮箱" }}

    -
    -
    - -
    - {% csrf_token %} - {% for field in form %} -
    - - - {% if field.help_text %} -

    {{ field.help_text }}

    - {% endif %} - {% for error in field.errors %} -

    {{ error }}

    - {% endfor %} -
    - {% endfor %} -
    - - - 取消 - -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/system_config.html b/templates/dashboard/system_config.html deleted file mode 100755 index 3e0682a..0000000 --- a/templates/dashboard/system_config.html +++ /dev/null @@ -1,199 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}系统配置 - {{ site_name }}{% endblock %} - -{% block content %} -
    -

    系统配置

    - -
    -
    -
    系统配置
    -
    -
    -
    - {% csrf_token %} - -
    -
    基本信息
    -
    -
    - - {{ form.site_name }} - {% if form.site_name.help_text %} - {{ form.site_name.help_text }} - {% endif %} - {% for error in form.site_name.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    - -
    -
    备案信息
    -
    -
    - - {{ form.icp_number }} - {% if form.icp_number.help_text %} - {{ form.icp_number.help_text }} - {% endif %} - {% for error in form.icp_number.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.police_number }} - {% if form.police_number.help_text %} - {{ form.police_number.help_text }} - {% endif %} - {% for error in form.police_number.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    - -
    -
    SMTP配置
    -
    -
    - - {{ form.smtp_host }} - {% if form.smtp_host.help_text %} - {{ form.smtp_host.help_text }} - {% endif %} - {% for error in form.smtp_host.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.smtp_port }} - {% if form.smtp_port.help_text %} - {{ form.smtp_port.help_text }} - {% endif %} - {% for error in form.smtp_port.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    -
    - - {{ form.smtp_username }} - {% if form.smtp_username.help_text %} - {{ form.smtp_username.help_text }} - {% endif %} - {% for error in form.smtp_username.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.smtp_password }} - {% if form.smtp_password.help_text %} - {{ form.smtp_password.help_text }} - {% endif %} - {% for error in form.smtp_password.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    -
    - - {{ form.smtp_from_email }} - {% if form.smtp_from_email.help_text %} - {{ form.smtp_from_email.help_text }} - {% endif %} - {% for error in form.smtp_from_email.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.smtp_encryption }} - {% if form.smtp_encryption.help_text %} - {{ form.smtp_encryption.help_text }} - {% endif %} - {% for error in form.smtp_encryption.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    - -
    -
    验证码设置
    -
    -
    - - {{ form.captcha_provider }} - {% if form.captcha_provider.help_text %} - {{ form.captcha_provider.help_text }} - {% endif %} - {% for error in form.captcha_provider.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.captcha_type }} - {% if form.captcha_type.help_text %} - {{ form.captcha_type.help_text }} - {% endif %} - {% for error in form.captcha_type.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -

    场景级别类型留空则使用默认类型

    -
    -
    - - {{ form.login_captcha_type }} - {% if form.login_captcha_type.help_text %} - {{ form.login_captcha_type.help_text }} - {% endif %} - {% for error in form.login_captcha_type.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.register_captcha_type }} - {% if form.register_captcha_type.help_text %} - {{ form.register_captcha_type.help_text }} - {% endif %} - {% for error in form.register_captcha_type.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - - {{ form.email_captcha_type }} - {% if form.email_captcha_type.help_text %} - {{ form.email_captcha_type.help_text }} - {% endif %} - {% for error in form.email_captcha_type.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    -
    - -
    - - - 返回 -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/dashboard/widget_config.html b/templates/dashboard/widget_config.html deleted file mode 100755 index e30aab7..0000000 --- a/templates/dashboard/widget_config.html +++ /dev/null @@ -1,105 +0,0 @@ -{% extends 'dashboard/base.html' %} - -{% block title %}组件配置 - {{ site_name }} 仪表盘{% endblock %} - -{% block content %} -
    -
    -
    -
    仪表盘组件配置
    - 返回仪表盘 -
    -
    -
    - {% csrf_token %} -
    - - - - - - - - - - - {% for widget in widgets %} - - - - - - - {% empty %} - - - - {% endfor %} - -
    组件类型标题显示顺序是否启用
    {{ widget.get_widget_type_display }}{{ widget.title }} - - - -
    暂无组件
    -
    -
    - -
    -
    -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/docs/index.html b/templates/docs/index.html deleted file mode 100644 index 18d2ea4..0000000 --- a/templates/docs/index.html +++ /dev/null @@ -1,365 +0,0 @@ -{% load static markdown_extras %} - - - - - - {{ doc_title }} - {{ site_name }} - - - - - - -
    - - - -
    -
    - - -
    - - -
    - -
    -
    -
    - {{ md_text|markdown_filter }} -
    -
    -
    - - -
    -
    - -
    -
    -

    © {% now "Y" %} 2c2a. All rights reserved.

    -
    -
    - - - - diff --git a/templates/errors/400.html b/templates/errors/400.html deleted file mode 100755 index 10586fd..0000000 --- a/templates/errors/400.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ error_title }} - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    - cancel -
    - -
    400
    - -

    {{ error_title }}

    - -

    - {{ error_message }} -

    - - {% if request_id %} -
    - 请求ID:{{ request_id }} -
    - {% endif %} - -
    -
    - info - 常见原因 -
    -
      -
    • 表单提交的数据格式不正确
    • -
    • 请求参数缺失或无效
    • -
    • 会话已过期,请重新登录
    • -
    -
    - -
    - - home - 返回首页 - - -
    -
    -
    -{% endblock %} diff --git a/templates/errors/403.html b/templates/errors/403.html deleted file mode 100755 index c16ef08..0000000 --- a/templates/errors/403.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ error_title }} - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    - lock -
    - -
    403
    - -

    {{ error_title }}

    - -

    - {{ error_message }} -

    - - {% if request_id %} -
    - 请求ID:{{ request_id }} -
    - {% endif %} - -
    -
    - lightbulb - 建议操作 -
    -

    您没有权限访问此资源。如果您认为这是一个错误,请联系系统管理员获取相应权限,或登录后重试。

    -
    - - -
    -
    -{% endblock %} diff --git a/templates/errors/404.html b/templates/errors/404.html deleted file mode 100755 index 96334fc..0000000 --- a/templates/errors/404.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ error_title }} - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    - travel_explore -
    - -
    404
    - -

    {{ error_title }}

    - -

    - {{ error_message }} -

    - - {% if request_path %} -
    - 请求路径: - {{ request_path }} -
    - {% endif %} - - {% if request_id %} -
    - 请求ID:{{ request_id }} -
    - {% endif %} - -
    -
    - lightbulb - 解决建议 -
    -
      -
    • 检查网址拼写是否正确
    • -
    • 返回上一页,尝试其他链接
    • -
    • 清除浏览器缓存后重试
    • -
    -
    - - -
    -
    -{% endblock %} diff --git a/templates/errors/500.html b/templates/errors/500.html deleted file mode 100755 index dc093ba..0000000 --- a/templates/errors/500.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "base.html" %} -{% load static %} - -{% block title %}{{ error_title }} - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    - construction -
    - -
    500
    - -

    {{ error_title }}

    - -

    - {{ error_message }} -

    - - {% if support_message %} -
    -
    - support_agent - 技术支持 -
    -

    {{ support_message }}

    -
    - {% endif %} - -
    - {% if request_id %} -
    - 请求ID:{{ request_id }} -
    - {% endif %} - - {% if trace_id %} -
    - 追踪ID:{{ trace_id }} -
    - {% endif %} -
    - -
    - - home - 返回首页 - - -
    -
    -
    -{% endblock %} diff --git a/templates/maintenance.html b/templates/maintenance.html deleted file mode 100755 index 600eca8..0000000 --- a/templates/maintenance.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - {{ site_name }} - 系统维护中 - - - - - - - - -
    -
    - build -
    - -

    系统升级维护中

    -

    - 为了提供更稳定、高效的 {{ site_name }} 云管理体验,我们正在进行关键的服务器优化。 -
    在此期间,Web端及开户服务将暂时不可用。 -

    - - -
    - 加载中... -
    - - -
    -
    -
    - -
    - email 联系管理员 - code GitHub Issues -
    © {% now "Y" %} {{ site_name }}. All rights reserved.
    -
    -
    - - - - diff --git a/templates/operations/account_opening_confirm.html b/templates/operations/account_opening_confirm.html deleted file mode 100644 index 11cdad9..0000000 --- a/templates/operations/account_opening_confirm.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}确认开户申请 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    - {% if messages %} - {% for message in messages %} - {% include 'components/alert.html' with message=message.tags content=message %} - {% endfor %} - {% endif %} - -
    -
    -

    确认开户申请

    -
    - -
    -

    请仔细核对以下信息,确认无误后提交申请。

    - -
    -
    - 联系邮箱 -
    {{ confirm_data.contact_email }}
    -
    - -
    - 主机连接用户名 -
    {{ confirm_data.username }}
    -
    - -
    - 主机显示用户名 -
    {{ confirm_data.user_fullname }}
    -
    - -
    - 目标产品 -
    {{ confirm_data.target_product_name }}
    -
    -
    - -
    - 申请理由 -
    {{ confirm_data.user_description }}
    -
    - - {% if confirm_data.requested_disk_capacity %} -
    - 磁盘容量需求 -
    - {% for disk, capacity in confirm_data.requested_disk_capacity.items %} -
    - {{ disk }}: - {{ capacity }} MB -
    - {% endfor %} -
    -
    - {% endif %} - -
    -
    - info - 提交后申请将进入审核流程,请耐心等待审核结果。审核通过后系统将自动为您创建账户。 -
    -
    - -
    - {% csrf_token %} -
    - {% include 'components/button.html' with type="submit" variant="filled" text="确认提交" size="large" icon="check" %} - {% url 'operations:account_opening_create' as account_opening_create_url %} - {% include 'components/button.html' with href=account_opening_create_url variant="outlined" text="返回修改" size="large" icon="edit" %} -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/operations/account_opening_request_detail.html b/templates/operations/account_opening_request_detail.html deleted file mode 100755 index 66c4964..0000000 --- a/templates/operations/account_opening_request_detail.html +++ /dev/null @@ -1,216 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% block title %}申请详情 - {{ site_name }}{% endblock %} -{% block extra_css %} - -{% endblock %} -{% block content %} -
    - {% if messages %} - {% for message in messages %} - {% include 'components/alert.html' with message=message.tags content=message %} - {% endfor %} - {% endif %} -
    -

    申请详情

    - {% url 'operations:account_opening_list' as account_opening_list_url %} - {% include 'components/button.html' with href=account_opening_list_url variant="outlined" text="返回列表" icon="arrow_back" %} -
    -
    -
    -

    申请信息

    -
    -
    -
    -
    - 申请人 -
    - {{ request.applicant.username }} -
    -
    -
    - 联系邮箱 -
    - {{ request.applicant.email }} -
    -
    -
    - 主机连接用户名 -
    - {{ request.username }} -
    -
    -
    - 主机显示用户名 -
    - {{ request.user_fullname }} -
    -
    -
    - 用户邮箱 -
    - {{ request.user_email }} -
    -
    -
    - 用户描述 -
    - {{ request.user_description|default:"暂无描述" }} -
    -
    -
    - 目标产品 -
    - {{ request.target_product.display_name }} -
    -
    -
    -
    -
    -
    -
    -

    状态时间线

    -
    -
    -
    - {% for step in timeline %} -
    -
    -
    - - {% if step.done %} - check_circle - {% else %} - radio_button_unchecked - {% endif %} - -
    - {% if not forloop.last %} -
    - {% endif %} -
    -
    -

    {{ step.label }}

    - {% if step.time %}

    {{ step.time|date:"Y-m-d H:i" }}

    {% endif %} - {% if step.detail %}

    {{ step.detail }}

    {% endif %} -
    -
    - {% endfor %} -
    -
    -
    -
    -
    -

    审核信息

    -
    -
    -
    -
    - 申请状态 -
    - - {{ request.get_status_display }} - -
    -
    - {% if request.approved_by %} -
    - 审核人 -
    - {{ request.approved_by.username }} -
    -
    - {% endif %} - {% if request.approval_date %} -
    - 审核时间 -
    - {{ request.approval_date|date:"Y-m-d H:i:s" }} -
    -
    - {% endif %} - {% if request.approval_notes %} -
    - 审核备注 -
    - {{ request.approval_notes }} -
    -
    - {% endif %} - {% if request.status == 'rejected' %} -
    - 拒绝原因 -
    - {% if request.approval_notes %} - {{ request.approval_notes }} - {% else %} - 未提供具体原因 - {% endif %} -
    -
    - {% endif %} - {% if request.result_message %} -
    - 处理结果 -
    - {% if request.status == 'failed' %} - 开户处理失败,请联系管理员了解详情 - {% else %} - {{ request.result_message }} - {% endif %} -
    -
    - {% endif %} -
    -
    -
    - {% if request.status == 'completed' %} -
    -
    -

    账户信息

    -
    -
    -
    -
    - 云电脑用户ID -
    - {{ request.cloud_user_id }} -
    -
    -
    - 初始密码 -
    - 出于安全考虑,密码不在此显示 -
    -
    -
    -
    -
    - {% endif %} -
    -
    -

    时间信息

    -
    -
    -
    -
    - 创建时间 -
    - {{ request.created_at|date:"Y-m-d H:i:s" }} -
    -
    -
    - 更新时间 -
    - {{ request.updated_at|date:"Y-m-d H:i:s" }} -
    -
    -
    -
    -
    -
    - {% url 'operations:account_opening_list' as account_opening_list_url %} - {% include 'components/button.html' with href=account_opening_list_url variant="outlined" text="返回列表" icon="arrow_back" %} -
    -
    -{% endblock %} diff --git a/templates/operations/account_opening_request_form.html b/templates/operations/account_opening_request_form.html deleted file mode 100644 index 355e71d..0000000 --- a/templates/operations/account_opening_request_form.html +++ /dev/null @@ -1,240 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}提交开户申请 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    - {% if messages %} - {% for message in messages %} - {% include 'components/alert.html' with message=message.tags content=message %} - {% endfor %} - {% endif %} - -
    -
    -

    提交开户申请

    -
    - -
    -
    - {% csrf_token %} - -
    -

    基本信息

    - -
    -
    -
    - - - 使用当前账户的注册邮箱,不可修改 -
    -
    -
    -
    - -
    -

    用户信息

    - -
    -
    -
    - - - {% if form.username.errors %} -
    - {% for error in form.username.errors %}{{ error }}{% endfor %} -
    - {% else %} -
    将在云电脑主机上创建的连接用户名
    - {% endif %} -
    -
    - -
    -
    - - - {% if form.user_fullname.errors %} -
    - {% for error in form.user_fullname.errors %}{{ error }}{% endfor %} -
    - {% else %} -
    用于在系统中显示的用户名
    - {% endif %} -
    -
    -
    -
    - -
    -

    申请详情

    - -
    -
    -
    - - - {% if form.user_description.errors %} -
    - {% for error in form.user_description.errors %}{{ error }}{% endfor %} -
    - {% else %} -
    请说明申请云电脑主机的用途和理由
    - {% endif %} -
    -
    -
    -
    - -
    - 目标产品: - {% if form.target_product.field.queryset.first %} - {{ form.target_product.field.queryset.first.display_name }} - {% else %} - 未指定 - {% endif %} -
    - - {{ form.target_product }} - - - -
    - {% include 'components/button.html' with type="submit" variant="filled" text="提交申请并确认" size="large" %} - {% url 'operations:account_opening_list' as account_opening_list_url %} - {% include 'components/button.html' with href=account_opening_list_url variant="outlined" text="取消" size="large" %} -
    -
    -
    -
    -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/operations/account_opening_request_list.html b/templates/operations/account_opening_request_list.html deleted file mode 100755 index 7e337cd..0000000 --- a/templates/operations/account_opening_request_list.html +++ /dev/null @@ -1,141 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}我提交的申请 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    我提交的申请

    -
    - - -
    -
    -
    -
    - -
    - - -
    -
    - -
    - -
    - - -
    -
    - -
    - -
    - - -
    -
    - -
    - {% include 'components/button.html' with type="submit" variant="outlined" text="搜索" %} - {% url 'operations:account_opening_list' as account_opening_list_url %} - {% include 'components/button.html' with href=account_opening_list_url variant="text" text="重置" %} -
    -
    -
    -
    - - -
    - - - - - - - - - - - - - {% if requests %} - {% for request in requests %} - - - - - - - - - {% endfor %} - {% else %} - - - - {% endif %} - -
    用户名用户姓名目标产品申请状态创建时间操作
    {{ request.username }}{{ request.user_fullname }}{{ request.target_product.display_name }} - - {{ request.get_status_display }} - - {{ request.created_at|date:"Y-m-d H:i:s" }} - {% url 'operations:account_opening_detail' request.id as detail_url %} - {% include 'components/button.html' with href=detail_url variant="text" text="查看详情" size="small" %} -
    暂无开户申请
    -
    - - - {% if is_paginated and paginator.num_pages > 1 %} -
    - -
    - {% endif %} -
    -{% endblock %} diff --git a/templates/operations/cloud_computer_user_list.html b/templates/operations/cloud_computer_user_list.html deleted file mode 100755 index 1c2e079..0000000 --- a/templates/operations/cloud_computer_user_list.html +++ /dev/null @@ -1,169 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}云电脑用户列表 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    云电脑用户列表

    -
    - - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - 重置 -
    -
    -
    -
    - - -
    -
    -
    - - -
    - 已选择 0 个用户 -
    -
    - - -
    -
    - - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
    - - 用户名姓名邮箱所属产品状态创建时间
    - - {{ user.username }}{{ user.fullname }}{{ user.email }}{{ user.product.display_name }} - - {{ user.get_status_display }} - - {{ user.created_at|date:"Y-m-d H:i" }}
    - people -

    暂无云电脑用户

    -
    -
    -
    - - - {% if is_paginated and paginator.num_pages > 1 %} -
    - {% if page_obj.has_previous %} - - 上一页 - - {% endif %} - - {{ page_obj.number }} / {{ paginator.num_pages }} - - {% if page_obj.has_next %} - - 下一页 - - {% endif %} -
    - {% endif %} -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/operations/invite_result.html b/templates/operations/invite_result.html deleted file mode 100644 index 443fdfe..0000000 --- a/templates/operations/invite_result.html +++ /dev/null @@ -1,87 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}邀请结果 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    - {% if needs_confirm %} - lock_open -

    确认解锁

    -

    {{ message }}

    -
    - {% csrf_token %} -
    - - - 取消 - -
    -
    - {% elif success %} - check_circle -

    邀请成功

    -

    {{ message }}

    - - {% if created_users %} -
    -

    已创建的用户:

    -
      - {% for user_info in created_users %} -
    • -
      {{ user_info.username }}
      -
      {{ user_info.fullname }} - {{ user_info.email }}
      -
    • - {% endfor %} -
    -
    - {% endif %} - - {% if products %} -
    -

    关联的产品:

    -
      - {% for product_info in products %} -
    • -
      {{ product_info.name }}
      -
      {{ product_info.hostname }}:{{ product_info.rdp_port }}
      -
    • - {% endfor %} -
    -
    - {% endif %} - - {% else %} - error -

    邀请失败

    -

    {{ error_message }}

    - - {% if errors %} -
    -

    错误详情:

    -
      - {% for error in errors %} -
    • {{ error }}
    • - {% endfor %} -
    -
    - {% endif %} - {% endif %} - - {% if not needs_confirm %} - - {% endif %} -
    -
    -{% endblock %} diff --git a/templates/operations/my_cloud_computer_detail.html b/templates/operations/my_cloud_computer_detail.html deleted file mode 100755 index ba64bcd..0000000 --- a/templates/operations/my_cloud_computer_detail.html +++ /dev/null @@ -1,324 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}{{ cloud_user.username }} - 云电脑详情 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    云电脑用户详情

    -
    - -
    -
    -

    {{ cloud_user.username }}

    - - {{ cloud_user.get_status_display }} - -
    - -
    -
    -
    - 用户姓名 -

    {{ cloud_user.fullname }}

    -
    - -
    - 邮箱 -

    {{ cloud_user.email }}

    -
    - -
    - 所属产品 -

    {{ cloud_user.product.display_name }}

    -
    - -
    - 创建时间 -

    {{ cloud_user.created_at|date:"Y-m-d H:i:s" }}

    -
    -
    - -
    - 用户描述 -

    {{ cloud_user.description|default:"-" }}

    -
    - - {% if cloud_user.created_from_request %} - - {% endif %} - -
    -
    - settings_ethernet -
    连接信息
    -
    - -
    -
    - 地址: - {{ cloud_user.product.display_hostname }} -
    -
    - RDP端口: - {{ cloud_user.product.rdp_port }} -
    -
    - 用户名: - {{ cloud_user.username }} -
    -
    - - -
    - -
    - - -
    -
    - vpn_key -
    初始密码
    -
    - -
    - {% if cloud_user.password_viewed %} -
    - 密码已被查看并销毁 -
    - - -
    - -
    - {% else %} - - -
    - -
    - {% endif %} - - {% if cloud_user.password_viewed and cloud_user.password_viewed_at %} -
    - - 密码已于 {{ cloud_user.password_viewed_at|date:"Y-m-d H:i:s" }} 被查看并销毁 - -
    - {% endif %} -
    -
    - -

    - 注意:请妥善保管您的登录凭据,首次登录可能需要更改密码 -

    -
    -
    -
    - - -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/operations/my_cloud_computers.html b/templates/operations/my_cloud_computers.html deleted file mode 100755 index 2b27a6e..0000000 --- a/templates/operations/my_cloud_computers.html +++ /dev/null @@ -1,153 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}我拥有的云电脑 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    我拥有的云电脑

    -

    显示您通过开户申请创建的所有云电脑用户

    -
    - - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - - 重置 - -
    -
    -
    -
    - - - {% if cloud_users_by_product %} - {% for product_name, users in cloud_users_by_product.items %} -
    -
    - computer -

    {{ product_name }}

    -
    - -
    - {% for user in users %} -
    -
    -
    -

    {{ user.username }}

    - - {{ user.get_status_display }} - -
    -
    - -
    -
    - 用户姓名 -

    {{ user.fullname }}

    -
    - -
    - 邮箱 -

    {{ user.email }}

    -
    - -
    - 描述 -

    {{ user.description|default:'-' }}

    -
    - -
    - 创建时间 -

    {{ user.created_at|date:"Y-m-d H:i:s" }}

    -
    - - {% if user.created_from_request %} - - {% endif %} - -
    - 连接信息 -

    地址:{{ user.product.display_hostname }}:{{ user.product.rdp_port }}

    -
    -
    - - -
    - {% endfor %} -
    -
    - {% endfor %} - {% else %} -
    - desktop_windows -

    暂无云电脑用户

    -

    您还没有通过开户申请创建任何云电脑用户

    - - 立即申请云电脑 - -
    - {% endif %} - - - {% if is_paginated and paginator.num_pages > 1 %} -
    - {% if page_obj.has_previous %} - - chevron_left - - {% endif %} - - {{ page_obj.number }} / {{ paginator.num_pages }} - - {% if page_obj.has_next %} - - chevron_right - - {% endif %} -
    - {% endif %} -
    -{% endblock %} diff --git a/templates/operations/systemtask_detail.html b/templates/operations/systemtask_detail.html deleted file mode 100755 index fa940c8..0000000 --- a/templates/operations/systemtask_detail.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}系统任务详情 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    系统任务详情

    - - 返回列表 - -
    - -
    -
    -

    基本信息

    -
    -
    -
    -
    - 任务类型 -

    {{ task.task_type }}

    -
    -
    - 状态 -

    - - {{ task.get_status_display }} - -

    -
    -
    - 创建时间 -

    {{ task.created_at|date:"Y-m-d H:i:s" }}

    -
    -
    - 完成时间 -

    {% if task.completed_at %}{{ task.completed_at|date:"Y-m-d H:i:s" }}{% else %}-{% endif %}

    -
    -
    -
    -
    - - {% if task.error_message %} -
    -

    - error - 错误信息 -

    -
    {{ task.error_message }}
    -
    - {% endif %} - - {% if task.result_data %} -
    -
    -

    任务结果

    -
    -
    -
    - {% for key, value in task.result_data.items %} -
    - {{ key }} -

    {{ value }}

    -
    - {% endfor %} -
    -
    -
    - {% endif %} - - -
    -{% endblock %} diff --git a/templates/operations/systemtask_list.html b/templates/operations/systemtask_list.html deleted file mode 100755 index e118830..0000000 --- a/templates/operations/systemtask_list.html +++ /dev/null @@ -1,115 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}系统任务列表 - {{ site_name }}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
    -
    -

    系统任务列表

    -
    - - -
    -
    -
    -
    - - -
    -
    - - 重置 -
    -
    -
    -
    - - -
    -
    - - - - - - - - - - - - - {% for task in tasks %} - - - - - - - - - {% empty %} - - - - {% endfor %} - -
    任务类型状态创建时间完成时间结果操作
    {{ task.task_type }} - - {{ task.get_status_display }} - - {{ task.created_at|date:"Y-m-d H:i" }}{% if task.completed_at %}{{ task.completed_at|date:"Y-m-d H:i" }}{% else %}-{% endif %} - {% if task.error_message %} - {{ task.error_message|truncatechars:50 }} - {% else %} - - - {% endif %} - - - 查看 - -
    - task -

    暂无系统任务

    -
    -
    -
    - - - {% if is_paginated and paginator.num_pages > 1 %} -
    - {% if page_obj.has_previous %} - - 上一页 - - {% endif %} - - {{ page_obj.number }} / {{ paginator.num_pages }} - - {% if page_obj.has_next %} - - 下一页 - - {% endif %} -
    - {% endif %} -
    -{% endblock %} diff --git a/templates/tickets/dashboard.html b/templates/tickets/dashboard.html deleted file mode 100644 index cf1cd8b..0000000 --- a/templates/tickets/dashboard.html +++ /dev/null @@ -1,157 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}工单仪表盘 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    -

    - dashboard - 工单仪表盘 -

    -

    工单统计概览

    -
    - -
    - - -
    -
    -

    {{ total_tickets }}

    - 总工单数 -
    -
    -

    {{ pending_count }}

    - 待处理 -
    -
    -

    {{ processing_count }}

    - 处理中 -
    -
    -

    {{ resolved_count }}

    - 已解决 -
    -
    -

    {{ closed_count }}

    - 已关闭 -
    - {% if user.is_staff or user.is_superuser %} -
    -

    {{ overdue_count }}

    - 已超时 -
    - {% endif %} -
    - - -
    -
    -
    -
    -
    优先级分布
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    最近工单
    -
    -
    - {% for ticket in tickets %} - -
    - {{ ticket.ticket_no }} - {{ ticket.title|truncatechars:25 }} -
    - {{ ticket.get_status_display }} -
    - {% empty %} -
    -

    暂无工单

    -
    - {% endfor %} -
    -
    -
    -
    - - -
    -
    -
    快速操作
    -
    - -
    -
    -{% endblock %} - -{% block extra_js %} - - -{% endblock %} diff --git a/templates/tickets/email/assigned.html b/templates/tickets/email/assigned.html deleted file mode 100644 index dc2cbe4..0000000 --- a/templates/tickets/email/assigned.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - 工单分配通知 - - -
    -
    -

    工单分配通知

    -
    -
    -

    您好,

    -

    您已被分配处理以下工单:

    - -
    -

    {{ ticket.title }}

    -

    工单编号:{{ ticket.ticket_no }}

    -

    优先级:{{ ticket.get_priority_display }}

    -

    分类:{% if ticket.category %}{{ ticket.category.name }}{% else %}未分类{% endif %}

    -

    创建者:{{ ticket.creator.username }}

    -

    创建时间:{{ ticket.created_at|date:"Y-m-d H:i:s" }}

    -
    - -

    问题描述:

    -

    {{ ticket.description|linebreaksbr }}

    - -

    - 查看工单详情 -

    -
    -
    -

    此邮件由 2c2a 工单系统自动发送

    -

    如不需要接收此类邮件,请联系管理员

    -
    -
    - - diff --git a/templates/tickets/email/closed.html b/templates/tickets/email/closed.html deleted file mode 100644 index 2fcae6e..0000000 --- a/templates/tickets/email/closed.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - 工单已关闭 - - -
    -
    -

    工单已关闭

    -
    -
    -

    您好 {{ ticket.creator.username }},

    -

    您提交的工单已处理完成并关闭:

    - -
    -

    {{ ticket.title }}

    -

    工单编号:{{ ticket.ticket_no }}

    -

    最终状态:{{ ticket.get_status_display }}

    -

    关闭时间:{{ ticket.closed_at|date:"Y-m-d H:i:s" }}

    -
    - -

    如果您对处理结果满意,欢迎给予好评。如有其他问题,请随时提交新的工单。

    - -

    - 查看工单详情 -

    -
    -
    -

    此邮件由 2c2a 工单系统自动发送

    -
    -
    - - diff --git a/templates/tickets/email/new_comment.html b/templates/tickets/email/new_comment.html deleted file mode 100644 index 94578f3..0000000 --- a/templates/tickets/email/new_comment.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - 工单新评论 - - -
    -
    -

    工单新评论

    -
    -
    -

    您好,

    -

    工单 {{ ticket.title }} 有新评论:

    - -
    -

    工单编号:{{ ticket.ticket_no }}

    -
    - -
    -

    {{ comment.author.username }} 评论:

    -

    {{ comment.content|linebreaksbr }}

    -

    {{ comment.created_at|date:"Y-m-d H:i:s" }}

    -
    - -

    - 查看工单详情 -

    -
    -
    -

    此邮件由 2c2a 工单系统自动发送

    -
    -
    - - diff --git a/templates/tickets/email/overdue.html b/templates/tickets/email/overdue.html deleted file mode 100644 index a5fc69d..0000000 --- a/templates/tickets/email/overdue.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - 工单超时提醒 - - -
    -
    -

    工单超时提醒

    -
    -
    -

    您好 {{ ticket.assignee.username }},

    - -
    - 注意:以下工单已超时或即将超时,请尽快处理。 -
    - -
    -

    {{ ticket.title }}

    -

    工单编号:{{ ticket.ticket_no }}

    -

    优先级:{{ ticket.get_priority_display }}

    -

    截止时间:{{ ticket.due_at|date:"Y-m-d H:i:s" }}

    -

    创建者:{{ ticket.creator.username }}

    -
    - -

    - 立即处理 -

    -
    -
    -

    此邮件由 2c2a 工单系统自动发送

    -
    -
    - - diff --git a/templates/tickets/email/status_update.html b/templates/tickets/email/status_update.html deleted file mode 100644 index 6512dfc..0000000 --- a/templates/tickets/email/status_update.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - 工单状态更新 - - -
    -
    -

    工单状态更新

    -
    -
    -

    您好 {{ ticket.creator.username }},

    -

    您提交的工单状态已更新:

    - -
    -

    {{ ticket.title }}

    -

    工单编号:{{ ticket.ticket_no }}

    -
    - -
    -

    状态变更:

    -

    {{ old_status }} → {{ new_status }}

    -
    - -

    - 查看工单详情 -

    -
    -
    -

    此邮件由 2c2a 工单系统自动发送

    -
    -
    - - diff --git a/templates/tickets/my_tickets.html b/templates/tickets/my_tickets.html deleted file mode 100644 index 3b9f118..0000000 --- a/templates/tickets/my_tickets.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}我的工单 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    -

    - confirmation_number - 我的工单 -

    -

    查看您提交的所有工单

    -
    - -
    - - -
    -
    -
    - {{ filter_form.status.label_tag }} - {{ filter_form.status }} -
    -
    - - - 重置 - -
    -
    -
    - - -
    - {% for ticket in tickets %} -
    -
    -
    -
    - {{ ticket.ticket_no }} - {{ ticket.get_status_display }} -
    -
    - - {{ ticket.title|truncatechars:40 }} - -
    -

    {{ ticket.description|truncatechars:80 }}

    -
    -
    - {{ ticket.get_priority_display }} - {% if ticket.category %} - {{ ticket.category.name }} - {% endif %} -
    - {{ ticket.created_at|date:"m-d H:i" }} -
    - {% if ticket.is_overdue %} -
    - 已超时 -
    - {% endif %} -
    -
    - - {% if ticket.assignee %} - 处理人: {{ ticket.assignee.username }} - {% else %} - 待分配 - {% endif %} - - - 查看详情 - -
    -
    -
    - {% empty %} -
    -
    - inbox -

    暂无工单

    -

    您还没有提交过工单,点击下方按钮创建第一个工单

    - - add - 创建工单 - -
    -
    - {% endfor %} -
    - - - {% if is_paginated %} - - {% endif %} -
    -{% endblock %} diff --git a/templates/tickets/pending_list.html b/templates/tickets/pending_list.html deleted file mode 100644 index 90ccfa8..0000000 --- a/templates/tickets/pending_list.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}待处理工单 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -

    - pending_actions - 待处理工单 -

    -

    查看需要处理的工单

    -
    - - -
    -
    - - - - - - - - - - - - - - {% for ticket in tickets %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
    工单编号标题状态优先级创建者截止时间操作
    - {{ ticket.ticket_no }} - - - {{ ticket.title|truncatechars:30 }} - - - - {{ ticket.get_status_display }} - - - - {{ ticket.get_priority_display }} - - {{ ticket.creator.username }} - {% if ticket.due_at %} - {% if ticket.is_overdue %} - {{ ticket.due_at|date:"m-d H:i" }} (已超时) - {% else %} - {{ ticket.due_at|date:"m-d H:i" }} - {% endif %} - {% else %} - - - {% endif %} - - - 处理 - -
    - check_circle -

    暂无待处理工单

    -
    -
    -
    - - - {% if is_paginated %} - - {% endif %} -
    -{% endblock %} diff --git a/templates/tickets/ticket_detail.html b/templates/tickets/ticket_detail.html deleted file mode 100644 index f3ec706..0000000 --- a/templates/tickets/ticket_detail.html +++ /dev/null @@ -1,329 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}{{ ticket.title }} - 工单详情 - {{ site_name }}{% endblock %} - -{% block content %} -
    - {% if forbidden %} -
    - block - 您没有权限查看此工单。 -
    - 返回我的工单 - {% else %} - -
    -
    - -

    {{ ticket.title }}

    -
    - {{ ticket.get_status_display }} - {{ ticket.get_priority_display }} - {% if ticket.category %} - {{ ticket.category.name }} - {% endif %} - {% if ticket.is_overdue %} - 已超时 - {% endif %} -
    -
    -
    - {% if ticket.status != 'closed' and ticket.status != 'rejected' %} - {% if user.is_staff or user.is_superuser or user == ticket.creator %} - - {% endif %} - {% endif %} -
    -
    - -
    - -
    - -
    -
    -
    工单详情
    -
    -
    -
    -
    工单编号
    -
    {{ ticket.ticket_no }}
    -
    -
    -
    创建者
    -
    {{ ticket.creator.username }}
    -
    -
    -
    处理人
    -
    - {% if ticket.assignee or ticket.assigned_group %} - {% if ticket.assignee %}{{ ticket.assignee.username }}{% endif %}{% if ticket.assignee and ticket.assigned_group %} / {% endif %}{% if ticket.assigned_group %}{{ ticket.assigned_group.name }}(组){% endif %} - {% else %} - 未分配 - {% endif %} - {% if user.is_staff or user.is_superuser %} - - {% endif %} -
    -
    -
    -
    创建时间
    -
    {{ ticket.created_at|date:"Y-m-d H:i:s" }}
    -
    - {% if ticket.due_at %} -
    -
    截止时间
    -
    - {% if ticket.is_overdue %} - {{ ticket.due_at|date:"Y-m-d H:i:s" }} (已超时) - {% else %} - {{ ticket.due_at|date:"Y-m-d H:i:s" }} - {% endif %} -
    -
    - {% endif %} - {% if ticket.related_cloud_computer %} -
    -
    关联云电脑
    -
    {{ ticket.related_cloud_computer.username }}@{{ ticket.related_cloud_computer.product.display_name }}
    -
    - {% endif %} - {% if ticket.related_request %} -
    -
    关联申请
    -
    {{ ticket.related_request }} ({{ ticket.related_request.get_status_display }})
    -
    - {% endif %} -
    -
    -
    问题描述
    -

    {{ ticket.description|linebreaksbr }}

    -
    -
    -
    - - - {% if user.is_staff or user.is_superuser %} - {% if ticket.status != 'closed' and ticket.status != 'rejected' %} -
    -
    -
    状态变更
    -
    -
    -
    - {% csrf_token %} -
    -
    - {{ status_form.status.label_tag }} - {{ status_form.status }} -
    -
    - {{ status_form.notes.label_tag }} - {{ status_form.notes }} -
    -
    - -
    -
    -
    -
    -
    - {% endif %} - {% endif %} - - -
    -
    -
    - 评论/回复 - {{ comments.count }} -
    -
    -
    - {% if comments %} -
    - {% for comment in comments %} -
    -
    -
    - {{ comment.author.username }} - {% if comment.is_internal %} - 内部 - {% endif %} -
    - {{ comment.created_at|date:"Y-m-d H:i" }} -
    -

    {{ comment.content|linebreaksbr }}

    -
    - {% endfor %} -
    - {% else %} -

    暂无评论

    - {% endif %} -
    -
    - - - {% if ticket.status != 'closed' and ticket.status != 'rejected' %} -
    -
    -
    添加评论
    -
    -
    -
    - {% csrf_token %} -
    - {{ comment_form.content }} -
    - {% if user.is_staff or user.is_superuser %} -
    - {{ comment_form.is_internal }} - {{ comment_form.is_internal.label_tag }} -
    - {% endif %} - -
    -
    -
    - {% endif %} -
    - - -
    -
    -
    -
    活动记录
    -
    -
    - {% if activities %} -
    - {% for activity in activities %} -
    -
    - {{ activity.get_action_display }} - {{ activity.created_at|date:"m-d H:i" }} -
    -

    {{ activity.description }}

    - {% if activity.actor %} - 操作人: {{ activity.actor.username }} - {% endif %} -
    - {% endfor %} -
    - {% else %} -

    暂无活动记录

    - {% endif %} -
    -
    -
    -
    - - - {% if user.is_staff or user.is_superuser %} - - {% endif %} - - - - {% endif %} -
    -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/tickets/ticket_form.html b/templates/tickets/ticket_form.html deleted file mode 100644 index 2b948f9..0000000 --- a/templates/tickets/ticket_form.html +++ /dev/null @@ -1,114 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}创建工单 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    -
    -
    -

    - add_circle - 创建工单 -

    -
    -
    -
    - {% csrf_token %} - -
    - - {{ form.title }} - {% if form.title.errors %} -
    - {{ form.title.errors }} -
    - {% endif %} - {{ form.title.help_text }} -
    - -
    - - {{ form.category }} - {% if form.category.errors %} -
    - {{ form.category.errors }} -
    - {% endif %} - {{ form.category.help_text }} -
    - -
    - - {{ form.priority }} - {% if form.priority.errors %} -
    - {{ form.priority.errors }} -
    - {% endif %} - {{ form.priority.help_text }} -
    - -
    - - {{ form.description }} - {% if form.description.errors %} -
    - {{ form.description.errors }} -
    - {% endif %} - {{ form.description.help_text }} -
    - -
    -
    - - {{ form.related_cloud_computer }} - {% if form.related_cloud_computer.errors %} -
    - {{ form.related_cloud_computer.errors }} -
    - {% endif %} - {{ form.related_cloud_computer.help_text }} -
    -
    - - {{ form.related_request }} - {% if form.related_request.errors %} -
    - {{ form.related_request.errors }} -
    - {% endif %} - {{ form.related_request.help_text }} -
    -
    - -
    - - 取消 - - -
    -
    -
    -
    -
    -
    -
    -{% endblock %} diff --git a/templates/tickets/ticket_list.html b/templates/tickets/ticket_list.html deleted file mode 100644 index 16d7ae1..0000000 --- a/templates/tickets/ticket_list.html +++ /dev/null @@ -1,147 +0,0 @@ -{% extends 'base.html' %} - -{% block title %}工单列表 - {{ site_name }}{% endblock %} - -{% block content %} -
    -
    -
    -

    - confirmation_number - 工单列表 -

    -

    查看和管理所有工单

    -
    - -
    - - -
    -
    -
    - {{ filter_form.status.label_tag }} - {{ filter_form.status }} -
    -
    - {{ filter_form.priority.label_tag }} - {{ filter_form.priority }} -
    -
    - {{ filter_form.category.label_tag }} - {{ filter_form.category }} -
    -
    - {{ filter_form.search.label_tag }} - {{ filter_form.search }} -
    -
    - - - 重置 - -
    -
    -
    - - -
    -
    - - - - - - - - - - - - - - - - {% for ticket in tickets %} - - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
    工单编号标题分类状态优先级创建者处理人创建时间操作
    - {{ ticket.ticket_no }} - - - {{ ticket.title|truncatechars:30 }} - - - {% if ticket.category %} - {{ ticket.category.name }} - {% else %} - - - {% endif %} - - - {{ ticket.get_status_display }} - - - - {{ ticket.get_priority_display }} - - {{ ticket.creator.username }} - {% if ticket.assignee or ticket.assigned_group %} - {% if ticket.assignee %}{{ ticket.assignee.username }}{% endif %}{% if ticket.assignee and ticket.assigned_group %} / {% endif %}{% if ticket.assigned_group %}{{ ticket.assigned_group.name }}(组){% endif %} - {% else %} - 未分配 - {% endif %} - {{ ticket.created_at|date:"Y-m-d H:i" }} - - 查看 - -
    - inbox -

    暂无工单

    - - 创建第一个工单 - -
    -
    -
    - - - {% if is_paginated %} - - {% endif %} -
    -{% endblock %} diff --git a/tickets/__init__.py b/tickets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tickets/admin.py b/tickets/admin.py deleted file mode 100644 index 07fb639..0000000 --- a/tickets/admin.py +++ /dev/null @@ -1 +0,0 @@ -# 工单系统无需后台管理 diff --git a/tickets/apps.py b/tickets/apps.py deleted file mode 100644 index 45a7d76..0000000 --- a/tickets/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class TicketsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'tickets' diff --git a/tickets/migrations/__init__.py b/tickets/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tickets/models.py b/tickets/models.py deleted file mode 100644 index 71a8362..0000000 --- a/tickets/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/tickets/tests/__init__.py b/tickets/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tickets/views.py b/tickets/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/tickets/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100755 index 0bb439a..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -2c2a工具模块 -""" diff --git a/utils/ca_bundle.py b/utils/ca_bundle.py deleted file mode 100644 index 237d489..0000000 --- a/utils/ca_bundle.py +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env python3 -""" -WinRM PKI 证书生成器 - -生成 WinRM HTTPS 所需的 CA、服务器、客户端证书(含 UPN SAN)。 -依赖: pip install cryptography -""" - -import datetime -import ipaddress -import shutil -from pathlib import Path - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.serialization import ( - BestAvailableEncryption, - Encoding, - NoEncryption, - PrivateFormat, - pkcs12, -) -from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID, ObjectIdentifier - -# ============================================================ -# 配置项(按需修改) -# ============================================================ -WINDOWS_IP = "192.168.122.234" -WINDOWS_HOSTNAME = "WIN-R8S5ITT8IC9" -OUTPUT_DIR = "winrm-pki" -VALIDITY_DAYS = 3650 -PFX_PASSWORD = b"changeit" -UPN_VALUE = "test@localhost" -UPN_OID = "1.3.6.1.4.1.311.20.2.3" - - -# ============================================================ -# 基础工具函数 -# ============================================================ - -def ensure_output_dir(output_dir: str) -> Path: - """确保输出目录存在并切换工作目录。""" - path = Path(output_dir) - path.mkdir(parents=True, exist_ok=True) - import os - os.chdir(path) - return path - - -def generate_ec_key() -> ec.EllipticCurvePrivateKey: - """生成 EC 私钥(prime256v1 / P-256 曲线)。""" - return ec.generate_private_key(ec.SECP256R1(), default_backend()) - - -def save_private_key(key: ec.EllipticCurvePrivateKey, filename: str) -> None: - """将私钥保存为 PEM 文件(SEC1 格式,与 openssl ecparam 输出一致)。""" - pem = key.private_bytes( - encoding=Encoding.PEM, - format=PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=NoEncryption(), - ) - Path(filename).write_bytes(pem) - print(f" 已保存私钥: {filename}") - - -def save_certificate(cert: x509.Certificate, filename: str) -> None: - """将证书保存为 PEM 文件。""" - pem = cert.public_bytes(Encoding.PEM) - Path(filename).write_bytes(pem) - print(f" 已保存证书: {filename}") - - -def export_pfx( - cert: x509.Certificate, - key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, - password: bytes, - filename: str, -) -> None: - """将证书 + 私钥 + CA 证书导出为 PKCS#12 (.pfx) 文件。""" - pfx_data = pkcs12.serialize_key_and_certificates( - name=None, - key=key, - cert=cert, - cas=[ca_cert], - encryption_algorithm=BestAvailableEncryption(password), - ) - Path(filename).write_bytes(pfx_data) - print(f" 已导出 PFX: {filename}") - - -def _validity_period(): - """返回证书的有效期起止时间。""" - now = datetime.datetime.now(datetime.timezone.utc) - return now, now + datetime.timedelta(days=VALIDITY_DAYS) - - -def _encode_upn_other_name(upn: str) -> x509.OtherName: - """ - 编码 UPN OtherName(OID 1.3.6.1.4.1.311.20.2.3)。 - 值为 DER 编码的 UTF8String。 - """ - oid = ObjectIdentifier(UPN_OID) - encoded = upn.encode("utf-8") - # DER: tag(0x0C=UTF8String) + length + value - der_value = b"\x0c" + bytes([len(encoded)]) + encoded - return x509.OtherName(oid, der_value) - - -# ============================================================ -# 步骤 1:创建 CA -# ============================================================ - -def build_ca_certificate(ca_key: ec.EllipticCurvePrivateKey) -> x509.Certificate: - """创建自签名 CA 证书(对应 bash 中 ca.cnf 的扩展配置)。""" - subject = issuer = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, "WinRM-CA"), - ]) - not_before, not_after = _validity_period() - - builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(ca_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - # basicConstraints = critical, CA:TRUE - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), critical=True - ) - # keyUsage = critical, digitalSignature, keyCertSign, cRLSign - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_cert_sign=True, - crl_sign=True, - content_commitment=False, - key_encipherment=False, - data_encipherment=False, - key_agreement=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - # subjectKeyIdentifier = hash - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), - critical=False, - ) - # authorityKeyIdentifier = keyid:always, issuer - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), - critical=False, - ) - ) - return builder.sign(ca_key, hashes.SHA256(), default_backend()) - - -def create_ca() -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: - """步骤 1:创建 CA 私钥和自签名证书。""" - print("\n" + "=" * 50) - print("1. 创建 CA") - print("=" * 50) - - ca_key = generate_ec_key() - save_private_key(ca_key, "ca.key") - - ca_cert = build_ca_certificate(ca_key) - save_certificate(ca_cert, "ca.crt") - - print_ca_info(ca_cert) - return ca_key, ca_cert - - -# ============================================================ -# 步骤 2:签发服务器证书 -# ============================================================ - -def build_server_csr( - server_key: ec.EllipticCurvePrivateKey, hostname: str -) -> x509.CertificateSigningRequest: - """生成服务器证书签名请求。""" - builder = x509.CertificateSigningRequestBuilder().subject_name( - x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)]) - ) - return builder.sign(server_key, hashes.SHA256(), default_backend()) - - -def sign_server_certificate( - csr: x509.CertificateSigningRequest, - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, - hostname: str, - ip_address: str, -) -> x509.Certificate: - """用 CA 签发服务器证书(对应 bash 中 server_ext.cnf 的扩展配置)。""" - not_before, not_after = _validity_period() - - san = x509.SubjectAlternativeName([ - x509.DNSName(hostname), - x509.IPAddress(ipaddress.ip_address(ip_address)), - ]) - - builder = ( - x509.CertificateBuilder() - .subject_name(csr.subject) - .issuer_name(ca_cert.subject) - .public_key(csr.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - # basicConstraints = CA:FALSE - .add_extension( - x509.BasicConstraints(ca=False, path_length=None), critical=False - ) - # keyUsage = critical, digitalSignature, keyEncipherment - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_encipherment=True, - content_commitment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - data_encipherment=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - # extendedKeyUsage = serverAuth - .add_extension( - x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), - critical=False, - ) - # subjectAltName - .add_extension(san, critical=False) - # subjectKeyIdentifier = hash - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(csr.public_key()), - critical=False, - ) - # authorityKeyIdentifier = keyid,issuer - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), - critical=False, - ) - ) - return builder.sign(ca_key, hashes.SHA256(), default_backend()) - - -def create_server_cert( - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, -) -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: - """步骤 2:签发服务器证书。""" - print("\n" + "=" * 50) - print("2. 签发服务器证书") - print("=" * 50) - - server_key = generate_ec_key() - save_private_key(server_key, "server.key") - - csr = build_server_csr(server_key, WINDOWS_HOSTNAME) - server_cert = sign_server_certificate( - csr, ca_key, ca_cert, WINDOWS_HOSTNAME, WINDOWS_IP - ) - save_certificate(server_cert, "server.crt") - - export_pfx(server_cert, server_key, ca_cert, PFX_PASSWORD, "server.pfx") - - print_server_info(server_cert) - return server_key, server_cert - - -# ============================================================ -# 步骤 3:签发客户端证书(含 UPN SAN) -# ============================================================ - -def build_client_csr( - client_key: ec.EllipticCurvePrivateKey, -) -> x509.CertificateSigningRequest: - """生成客户端证书签名请求。""" - builder = x509.CertificateSigningRequestBuilder().subject_name( - x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "winrm-client")]) - ) - return builder.sign(client_key, hashes.SHA256(), default_backend()) - - -def sign_client_certificate( - csr: x509.CertificateSigningRequest, - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, -) -> x509.Certificate: - """用 CA 签发客户端证书(对应 bash 中 client_ext.cnf,含 UPN SAN)。""" - not_before, not_after = _validity_period() - - san = x509.SubjectAlternativeName([_encode_upn_other_name(UPN_VALUE)]) - - builder = ( - x509.CertificateBuilder() - .subject_name(csr.subject) - .issuer_name(ca_cert.subject) - .public_key(csr.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - # basicConstraints = CA:FALSE - .add_extension( - x509.BasicConstraints(ca=False, path_length=None), critical=False - ) - # keyUsage = critical, digitalSignature - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_encipherment=False, - content_commitment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - data_encipherment=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - # extendedKeyUsage = clientAuth - .add_extension( - x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), - critical=False, - ) - # subjectKeyIdentifier = hash - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(csr.public_key()), - critical=False, - ) - # authorityKeyIdentifier = keyid,issuer - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), - critical=False, - ) - # ★ subjectAltName = UPN otherName ★ - .add_extension(san, critical=False) - ) - return builder.sign(ca_key, hashes.SHA256(), default_backend()) - - -def create_client_cert( - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, -) -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: - """步骤 3:签发客户端证书。""" - print("\n" + "=" * 50) - print("3. 签发客户端证书") - print("=" * 50) - - client_key = generate_ec_key() - save_private_key(client_key, "client.key") - - csr = build_client_csr(client_key) - client_cert = sign_client_certificate(csr, ca_key, ca_cert) - save_certificate(client_cert, "client.crt") - - # 复制为兼容命名的 PEM 文件 - shutil.copy2("client.crt", "client-cert.pem") - shutil.copy2("client.key", "client-key.pem") - print(" 已复制: client.crt -> client-cert.pem") - print(" 已复制: client.key -> client-key.pem") - - export_pfx(client_cert, client_key, ca_cert, PFX_PASSWORD, "client.pfx") - - print_client_info(client_cert) - return client_key, client_cert - - -# ============================================================ -# 证书信息打印 -# ============================================================ - -def print_ca_info(cert: x509.Certificate) -> None: - """打印 CA 证书关键信息。""" - print("\n=== CA 证书 ===") - print(f" Subject: {cert.subject.rfc4514_string()}") - for ext in cert.extensions: - if isinstance(ext.value, x509.BasicConstraints): - print(f" Basic Constraints: CA={ext.value.ca}") - elif isinstance(ext.value, x509.KeyUsage): - usages = [] - if ext.value.digital_signature: - usages.append("digitalSignature") - if ext.value.key_cert_sign: - usages.append("keyCertSign") - if ext.value.crl_sign: - usages.append("cRLSign") - print(f" Key Usage: {', '.join(usages)}") - fingerprint = cert.fingerprint(hashes.SHA256()).hex(":") - print(f" SHA256 Fingerprint: {fingerprint}") - - -def print_server_info(cert: x509.Certificate) -> None: - """打印服务器证书关键信息。""" - print("\n=== 服务器证书 ===") - print(f" Subject: {cert.subject.rfc4514_string()}") - print(f" Issuer: {cert.issuer.rfc4514_string()}") - - -def print_client_info(cert: x509.Certificate) -> None: - """打印客户端证书关键信息(含 SAN)。""" - print("\n=== 客户端证书 ===") - print(f" Subject: {cert.subject.rfc4514_string()}") - print(f" Issuer: {cert.issuer.rfc4514_string()}") - print(" --- SAN ---") - for ext in cert.extensions: - if isinstance(ext.value, x509.SubjectAlternativeName): - for name in ext.value: - if isinstance(name, x509.OtherName): - print( - f" OtherName (OID {name.type_id.dotted_string}): " - f"{name.value!r}" - ) - - -def print_summary() -> None: - """打印最终汇总信息。""" - print("\n" + "=" * 50) - print("生成完毕!需要导入 Windows 的文件:") - print(" ca.crt → LocalMachine\\Root") - print(" server.pfx → LocalMachine\\My") - print(" client.crt → LocalMachine\\TrustedPeople") - print("=" * 50) - - -# ============================================================ -# 主入口 -# ============================================================ - -def main() -> None: - """主入口:按顺序执行 CA → 服务器证书 → 客户端证书。""" - ensure_output_dir(OUTPUT_DIR) - - ca_key, ca_cert = create_ca() - create_server_cert(ca_key, ca_cert) - create_client_cert(ca_key, ca_cert) - - print_summary() - - -if __name__ == "__main__": - main() diff --git a/utils/cert_service.py b/utils/cert_service.py deleted file mode 100644 index b3c43f1..0000000 --- a/utils/cert_service.py +++ /dev/null @@ -1,272 +0,0 @@ -import datetime -import ipaddress -import secrets -import string - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.serialization import ( - BestAvailableEncryption, - pkcs12, -) -from cryptography.x509.oid import ( - ExtendedKeyUsageOID, - NameOID, - ObjectIdentifier, -) - - -def generate_ec_key() -> ec.EllipticCurvePrivateKey: - return ec.generate_private_key(ec.SECP256R1(), default_backend()) - - -def generate_ca( - ca_name: str = "WinRM-CA", - validity_days: int = 3650, -) -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: - ca_key = generate_ec_key() - subject = issuer = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, ca_name), - ]) - now = datetime.datetime.now(datetime.timezone.utc) - not_before = now - not_after = now + datetime.timedelta(days=validity_days) - builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(ca_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - .add_extension( - x509.BasicConstraints(ca=True, path_length=None), - critical=True, - ) - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_cert_sign=True, - crl_sign=True, - content_commitment=False, - key_encipherment=False, - data_encipherment=False, - key_agreement=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), - critical=False, - ) - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key( - ca_key.public_key() - ), - critical=False, - ) - ) - ca_cert = builder.sign(ca_key, hashes.SHA256(), default_backend()) - return ca_key, ca_cert - - -def issue_server_cert( - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, - hostname: str, - ip_address: str | None = None, - validity_days: int = 3650, - pfx_password: str | None = None, -) -> dict: - server_key = generate_ec_key() - subject = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, hostname), - ]) - now = datetime.datetime.now(datetime.timezone.utc) - not_before = now - not_after = now + datetime.timedelta(days=validity_days) - san_entries = [x509.DNSName(hostname)] - if ip_address: - try: - san_entries.append( - x509.IPAddress(ipaddress.ip_address(ip_address)) - ) - except ValueError: - # Invalid IP input is intentionally ignored; DNS SAN is still used. - pass - san = x509.SubjectAlternativeName(san_entries) - builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(ca_cert.subject) - .public_key(server_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - .add_extension( - x509.BasicConstraints(ca=False, path_length=None), - critical=False, - ) - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_encipherment=True, - content_commitment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - data_encipherment=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - .add_extension( - x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), - critical=False, - ) - .add_extension(san, critical=False) - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(server_key.public_key()), - critical=False, - ) - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key( - ca_key.public_key() - ), - critical=False, - ) - ) - server_cert = builder.sign(ca_key, hashes.SHA256(), default_backend()) - if pfx_password is None: - pfx_password = generate_random_pfx_password() - pfx_data = export_pfx( - server_cert, server_key, ca_cert, - pfx_password.encode("utf-8"), - ) - return { - "server_key": server_key, - "server_cert": server_cert, - "pfx_data": pfx_data, - "pfx_password": pfx_password, - } - - -def issue_client_cert( - ca_key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, - upn_value: str, - validity_days: int = 3650, -) -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: - client_key = generate_ec_key() - subject = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, "winrm-client"), - ]) - now = datetime.datetime.now(datetime.timezone.utc) - not_before = now - not_after = now + datetime.timedelta(days=validity_days) - san = x509.SubjectAlternativeName([ - encode_upn_other_name(upn_value), - ]) - builder = ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(ca_cert.subject) - .public_key(client_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(not_before) - .not_valid_after(not_after) - .add_extension( - x509.BasicConstraints(ca=False, path_length=None), - critical=False, - ) - .add_extension( - x509.KeyUsage( - digital_signature=True, - key_encipherment=False, - content_commitment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - data_encipherment=False, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - .add_extension( - x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), - critical=False, - ) - .add_extension( - x509.SubjectKeyIdentifier.from_public_key(client_key.public_key()), - critical=False, - ) - .add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key( - ca_key.public_key() - ), - critical=False, - ) - .add_extension(san, critical=False) - ) - client_cert = builder.sign(ca_key, hashes.SHA256(), default_backend()) - return client_key, client_cert - - -def encode_upn_other_name(upn: str) -> x509.OtherName: - oid = ObjectIdentifier("1.3.6.1.4.1.311.20.2.3") - encoded = upn.encode("utf-8") - der_value = b"\x0c" + bytes([len(encoded)]) + encoded - return x509.OtherName(oid, der_value) - - -def export_pfx( - cert: x509.Certificate, - key: ec.EllipticCurvePrivateKey, - ca_cert: x509.Certificate, - password_bytes: bytes, -) -> bytes: - return pkcs12.serialize_key_and_certificates( - name=None, - key=key, - cert=cert, - cas=[ca_cert], - encryption_algorithm=BestAvailableEncryption(password_bytes), - ) - - -def generate_random_pfx_password(length: int = 16) -> str: - alphabet = string.ascii_letters + string.digits - return "".join(secrets.choice(alphabet) for _ in range(length)) - - -def generate_random_username(prefix: str = "c2a_", length: int = 8) -> str: - alphabet = string.ascii_lowercase + string.digits - return prefix + "".join(secrets.choice(alphabet) for _ in range(length)) - - -def generate_random_password(length: int = 16) -> str: - if length < 4: - raise ValueError( - "Password length must be at least 4 " - "to meet complexity requirements" - ) - parts = [ - secrets.choice(string.ascii_uppercase), - secrets.choice(string.ascii_lowercase), - secrets.choice(string.digits), - secrets.choice("!@#$%^&*"), - ] - remaining = length - len(parts) - pool = string.ascii_letters + string.digits + "!@#$%^&*" - parts.extend(secrets.choice(pool) for _ in range(remaining)) - shuffled = list(parts) - for i in range(len(shuffled) - 1, 0, -1): - j = secrets.randbelow(i + 1) - shuffled[i], shuffled[j] = shuffled[j], shuffled[i] - return "".join(shuffled) diff --git a/utils/cert_storage.py b/utils/cert_storage.py deleted file mode 100644 index 0d051cd..0000000 --- a/utils/cert_storage.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import re -import secrets -import shutil -from pathlib import Path -from django.conf import settings - - -def _sanitizePathComponent(value: str) -> str: - """过滤非字母数字字符,防止路径穿越""" - return re.sub(r'[^a-zA-Z0-9]', '', value) - - -def get_cert_base_dir(): - return Path(settings.MEDIA_ROOT) / 'certificates' - - -def get_cert_dir(cert_root: str, cert_sub: str): - return ( - get_cert_base_dir() - / _sanitizePathComponent(cert_root) - / _sanitizePathComponent(cert_sub) - ) - - -def generate_cert_paths(): - cert_root = secrets.token_hex(1) - cert_sub = secrets.token_hex(1) - return cert_root, cert_sub - - -def save_cert_files( - cert_root: str, - cert_sub: str, - ca_cert_pem: bytes, - client_cert_pem: bytes, - server_pfx_bytes: bytes, - client_key_pem: bytes | None = None, -): - cert_dir = get_cert_dir(cert_root, cert_sub) - cert_dir.mkdir(parents=True, exist_ok=True) - - ca_cert_path = cert_dir / 'ca.crt' - ca_cert_path.write_bytes(ca_cert_pem) - os.chmod(ca_cert_path, 0o600) - - client_cert_path = cert_dir / 'client.crt' - client_cert_path.write_bytes(client_cert_pem) - os.chmod(client_cert_path, 0o600) - - if client_key_pem: - client_key_path = cert_dir / 'client.key' - client_key_path.write_bytes(client_key_pem) - os.chmod(client_key_path, 0o600) - - server_pfx_path = cert_dir / 'server.pfx' - server_pfx_path.write_bytes(server_pfx_bytes) - os.chmod(server_pfx_path, 0o600) - - return cert_dir - - -def delete_cert_files(cert_root: str, cert_sub: str): - cert_dir = get_cert_dir(cert_root, cert_sub) - if cert_dir.exists(): - shutil.rmtree(cert_dir) - parent_dir = cert_dir.parent - try: - parent_dir.rmdir() - except OSError: - # Best-effort cleanup: parent may be non-empty or changed concurrently. - pass - return True - return False - - -def get_cert_file_paths(cert_root: str, cert_sub: str): - cert_dir = get_cert_dir(cert_root, cert_sub) - return { - 'ca_cert': cert_dir / 'ca.crt', - 'client_cert': cert_dir / 'client.crt', - 'client_key': cert_dir / 'client.key', - 'server_pfx': cert_dir / 'server.pfx', - } - - -def get_ca_base_dir(): - return Path(settings.MEDIA_ROOT) / 'certificates' / 'ca' - - -def get_ca_dir(cert_root: str, cert_sub: str): - return ( - get_ca_base_dir() - / _sanitizePathComponent(cert_root) - / _sanitizePathComponent(cert_sub) - ) - - -def generate_ca_paths(): - cert_root = secrets.token_hex(1) - cert_sub = secrets.token_hex(1) - return cert_root, cert_sub - - -def save_ca_files(cert_root: str, cert_sub: str, ca_key_pem: bytes, ca_cert_pem: bytes): - ca_dir = get_ca_dir(cert_root, cert_sub) - ca_dir.mkdir(parents=True, exist_ok=True) - - key_path = ca_dir / 'ca.key' - key_path.write_bytes(ca_key_pem) - os.chmod(key_path, 0o600) - - cert_path = ca_dir / 'ca.crt' - cert_path.write_bytes(ca_cert_pem) - os.chmod(cert_path, 0o600) - - return ca_dir - - -def get_ca_file_paths(cert_root: str, cert_sub: str): - ca_dir = get_ca_dir(cert_root, cert_sub) - return { - 'key': ca_dir / 'ca.key', - 'cert': ca_dir / 'ca.crt', - } - - -def delete_ca_files(cert_root: str, cert_sub: str): - ca_dir = get_ca_dir(cert_root, cert_sub) - if ca_dir.exists(): - shutil.rmtree(ca_dir) - parent_dir = ca_dir.parent - try: - parent_dir.rmdir() - except OSError: - # Best-effort cleanup: parent may be non-empty or changed concurrently. - pass - return True - return False diff --git a/utils/crypto.py b/utils/crypto.py deleted file mode 100644 index c382567..0000000 --- a/utils/crypto.py +++ /dev/null @@ -1,28 +0,0 @@ -import base64 -import hashlib -from django.conf import settings -from cryptography.fernet import Fernet - -_fernet_instance = None - -def get_fernet(): - global _fernet_instance - if _fernet_instance is None: - key = hashlib.sha256(settings.SECRET_KEY.encode()).digest() - _fernet_instance = Fernet(base64.urlsafe_b64encode(key)) - return _fernet_instance - -def encrypt_value(plaintext): - if not plaintext: - return '' - f = get_fernet() - return f.encrypt(plaintext.encode()).decode() - -def decrypt_value(ciphertext): - if not ciphertext: - return '' - f = get_fernet() - try: - return f.decrypt(ciphertext.encode()).decode() - except Exception: - raise ValueError("解密失败") diff --git a/utils/disk_quota.py b/utils/disk_quota.py deleted file mode 100644 index ab7dfe1..0000000 --- a/utils/disk_quota.py +++ /dev/null @@ -1,336 +0,0 @@ -""" -磁盘配额管理工具 - -通过 WinRM 或本地命令管理 Windows 磁盘配额。 -使用 fsutil quota 和 PowerShell 管理NTFS磁盘配额。 -""" -import json -import logging -import os -import re -from typing import Dict, List, Optional, Any - -logger = logging.getLogger("2c2a") - -DISK_LETTER_PATTERN = re.compile(r'^[A-Za-z]:\\?$') -MB_TO_BYTES = 1024 * 1024 - - -def validate_disk_letter(disk_letter: str) -> str: - disk_letter = disk_letter.strip().upper() - if not DISK_LETTER_PATTERN.match(disk_letter): - raise ValueError(f"无效的磁盘盘符: {disk_letter}") - return disk_letter.rstrip('\\') - - -def validate_quota_value(value: Any, field_name: str = "配额值") -> int: - try: - v = int(value) - except (TypeError, ValueError): - raise ValueError(f"{field_name}必须为数字") - if v < 0: - raise ValueError(f"{field_name}不能为负数") - return v - - -def get_disk_info_via_client(client) -> List[Dict[str, Any]]: - """ - 通过客户端获取磁盘信息列表 - - Args: - client: WinrmClient 或 LocalWinServerClient 实例 - - Returns: - List[Dict]: 磁盘信息列表,每项包含 drive, total_mb, free_mb - """ - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info("DEMO模式: 返回模拟磁盘信息") - return [ - {"drive": "C:", "total_mb": 102400, "free_mb": 51200}, - {"drive": "D:", "total_mb": 204800, "free_mb": 102400}, - ] - - script = ''' -$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" -$result = @() -foreach ($disk in $disks) { - $result += [PSCustomObject]@{ - Drive = $disk.DeviceID - TotalMB = [math]::Round($disk.Size / 1MB) - FreeMB = [math]::Round($disk.FreeSpace / 1MB) - } -} -$result | ConvertTo-Json -Compress -''' - try: - result = client.execute_powershell(script) - if result.status_code == 0 and result.std_out.strip(): - output = result.std_out.strip() - try: - disks = json.loads(output) - except json.JSONDecodeError: - disks = [] - - if isinstance(disks, dict): - disks = [disks] - - disk_list = [] - for d in disks: - disk_list.append({ - "drive": d.get("Drive", ""), - "total_mb": d.get("TotalMB", 0), - "free_mb": d.get("FreeMB", 0), - }) - return disk_list - else: - logger.error(f"获取磁盘信息失败: {result.std_err}") - return [] - except Exception as e: - logger.error(f"获取磁盘信息异常: {str(e)}") - return [] - - -def set_disk_quota_via_client(client, username: str, disk_letter: str, quota_mb: int, warning_mb: Optional[int] = None) -> Dict[str, Any]: - """ - 通过客户端设置磁盘配额 - - Args: - client: WinrmClient 或 LocalWinServerClient 实例 - username: Windows 用户名 - disk_letter: 磁盘盘符,如 "C:" - quota_mb: 配额大小(MB) - warning_mb: 警告阈值(MB),默认为配额的80% - - Returns: - Dict: {"success": bool, "message": str} - """ - validate_disk_letter(disk_letter) - validate_quota_value(quota_mb, "配额大小") - - if warning_mb is None: - warning_mb = int(quota_mb * 0.8) - else: - validate_quota_value(warning_mb, "警告阈值") - - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f"DEMO模式: 模拟设置用户 {username} 在 {disk_letter} 的配额为 {quota_mb}MB") - return {"success": True, "message": f"DEMO模式: 已设置用户 {username} 在 {disk_letter} 的配额为 {quota_mb}MB"} - - disk_letter = disk_letter.upper().rstrip('\\') - quota_bytes = quota_mb * MB_TO_BYTES - warning_bytes = warning_mb * MB_TO_BYTES - - script = f''' -$ErrorActionPreference = 'Stop' -$drive = "{disk_letter}" -$username = "{username}" -$quotaBytes = [long]{quota_bytes} -$warningBytes = [long]{warning_bytes} - -try {{ - $vol = Get-CimInstance Win32_Volume -Filter "DriveLetter='$drive'" -ErrorAction Stop - if (-not $vol) {{ - Write-Error "找不到卷 $drive" - exit 1 - }} - - if (-not $vol.QuotasEnabled) {{ - $enforceOutput = & fsutil quota enforce $drive 2>&1 - if ($LASTEXITCODE -ne 0) {{ - Write-Error "启用配额失败: $enforceOutput" - exit 1 - }} - $vol = Get-CimInstance Win32_Volume -Filter "DriveLetter='$drive'" -ErrorAction Stop - if (-not $vol.QuotasEnabled) {{ - Write-Error "无法启用卷 $drive 的磁盘配额" - exit 1 - }} - }} - - $user = Get-LocalUser -Name $username -ErrorAction SilentlyContinue - if (-not $user) {{ - $user = Get-CimInstance Win32_UserAccount -Filter "Name='$username'" -ErrorAction SilentlyContinue - if (-not $user) {{ - Write-Error "用户 $username 不存在" - exit 1 - }} - }} - - $modifyOutput = & fsutil quota modify $drive $warningBytes $quotaBytes $username 2>&1 - if ($LASTEXITCODE -ne 0) {{ - Write-Error "设置用户配额失败: $modifyOutput" - exit 1 - }} - - Write-Output "SUCCESS" -}} catch {{ - Write-Error $_.Exception.Message - exit 1 -}} -''' - try: - result = client.execute_powershell(script) - if result.status_code == 0 and "SUCCESS" in result.std_out: - logger.info(f"设置磁盘配额成功: 用户={username}, 磁盘={disk_letter}, 配额={quota_mb}MB") - return {"success": True, "message": f"已设置用户 {username} 在 {disk_letter} 的配额为 {quota_mb}MB"} - else: - error_msg = result.std_err.strip() if result.std_err else "未知错误" - logger.error(f"设置磁盘配额失败: {error_msg}") - return {"success": False, "message": f"设置磁盘配额失败: {error_msg}"} - except Exception as e: - logger.error(f"设置磁盘配额异常: {str(e)}") - return {"success": False, "message": f"设置磁盘配额异常: {str(e)}"} - - -def get_disk_quota_via_client(client, username: str, disk_letter: str) -> Dict[str, Any]: - """ - 通过客户端获取用户磁盘配额 - - Args: - client: WinrmClient 或 LocalWinServerClient 实例 - username: Windows 用户名 - disk_letter: 磁盘盘符 - - Returns: - Dict: {"success": bool, "quota_mb": int, "warning_mb": int, "used_mb": int} - """ - validate_disk_letter(disk_letter) - - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f"DEMO模式: 模拟获取用户 {username} 在 {disk_letter} 的配额") - return {"success": True, "quota_mb": 10240, "warning_mb": 8192, "used_mb": 5120} - - disk_letter = disk_letter.upper().rstrip('\\') - - script = f''' -$ErrorActionPreference = 'Stop' -$drive = "{disk_letter}" -$username = "{username}" - -try {{ - $account = New-Object System.Security.Principal.NTAccount($username) - $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) - - $quota = Get-CimInstance Win32_DiskQuota -Filter "VolumePath='$drive\\' AND UserSID='$($sid.Value)'" -ErrorAction Stop - if ($quota) {{ - $result = [PSCustomObject]@{{ - QuotaMB = [math]::Round($quota.Limit / 1MB) - WarningMB = [math]::Round($quota.WarningLimit / 1MB) - UsedMB = [math]::Round($quota.DiskSpaceUsed / 1MB) - }} - $result | ConvertTo-Json -Compress - }} else {{ - Write-Output "NO_QUOTA" - }} -}} catch {{ - Write-Error $_.Exception.Message - exit 1 -}} -''' - try: - result = client.execute_powershell(script) - if result.status_code == 0: - output = result.std_out.strip() - if "NO_QUOTA" in output: - return {"success": True, "quota_mb": 0, "warning_mb": 0, "used_mb": 0} - try: - data = json.loads(output) - return { - "success": True, - "quota_mb": data.get("QuotaMB", 0), - "warning_mb": data.get("WarningMB", 0), - "used_mb": data.get("UsedMB", 0), - } - except json.JSONDecodeError: - return {"success": False, "quota_mb": 0, "warning_mb": 0, "used_mb": 0} - else: - return {"success": False, "quota_mb": 0, "warning_mb": 0, "used_mb": 0} - except Exception as e: - logger.error(f"获取磁盘配额异常: {str(e)}") - return {"success": False, "quota_mb": 0, "warning_mb": 0, "used_mb": 0} - - -def remove_disk_quota_via_client(client, username: str, disk_letter: str) -> Dict[str, Any]: - """ - 通过客户端删除用户磁盘配额 - - Args: - client: WinrmClient 或 LocalWinServerClient 实例 - username: Windows 用户名 - disk_letter: 磁盘盘符 - - Returns: - Dict: {"success": bool, "message": str} - """ - validate_disk_letter(disk_letter) - - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f"DEMO模式: 模拟删除用户 {username} 在 {disk_letter} 的配额") - return {"success": True, "message": f"DEMO模式: 已删除用户 {username} 在 {disk_letter} 的配额"} - - disk_letter = disk_letter.upper().rstrip('\\') - - script = f''' -$ErrorActionPreference = 'Stop' -$drive = "{disk_letter}" -$username = "{username}" - -try {{ - $account = New-Object System.Security.Principal.NTAccount($username) - $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) - - $quota = Get-CimInstance Win32_DiskQuota -Filter "VolumePath='$drive\\' AND UserSID='$($sid.Value)'" -ErrorAction Stop - if ($quota) {{ - Remove-CimInstance -InputObject $quota -ErrorAction Stop - Write-Output "SUCCESS" - }} else {{ - Write-Output "NO_QUOTA" - }} -}} catch {{ - Write-Error $_.Exception.Message - exit 1 -}} -''' - try: - result = client.execute_powershell(script) - if result.status_code == 0 and ("SUCCESS" in result.std_out or "NO_QUOTA" in result.std_out): - logger.info(f"删除磁盘配额成功: 用户={username}, 磁盘={disk_letter}") - return {"success": True, "message": f"已删除用户 {username} 在 {disk_letter} 的配额"} - else: - error_msg = result.std_err.strip() if result.std_err else "未知错误" - return {"success": False, "message": f"删除磁盘配额失败: {error_msg}"} - except Exception as e: - logger.error(f"删除磁盘配额异常: {str(e)}") - return {"success": False, "message": f"删除磁盘配额异常: {str(e)}"} - - -def set_user_disk_quotas(client, username: str, quota_config: Dict[str, int]) -> Dict[str, Any]: - """ - 批量设置用户磁盘配额 - - Args: - client: WinrmClient 或 LocalWinServerClient 实例 - username: Windows 用户名 - quota_config: 配额配置,如 {"C:": 10240, "D:": 20480} - - Returns: - Dict: {"success": bool, "results": list, "errors": list} - """ - results = [] - errors = [] - - for disk_letter, quota_mb in quota_config.items(): - try: - validate_quota_value(quota_mb, f"磁盘 {disk_letter} 配额") - result = set_disk_quota_via_client(client, username, disk_letter, quota_mb) - results.append({"disk": disk_letter, "result": result}) - if not result["success"]: - errors.append(f"{disk_letter}: {result['message']}") - except ValueError as e: - errors.append(f"{disk_letter}: {str(e)}") - - return { - "success": len(errors) == 0, - "results": results, - "errors": errors, - } diff --git a/utils/error_handlers.py b/utils/error_handlers.py deleted file mode 100755 index 4ca413d..0000000 --- a/utils/error_handlers.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -异常处理工具模块 -提供安全的异常处理和错误包装 -""" -import logging -from typing import Optional, Any, Dict -from django.db import DatabaseError, IntegrityError -from django.core.exceptions import ValidationError, PermissionDenied -from rest_framework.exceptions import APIException - -logger = logging.getLogger('2c2a') - - -class SecurityException(Exception): - """安全相关异常""" - pass - - -class WinRMConnectionException(Exception): - """WinRM 连接异常""" - pass - - -class InvalidUserInputException(Exception): - """用户输入无效异常""" - pass - - -def safe_exception_handler(func): - """安全的异常处理装饰器""" - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except SecurityException as e: - # 安全异常,记录但不暴露详情 - logger.warning(f"Security exception in {func.__name__}: {str(e)}") - raise Exception("操作被拒绝,请联系管理员") - except WinRMConnectionException as e: - # WinRM 连接异常,提供有用信息 - logger.error(f"WinRM connection error in {func.__name__}: {str(e)}") - raise Exception("无法连接到远程主机,请检查主机状态和网络连接") - except (DatabaseError, IntegrityError) as e: - # 数据库异常,不暴露具体错误 - logger.error(f"Database error in {func.__name__}: {type(e).__name__}") - raise Exception("数据处理失败,请稍后重试") - except ValidationError as e: - # 验证异常,可以返回原始信息 - logger.info(f"Validation error in {func.__name__}: {str(e)}") - raise - except PermissionDenied as e: - # 权限异常 - logger.warning(f"Permission denied in {func.__name__}: {str(e)}") - raise Exception("您没有执行此操作的权限") - except APIException as e: - # API 异常,保持原样 - raise - except ValueError as e: - # 值错误,提供有用信息 - logger.info(f"Value error in {func.__name__}: {str(e)}") - raise Exception(f"无效的输入: {str(e)}") - except Exception as e: - # 其他未预期的异常 - from django.conf import settings - logger.error(f"Unexpected error in {func.__name__}: {type(e).__name__}") - # 在生产环境中不暴露内部错误 - if hasattr(settings, 'DEBUG') and settings.DEBUG: - raise - else: - raise Exception("发生未知错误,请联系技术支持") - return wrapper - - -def sanitize_error_message(error_msg: str, user_friendly: bool = True) -> str: - """ - 清理错误消息,移除敏感信息 - - Args: - error_msg: 原始错误消息 - user_friendly: 是否返回用户友好的消息 - - Returns: - 清理后的错误消息 - """ - # 敏感信息模式 - sensitive_patterns = [ - r'password\s*[:=]\s*\S+', - r'pwd\s*[:=]\s*\S+', - r'secret\s*[:=]\s*\S+', - r'token\s*[:=]\s*\S+', - r'key\s*[:=]\s*\S+', - r'\\Users\\\w+', # Windows 用户名 - r'/home/\w+', # Linux 用户名 - r'\d+\.\d+\.\d+\.\d+', # IP 地址(部分脱敏) - ] - - import re - sanitized = error_msg - - for pattern in sensitive_patterns: - sanitized = re.sub(pattern, '***', sanitized, flags=re.IGNORECASE) - - if user_friendly: - # 替换技术术语为用户友好的消息 - replacements = { - 'NameResolutionError': '无法解析主机名', - 'ConnectionRefusedError': '连接被拒绝,请检查服务是否运行', - 'TimeoutError': '连接超时,请检查网络连接', - 'AuthenticationError': '认证失败,请检查用户名和密码', - 'AccessDenied': '访问被拒绝,权限不足', - 'NotFound': '请求的资源不存在', - } - - for tech_term, friendly_msg in replacements.items(): - if tech_term in sanitized: - sanitized = friendly_msg - break - - return sanitized - - -def create_error_response(error: Exception, request=None) -> Dict[str, Any]: - """ - 创建标准的错误响应 - - Args: - error: 异常对象 - request: HTTP 请求对象(可选) - - Returns: - 错误响应字典 - """ - from django.conf import settings - - error_type = type(error).__name__ - error_message = str(error) - - # 清理错误消息 - if not (hasattr(settings, 'DEBUG') and settings.DEBUG): - error_message = sanitize_error_message(error_message) - - response = { - 'success': False, - 'error': { - 'type': error_type, - 'message': error_message, - } - } - - # 添加追踪 ID(如果有) - import uuid - response['trace_id'] = str(uuid.uuid4()) - - # 添加请求信息(如果有) - if request: - response['request_id'] = getattr(request, 'request_id', None) - response['user'] = request.user.username if request.user.is_authenticated else 'anonymous' - - return response \ No newline at end of file diff --git a/utils/gateway_client.py b/utils/gateway_client.py deleted file mode 100644 index 9d1a14b..0000000 --- a/utils/gateway_client.py +++ /dev/null @@ -1,150 +0,0 @@ -import logging -from typing import Any, Dict, Optional - -logger = logging.getLogger('2c2a') - - -class GatewayError(Exception): - pass - - -def _get_gateway_service(): - from plugins.core.plugin_manager import get_plugin_manager - from plugins.gateway.interfaces import GatewayServiceInterface - - pm = get_plugin_manager() - service = pm.get_service('gateway') - if service is not None and isinstance(service, GatewayServiceInterface): - return service - return None - - -def is_gateway_enabled() -> bool: - service = _get_gateway_service() - if service is not None: - return service.is_enabled() - from django.conf import settings - return getattr(settings, 'GATEWAY_ENABLED', False) - - -class GatewayClient: - _instance = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self, socket_path: Optional[str] = None): - if hasattr(self, '_initialized'): - return - self._initialized = True - - def _get_service(self): - return _get_gateway_service() - - @property - def enabled(self) -> bool: - service = self._get_service() - if service: - return service.is_enabled() - return False - - def _is_available(self) -> bool: - service = self._get_service() - if service: - return service.is_available() - return False - - def tunnel_kick(self, token: str) -> bool: - service = self._get_service() - if service: - return service.tunnel_kick(token) - return False - - def tunnel_stats(self, token: Optional[str] = None) -> Optional[Any]: - service = self._get_service() - if service: - return service.tunnel_stats(token) - return None - - def rdp_session_stats(self) -> Optional[Any]: - service = self._get_service() - if service: - return service.rdp_session_stats() - return None - - def rdp_session_kick(self, session_id: str) -> bool: - service = self._get_service() - if service: - return service.rdp_session_kick(session_id) - return False - - def remote_exec( - self, - token: str, - script: bytes, - encrypted_key: Optional[bytes] = None, - signature: Optional[bytes] = None, - pub_key_id: Optional[str] = None, - ) -> Optional[Dict[str, Any]]: - service = self._get_service() - if service: - return service.remote_exec( - token, script, encrypted_key, signature, pub_key_id - ) - return None - - def issue_paa_token( - self, user_email: str, tunnel_token: str, - client_ip: Optional[str] = None, expires_in: int = 600 - ) -> str: - service = self._get_service() - if service: - return service.issue_paa_token( - user_email, tunnel_token, client_ip, expires_in - ) - return '' - - def generate_rdp_file( - self, gateway_address: str, gateway_port: int, - user_email: str, paa_token: str, - enable_clipboard: bool = True, enable_printers: bool = True, - enable_drive: bool = True, enable_port: bool = False, - enable_pnp: bool = False - ) -> str: - service = self._get_service() - if service: - return service.generate_rdp_file( - gateway_address, gateway_port, user_email, paa_token, - enable_clipboard, enable_printers, enable_drive, - enable_port, enable_pnp - ) - return '' - - -class GatewayEventListener: - def __init__(self, socket_path: Optional[str] = None): - self._socket_path = socket_path - self._running = False - self._handlers = {} - - def register_handler(self, event_type: str, handler): - self._handlers[event_type] = handler - - def start(self): - from plugins.core.plugin_manager import get_plugin_manager - pm = get_plugin_manager() - plugin = pm.get_plugin('gateway') - if plugin and hasattr(plugin, 'get_event_listener'): - listener = plugin.get_event_listener(self._socket_path) - for event_type, handler in self._handlers.items(): - listener.register_handler(event_type, handler) - listener.start() - else: - logger.warning( - 'Gateway plugin not available, event listener not starting' - ) - - def stop(self): - self._running = False diff --git a/utils/helpers.py b/utils/helpers.py deleted file mode 100755 index c9faf04..0000000 --- a/utils/helpers.py +++ /dev/null @@ -1,424 +0,0 @@ -""" -辅助函数模块 -提供项目中常用的辅助函数 -""" -import re -import json -from datetime import datetime, timedelta -from typing import Optional, List, Dict, Any, Union -from django.utils import timezone -from django.conf import settings - - -def get_client_ip(request) -> Optional[str]: - """获取客户端真实 IP 地址(支持 nginx 反向代理) - - 优先级: - 1. 当请求来自可信代理时,依次尝试 X-Forwarded-For(取第一个非可信 IP)、X-Real-IP - 2. 否则直接使用 REMOTE_ADDR - """ - remote_addr = request.META.get('REMOTE_ADDR', '') - - # 检查请求是否来自可信代理 - trusted_proxies = getattr(settings, 'TRUSTED_PROXY_IPS', None) - if trusted_proxies and remote_addr in trusted_proxies: - # X-Forwarded-For: client, proxy1, proxy2 — 取最右边的非可信 IP - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ips = [ip.strip() for ip in x_forwarded_for.split(',')] - # 从右往左找第一个非可信代理的 IP - for ip in reversed(ips): - if ip not in trusted_proxies: - return ip - # 全部都是可信代理,取最左边的 - return ips[0] - - # 尝试 X-Real-IP(nginx 默认设置) - x_real_ip = request.META.get('HTTP_X_REAL_IP') - if x_real_ip: - return x_real_ip.strip() - - # 兼容旧配置 USE_X_FORWARDED_FOR - if getattr(settings, 'USE_X_FORWARDED_FOR', False): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - return x_forwarded_for.split(',')[0].strip() - - return remote_addr or None - - -def validate_ip_address(ip: str) -> bool: - """ - 验证IP地址格式是否正确 - - Args: - ip: 待验证的IP地址字符串 - - Returns: - bool: IP地址格式是否有效 - """ - pattern = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' - return bool(re.match(pattern, ip)) - - -def validate_port(port: Union[int, str]) -> bool: - """ - 验证端口号是否有效 - - Args: - port: 待验证的端口号 - - Returns: - bool: 端口号是否有效(1-65535) - """ - try: - port_num = int(port) - return 1 <= port_num <= 65535 - except (ValueError, TypeError): - return False - - -def format_datetime(dt: datetime, format_str: str = '%Y-%m-%d %H:%M:%S') -> str: - """ - 格式化日期时间 - - Args: - dt: 日期时间对象 - format_str: 格式化字符串,默认为 '%Y-%m-%d %H:%M:%S' - - Returns: - str: 格式化后的日期时间字符串 - """ - if dt is None: - return '' - return dt.strftime(format_str) - - -def parse_datetime(dt_str: str, format_str: str = '%Y-%m-%d %H:%M:%S') -> Optional[datetime]: - """ - 解析日期时间字符串 - - Args: - dt_str: 日期时间字符串 - format_str: 格式化字符串,默认为 '%Y-%m-%d %H:%M:%S' - - Returns: - datetime: 解析后的日期时间对象,如果解析失败则返回None - """ - try: - return datetime.strptime(dt_str, format_str) - except (ValueError, TypeError): - return None - - -def get_time_range(days: int = 7) -> tuple: - """ - 获取指定天数的时间范围 - - Args: - days: 天数,默认为7天 - - Returns: - tuple: (开始时间, 结束时间) 的元组 - """ - end_time = timezone.now() - start_time = end_time - timedelta(days=days) - return start_time, end_time - - -def safe_json_loads(json_str: str, default: Any = None) -> Any: - """ - 安全地解析JSON字符串 - - Args: - json_str: JSON字符串 - default: 解析失败时的默认返回值 - - Returns: - 解析后的对象或默认值 - """ - try: - return json.loads(json_str) - except (json.JSONDecodeError, TypeError): - return default - - -def safe_json_dumps(obj: Any, default: str = '{}', **kwargs) -> str: - """ - 安全地序列化对象为JSON字符串 - - Args: - obj: 待序列化的对象 - default: 序列化失败时的默认返回值 - **kwargs: 传递给json.dumps的额外参数 - - Returns: - str: JSON字符串或默认值 - """ - try: - return json.dumps(obj, **kwargs) - except (TypeError, ValueError): - return default - - -def mask_sensitive_data(data: str, mask_char: str = '*', visible_chars: int = 4) -> str: - """ - 掩码处理敏感数据 - - Args: - data: 待处理的字符串 - mask_char: 掩码字符,默认为 '*' - visible_chars: 保留可见的字符数,默认为4 - - Returns: - str: 掩码处理后的字符串 - """ - if not data or len(data) <= visible_chars: - return mask_char * len(data) if data else data - - return data[:visible_chars] + mask_char * (len(data) - visible_chars) - - -def truncate_string(text: str, max_length: int = 100, suffix: str = '...') -> str: - """ - 截断字符串 - - Args: - text: 待截断的字符串 - max_length: 最大长度,默认为100 - suffix: 截断后添加的后缀,默认为 '...' - - Returns: - str: 截断后的字符串 - """ - if not text or len(text) <= max_length: - return text - - return text[:max_length - len(suffix)] + suffix - - -def format_file_size(size_bytes: int) -> str: - """ - 格式化文件大小 - - Args: - size_bytes: 文件大小(字节) - - Returns: - str: 格式化后的文件大小字符串 - """ - if size_bytes == 0: - return '0B' - - size_names = ['B', 'KB', 'MB', 'GB', 'TB'] - i = 0 - while size_bytes >= 1024 and i < len(size_names) - 1: - size_bytes /= 1024.0 - i += 1 - - return f'{size_bytes:.2f}{size_names[i]}' - - -def generate_random_string(length: int = 32, - include_uppercase: bool = True, - include_lowercase: bool = True, - include_digits: bool = True, - include_special_chars: bool = False) -> str: - """ - 生成随机字符串 - - Args: - length: 字符串长度,默认为32 - include_uppercase: 是否包含大写字母,默认为True - include_lowercase: 是否包含小写字母,默认为True - include_digits: 是否包含数字,默认为True - include_special_chars: 是否包含特殊字符,默认为False - - Returns: - str: 生成的随机字符串 - """ - import secrets as _secrets - import string - - chars = '' - if include_uppercase: - chars += string.ascii_uppercase - if include_lowercase: - chars += string.ascii_lowercase - if include_digits: - chars += string.digits - if include_special_chars: - chars += '!@#$%^&*()_+-=[]{}|;:,.<>?' - - if not chars: - chars = string.ascii_letters + string.digits - - return ''.join(_secrets.choice(chars) for _ in range(length)) - - -def validate_email(email: str) -> bool: - """ - 验证电子邮件地址格式 - - Args: - email: 待验证的电子邮件地址 - - Returns: - bool: 电子邮件地址格式是否有效 - """ - pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' - return bool(re.match(pattern, email)) - - -def is_valid_hostname(hostname: str) -> bool: - """ - 验证主机名是否有效 - - Args: - hostname: 待验证的主机名 - - Returns: - bool: 主机名是否有效 - """ - if not hostname or len(hostname) > 253: - return False - - # 检查是否为IP地址 - if validate_ip_address(hostname): - return True - - # 检查主机名格式 - hostname_pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$' - return bool(re.match(hostname_pattern, hostname)) - - -def get_setting(key: str, default: Any = None) -> Any: - """ - 获取Django设置值 - - Args: - key: 设置键名 - default: 默认值 - - Returns: - 设置值或默认值 - """ - return getattr(settings, key, default) - - -def chunk_list(lst: List[Any], chunk_size: int) -> List[List[Any]]: - """ - 将列表分块 - - Args: - lst: 待分块的列表 - chunk_size: 每块的大小 - - Returns: - List[List[Any]]: 分块后的列表 - """ - return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)] - - -def merge_dicts(*dicts: Dict[Any, Any]) -> Dict[Any, Any]: - """ - 合并多个字典 - - Args: - *dicts: 待合并的字典 - - Returns: - Dict[Any, Any]: 合并后的字典 - """ - result = {} - for d in dicts: - if isinstance(d, dict): - result.update(d) - return result - - -def deep_update_dict(base_dict: Dict[Any, Any], - update_dict: Dict[Any, Any]) -> Dict[Any, Any]: - """ - 深度更新字典 - - Args: - base_dict: 基础字典 - update_dict: 更新字典 - - Returns: - Dict[Any, Any]: 更新后的字典 - """ - for key, value in update_dict.items(): - if isinstance(value, dict) and key in base_dict and isinstance(base_dict[key], dict): - base_dict[key] = deep_update_dict(base_dict[key], value) - else: - base_dict[key] = value - return base_dict - - -def format_duration(seconds: float) -> str: - """ - 格式化持续时间 - - Args: - seconds: 持续时间(秒) - - Returns: - str: 格式化后的持续时间字符串 - """ - if seconds < 0: - return '0秒' - - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - secs = int(seconds % 60) - - parts = [] - if hours > 0: - parts.append(f'{hours}小时') - if minutes > 0: - parts.append(f'{minutes}分钟') - if secs > 0 or not parts: - parts.append(f'{secs}秒') - - return ''.join(parts) - - -def is_valid_url(url: str) -> bool: - """ - 验证URL是否有效 - - Args: - url: 待验证的URL - - Returns: - bool: URL是否有效 - """ - pattern = r'^https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[/\w .-]*/?$' - return bool(re.match(pattern, url)) - - -def sanitize_filename(filename: str) -> str: - """ - 清理文件名,移除不安全的字符 - - Args: - filename: 待清理的文件名 - - Returns: - str: 清理后的文件名 - """ - # 移除路径分隔符和其他危险字符 - unsafe_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\x00'] - for char in unsafe_chars: - filename = filename.replace(char, '_') - - # 移除前后空格 - filename = filename.strip() - - # 确保文件名不为空 - if not filename: - filename = 'unnamed' - - return filename diff --git a/utils/local_winserver_client.py b/utils/local_winserver_client.py deleted file mode 100755 index ccfbdb0..0000000 --- a/utils/local_winserver_client.py +++ /dev/null @@ -1,611 +0,0 @@ -""" -本地WinServer客户端工具 - -该模块提供了与本地Windows服务器交互的客户端实现, -用于执行本地命令和PowerShell脚本。 - -主要功能: -- 执行本地PowerShell命令和脚本 -- 管理本地用户账户 -- 提供本地系统管理功能 - -使用示例: - client = LocalWinServerClient("username", "password") - result = client.execute_command("ipconfig") - if result.success: - print(result.std_out) -""" -import logging -import subprocess -import os -import re -from dataclasses import dataclass -from typing import Optional, Dict, Any -from django.conf import settings - -logger = logging.getLogger("2c2a") - -USERNAME_PATTERN = re.compile(r'^[a-zA-Z0-9_]{1,150}$') -GROUPNAME_PATTERN = re.compile(r'^[a-zA-Z0-9_\-\s]{1,256}$') -MAX_STRING_LENGTH = 4096 - - -class CommandInjectionError(Exception): - pass - - -def validate_username(username: str) -> str: - if not username: - raise CommandInjectionError("用户名不能为空") - if len(username) > 150: - raise CommandInjectionError("用户名长度不能超过150个字符") - if not USERNAME_PATTERN.match(username): - raise CommandInjectionError("用户名格式无效: 只允许字母、数字和下划线") - return username - - -def validate_groupname(group: str) -> str: - if not group: - raise CommandInjectionError("组名不能为空") - if len(group) > 256: - raise CommandInjectionError("组名长度不能超过256个字符") - if not GROUPNAME_PATTERN.match(group): - raise CommandInjectionError("组名格式无效: 只允许字母、数字、下划线、连字符和空格") - return group - - -def validate_string_length(s: str, max_length: int = MAX_STRING_LENGTH, field_name: str = "输入") -> str: - if s and len(s) > max_length: - raise CommandInjectionError(f"{field_name}长度不能超过{max_length}个字符") - return s - - -def _escape_ps_string(s: str) -> str: - if not s: - return s - if len(s) > MAX_STRING_LENGTH: - raise CommandInjectionError(f"字符串长度超过最大限制 {MAX_STRING_LENGTH}") - return s.replace('\x00', '').replace('`', '``').replace('"', '`"').replace('$', '`$').replace('\n', '`n').replace('\r', '`r') - - -@dataclass -class LocalWinServerResult: - """ - 本地WinServer执行结果的数据类 - - 属性: - status_code: 命令执行的状态码,0表示成功 - std_out: 标准输出内容 - std_err: 标准错误内容 - success: 命令是否执行成功的布尔值 - """ - status_code: int - std_out: str - std_err: str - - @property - def success(self) -> bool: - """判断命令是否执行成功""" - return self.status_code == 0 - - -class LocalWinServerClient: - """ - 本地WinServer客户端封装类 - - 用于与本地Windows服务器进行交互,绕过网络连接限制, - 直接执行本地PowerShell命令和系统管理任务。 - - 属性: - username: 本地管理员用户名 - password: 本地管理员密码 - timeout: 操作超时时间(秒) - max_retries: 最大重试次数 - """ - - def __init__( - self, - username: str, - password: str, - timeout: Optional[int] = None, - max_retries: Optional[int] = None - ): - """ - 初始化本地WinServer客户端 - - 参数: - username: 本地管理员用户名 - password: 本地管理员密码 - timeout: 操作超时时间(秒),默认使用配置文件中的值 - max_retries: 最大重试次数,默认使用配置文件中的值 - """ - self.username = username - self.password = password - self.timeout = timeout or getattr(settings, 'WINRM_TIMEOUT', 30) - self.max_retries = max_retries or getattr(settings, 'WINRM_MAX_RETRIES', 3) - - logger.info( - f"初始化本地WinServer客户端: 用户={username}, " - f"超时={self.timeout}秒, 最大重试={self.max_retries}次" - ) - - def execute_command( - self, - command: str, - arguments: Optional[list] = None - ) -> LocalWinServerResult: - """ - 执行本地命令(通过PowerShell) - - 参数: - command: 要执行的命令 - arguments: 命令参数列表 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - # 如果是DEMO模式,模拟执行命令而不实际执行 - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info(f"DEMO模式: 模拟执行本地命令: {command}, 参数: {arguments}") - # 模拟成功执行的结果 - return LocalWinServerResult( - status_code=0, - std_out="Command executed successfully in demo mode", - std_err="" - ) - - logger.info(f"执行本地命令: {command}, 参数: {arguments}") - - try: - # 构建PowerShell命令 - if arguments: - cmd_parts = [command] + [str(arg) for arg in arguments] - ps_command = ' '.join(cmd_parts) - else: - ps_command = command - - # 使用PowerShell执行命令 - full_command = ['powershell.exe', '-Command', ps_command] - - # 执行命令 - result = subprocess.run( - full_command, - capture_output=True, - text=True, - timeout=self.timeout - ) - - local_result = LocalWinServerResult( - status_code=result.returncode, - std_out=result.stdout, - std_err=result.stderr - ) - - if local_result.success: - logger.info(f"本地命令执行成功: {command}") - else: - logger.warning( - f"本地命令执行返回非零状态码: {command}, " - f"状态码={result.returncode}, 错误={result.stderr}" - ) - - return local_result - except subprocess.TimeoutExpired: - logger.error(f"本地命令执行超时: {command}") - return LocalWinServerResult( - status_code=-1, - std_out="", - std_err=f"命令执行超时 ({self.timeout}秒)" - ) - except Exception as e: - logger.error(f"本地命令执行失败: {command}, 错误: {str(e)}") - return LocalWinServerResult( - status_code=-1, - std_out="", - std_err=str(e) - ) - - def execute_powershell( - self, - script: str, - arguments: Optional[Dict[str, Any]] = None - ) -> LocalWinServerResult: - """ - 执行本地PowerShell脚本 - - 参数: - script: 要执行的PowerShell脚本 - arguments: 脚本参数字典 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - # 如果是DEMO模式,模拟执行PowerShell而不实际执行 - if os.environ.get('2C2A_DEMO', '').lower() == '1': - logger.info("DEMO模式: 模拟执行本地PowerShell脚本") - # 模拟成功执行的结果 - return LocalWinServerResult( - status_code=0, - std_out="PowerShell script executed successfully in demo mode", - std_err="" - ) - - logger.info("执行本地PowerShell脚本") - - try: - # 使用PowerShell执行脚本 - full_command = ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-Command', script] - - # 执行命令 - result = subprocess.run( - full_command, - capture_output=True, - text=True, - timeout=self.timeout - ) - - local_result = LocalWinServerResult( - status_code=result.returncode, - std_out=result.stdout, - std_err=result.stderr - ) - - if local_result.success: - logger.info(f"本地PowerShell脚本执行成功") - else: - logger.warning( - f"本地PowerShell脚本执行返回非零状态码: " - f"状态码={result.returncode}, 错误={result.stderr}" - ) - - return local_result - except subprocess.TimeoutExpired: - logger.error(f"本地PowerShell脚本执行超时") - return LocalWinServerResult( - status_code=-1, - std_out="", - std_err=f"PowerShell脚本执行超时 ({self.timeout}秒)" - ) - except Exception as e: - logger.error(f"本地PowerShell脚本执行失败: 错误: {str(e)}") - return LocalWinServerResult( - status_code=-1, - std_out="", - std_err=str(e) - ) - - def create_user( - self, - username: str, - password: str, - description: Optional[str] = None, - group: Optional[str] = None - ) -> LocalWinServerResult: - """ - 创建本地用户 - - 参数: - username: 用户名 - password: 密码 - description: 用户描述 - group: 要加入的用户组 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - validate_username(username) - validate_string_length(password, field_name="密码") - desc = _escape_ps_string(description or '') - escaped_password = _escape_ps_string(password) - escaped_username = _escape_ps_string(username) - script = f''' - $password = ConvertTo-SecureString "{escaped_password}" -AsPlainText -Force - $user = New-LocalUser -Name "{escaped_username}" -Password $password -Description "{desc}" -ErrorAction Stop - ''' - - if group: - validate_groupname(group) - escaped_group = _escape_ps_string(group) - script = script + f''' - Add-LocalGroupMember -Group "{escaped_group}" -Member "{escaped_username}" -ErrorAction Stop - ''' - - logger.info(f"创建本地用户: {username}, 组: {group}") - result = self.execute_powershell(script) - - if result.success: - logger.info(f"本地用户创建成功: {username}") - else: - logger.error(f"本地用户创建失败: {username}, 错误: {result.std_err}") - - return result - - def delete_user(self, username: str) -> LocalWinServerResult: - """ - 删除本地用户 - - 参数: - username: 要删除的用户名 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f''' - Remove-LocalUser -Name "{escaped_username}" -ErrorAction Stop - ''' - - logger.info(f"删除本地用户: {username}") - result = self.execute_powershell(script) - - if result.success: - logger.info(f"本地用户删除成功: {username}") - else: - logger.error(f"本地用户删除失败: {username}, 错误: {result.std_err}") - - return result - - def enable_user(self, username: str) -> LocalWinServerResult: - """ - 启用本地用户 - - 参数: - username: 用户名 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f''' - Enable-LocalUser -Name "{escaped_username}" -ErrorAction Stop - ''' - - logger.info(f"启用本地用户: {username}") - result = self.execute_powershell(script) - - if result.success: - logger.info(f"本地用户启用成功: {username}") - - return result - - def disable_user(self, username: str) -> LocalWinServerResult: - """ - 禁用本地用户 - - 参数: - username: 用户名 - - 返回: - LocalWinServerResult对象,包含执行结果 - """ - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f''' - Disable-LocalUser -Name "{escaped_username}" -ErrorAction Stop - ''' - - logger.info(f"禁用本地用户: {username}") - result = self.execute_powershell(script) - - if result.success: - logger.info(f"本地用户禁用成功: {username}") - - return result - - def get_user_info(self, username: str) -> LocalWinServerResult: - """ - 获取本地用户信息 - - 参数: - username: 用户名 - - 返回: - LocalWinServerResult对象,包含用户信息的JSON格式数据 - """ - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f''' - Get-LocalUser -Name "{escaped_username}" | ConvertTo-Json - ''' - - logger.info(f"获取本地用户信息: {username}") - return self.execute_powershell(script) - - def list_users(self) -> LocalWinServerResult: - """ - 列出所有本地用户 - - 返回: - LocalWinServerResult对象,包含用户列表的JSON格式数据 - """ - script = ''' - Get-LocalUser | ConvertTo-Json - ''' - - logger.info("列出所有本地用户") - return self.execute_powershell(script) - - def check_user_exists(self, username: str) -> bool: - """ - 检查本地用户是否存在 - - 参数: - username: 要检查的用户名 - - 返回: - bool: 用户存在返回True,否则返回False - """ - try: - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f''' - $user = Get-LocalUser -Name "{escaped_username}" -ErrorAction Stop - $true - ''' - result = self.execute_powershell(script) - exists = result.success and 'True' in result.std_out - logger.info(f"检查本地用户是否存在: {username}, 结果: {exists}") - return exists - except Exception as e: - logger.error(f"检查本地用户存在性时出错: {username}, 错误: {str(e)}") - return False - - def get_password_policy(self) -> Dict[str, Any]: - """ - 动态获取本地密码策略要求 - - 返回: - Dict: 包含密码策略信息的字典 - """ - try: - script = f''' - secedit /export /cfg "$env:TEMP\\secpol.cfg" | Out-Null - Get-Content "$env:TEMP\\secpol.cfg" | Where-Object {{ $_ -match '^(MinimumPasswordLength|PasswordComplexity|PasswordHistorySize|MaximumPasswordAge|MinimumPasswordAge)\\s*=' }} - Remove-Item "$env:TEMP\\secpol.cfg" -ErrorAction SilentlyContinue - ''' - result = self.execute_powershell(script) - - policy = {} - if result.success: - lines = result.std_out.strip().split("\n") - for line in lines: - line = line.strip() - if line.startswith("MinimumPasswordLength"): - try: - policy["minimum_length"] = int(line.split("=")[1].strip()) - except: - policy["minimum_length"] = 8 # 默认值 - elif line.startswith("PasswordComplexity"): - try: - policy["complexity_required"] = bool(int(line.split("=")[1].strip())) - except: - policy["complexity_required"] = True # 默认值 - elif line.startswith("PasswordHistorySize"): - try: - policy["history_size"] = int(line.split("=")[1].strip()) - except: - policy["history_size"] = 0 # 默认值 - elif line.startswith("MaximumPasswordAge"): - try: - policy["max_age_days"] = int(line.split("=")[1].strip()) - except: - policy["max_age_days"] = 0 # 默认值 - elif line.startswith("MinimumPasswordAge"): - try: - policy["min_age_days"] = int(line.split("=")[1].strip()) - except: - policy["min_age_days"] = 0 # 默认值 - - # 设置默认值 - if "minimum_length" not in policy: - policy["minimum_length"] = 8 - if "complexity_required" not in policy: - policy["complexity_required"] = True - - logger.info(f"获取本地密码策略成功: {policy}") - return policy - except Exception as e: - logger.error(f"获取本地密码策略失败: 错误: {str(e)}") - # 返回默认密码策略 - return { - "minimum_length": 8, - "complexity_required": True, - "history_size": 0, - "max_age_days": 42, - "min_age_days": 1 - } - - def generate_strong_password(self, length: Optional[int] = None) -> str: - """ - 根据密码策略生成强密码 - - 参数: - length: 密码长度,默认根据服务器策略确定 - - 返回: - str: 生成的强密码 - """ - import secrets - import string - - # 获取服务器密码策略 - policy = self.get_password_policy() - - # 确定密码长度 - actual_length = length or max(policy["minimum_length"], 12) # 默认至少12位 - - if policy["complexity_required"]: - # 密码复杂性要求:至少包含大写字母、小写字母、数字和特殊字符 - uppercase = secrets.choice(string.ascii_uppercase) - lowercase = secrets.choice(string.ascii_lowercase) - digit = secrets.choice(string.digits) - special_char = secrets.choice("!@#$%^&*()_+-=[]{}|;:,.<>?") - - # 剩余部分随机生成 - remaining_length = max(0, actual_length - 4) - alphabet = string.ascii_letters + string.digits + "!@#$%^&*()_+-=[]{}|;:,.<>?" - rest = "".join(secrets.choice(alphabet) for i in range(remaining_length)) - - # 打乱顺序以确保安全 - password_chars = list(uppercase + lowercase + digit + special_char + rest) - secrets.SystemRandom().shuffle(password_chars) - password = "".join(password_chars) - else: - # 不需要复杂性要求,简单生成随机密码 - alphabet = string.ascii_letters + string.digits - password = "".join(secrets.choice(alphabet) for i in range(actual_length)) - - logger.info(f"生成本地强密码完成,长度: {len(password)}") - return password - - def grant_admin_privileges(self, username: str) -> bool: - """ - 为指定用户授予管理员权限 - - 参数: - username: 用户名 - - 返回: - bool: 是否成功授予权限 - """ - try: - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f'net localgroup Administrators "{escaped_username}" /add' - result = self.execute_powershell(script) - if result.success: - logger.info(f"为本地用户{username}授予管理员权限成功") - return True - else: - logger.error(f"为本地用户{username}授予管理员权限失败: 错误: {result.std_err}") - return False - except Exception as e: - logger.error(f"为本地用户{username}授予管理员权限失败: 错误: {str(e)}") - return False - - def revoke_admin_privileges(self, username: str) -> bool: - """ - 撤销指定用户的管理员权限 - - 参数: - username: 用户名 - - 返回: - bool: 是否成功撤销权限 - """ - try: - validate_username(username) - escaped_username = _escape_ps_string(username) - script = f'net localgroup Administrators "{escaped_username}" /delete' - result = self.execute_powershell(script) - if result.success: - logger.info(f"撤销本地用户{username}的管理员权限成功") - return True - else: - logger.error(f"撤销本地用户{username}的管理员权限失败: 错误: {result.std_err}") - return False - except Exception as e: - logger.error(f"撤销本地用户{username}的管理员权限失败: 错误: {str(e)}") - return False \ No newline at end of file diff --git a/utils/production_checker.py b/utils/production_checker.py deleted file mode 100755 index 647c7a5..0000000 --- a/utils/production_checker.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -生产环境安全配置检查器 -确保部署前进行安全验证 -""" -import os -import sys -from django.conf import settings - - -def check_production_readiness(): - """检查生产环境配置的安全性""" - errors = [] - warnings = [] - - # 1. 检查 SECRET_KEY - if not os.environ.get('DJANGO_SECRET_KEY'): - if not settings.DEBUG: - errors.append("生产环境必须设置 DJANGO_SECRET_KEY 环境变量") - else: - warnings.append("未设置 DJANGO_SECRET_KEY,系统将生成临时密钥") - - if settings.DEBUG and not settings.DEBUG: - errors.append("生产环境不得启用 DEBUG 模式") - - if not settings.DEBUG: - allowed_hosts = settings.ALLOWED_HOSTS - if not allowed_hosts or allowed_hosts == ['*'] or 'localhost' in allowed_hosts: - errors.append("生产环境必须设置有效的 ALLOWED_HOSTS,不能使用 * 或 localhost") - - if not settings.DEBUG: - if not getattr(settings, 'SECURE_SSL_REDIRECT', False): - warnings.append("建议启用 SECURE_SSL_REDIRECT 强制 HTTPS 重定向") - - if not getattr(settings, 'SECURE_HSTS_SECONDS', 0): - warnings.append("建议启用 HSTS (HTTP Strict Transport Security)") - - if not getattr(settings, 'CSRF_COOKIE_SECURE', False): - errors.append("生产环境必须启用 CSRF_COOKIE_SECURE") - - if not getattr(settings, 'SESSION_COOKIE_SECURE', False): - errors.append("生产环境必须启用 SESSION_COOKIE_SECURE") - - # 5. 检查 WinRM 安全配置 - if hasattr(settings, 'WINRM_CLIENT_CERT_VALIDATION') and \ - settings.WINRM_CLIENT_CERT_VALIDATION == 'validate': - if not settings.WINRM_CLIENT_CERT_PATH: - warnings.append("WinRM 证书验证已启用但未指定客户端证书路径") - - # 6. 检查日志配置 - if not os.path.exists(os.path.join(settings.BASE_DIR, 'logs')): - warnings.append("日志目录不存在,将自动创建") - - # 7. 检查数据库配置 - db_engine = settings.DATABASES['default']['ENGINE'] - if 'sqlite3' in db_engine and not settings.DEBUG: - warnings.append("生产环境建议使用 PostgreSQL 或 MySQL 而不是 SQLite") - - return errors, warnings - - -def print_production_status(): - """打印生产环境状态""" - errors, warnings = check_production_readiness() - - print("\n" + "="*60) - print("2c2a 生产环境安全性检查报告") - print("="*60) - print(f"生产模式: {'是' if not settings.DEBUG else '否'}") - print(f"DEBUG 模式: {'是' if settings.DEBUG else '否'}") - print(f"秘密密钥: {'已设置' if os.environ.get('DJANGO_SECRET_KEY') else '未设置'}") - - if errors: - print("\n❌ 必须修复的错误:") - for error in errors: - print(f" - {error}") - - if warnings: - print("\n⚠️ 建议修复的警告:") - for warning in warnings: - print(f" - {warning}") - - if not errors and not warnings: - print("\n✅ 所有安全检查通过,系统已准备好部署到生产环境") - - print("\n="*60) - - # 如果有严重错误,退出程序 - if errors: - sys.exit(1) - - -if __name__ == '__main__': - print_production_status() \ No newline at end of file diff --git a/utils/provider.py b/utils/provider.py deleted file mode 100644 index bcffd62..0000000 --- a/utils/provider.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -提供商共享工具模块 - -本模块是提供商数据隔离的 SINGLE SOURCE OF TRUTH。 -所有提供商相关的身份验证和数据查询逻辑 -应统一使用此模块,替代 -apps/hosts/admin.py、apps/operations/admin.py、 -apps/tickets/admin.py 中重复的 is_provider 函数。 - -使用方式: - from utils.provider import ( - is_provider, get_provider_hosts, get_provider_products, - ) -""" - -from django.db import models - - -PROVIDER_GROUP_NAME = "主机提供商" - - -def is_provider(user): - """ - 检查用户是否属于提供商组 - - 超级管理员不属于提供商组,即使其权限更高。 - 此逻辑与 Admin 后台的数据隔离保持一致。 - - Args: - user: 用户对象 - - Returns: - bool: 如果用户属于提供商组且不是超级管理员,返回 True - """ - if user.is_superuser: - return False - return user.groups.filter(name=PROVIDER_GROUP_NAME).exists() - - -def get_provider_hosts(user, site_group=None): - """ - 获取提供商管理的主机 - - 提供商可以看到: - - 自己创建的主机 (created_by=user) - - 分配给自己的主机 (providers=user) - - Args: - user: 提供商用户对象 - site_group: 站点分组对象,如果提供则进一步过滤该站点分组下的主机 - - Returns: - QuerySet: 该提供商可见的主机查询集 - """ - from apps.hosts.models import Host - - qs = Host.objects.filter( - models.Q(created_by=user) | models.Q(providers=user) - ).distinct() - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - -def get_provider_products(user, site_group=None): - """ - 获取提供商创建的产品 - - 提供商可以看到自己创建的产品 (created_by=user) - - Args: - user: 提供商用户对象 - site_group: 站点分组对象,如果提供则进一步过滤该站点分组下的产品 - - Returns: - QuerySet: 该提供商可见的产品查询集 - """ - from apps.operations.models import Product - - qs = Product.objects.filter(created_by=user) - if site_group is not None: - qs = qs.filter(site_group=site_group) - return qs - - -def get_provider_queryset( - user, model_class, filter_field="created_by", site_group=None -): - """ - 通用的提供商数据隔离查询 - - 根据模型和过滤字段,返回该提供商可见的数据查询集。 - 对于有 providers ManyToManyField 的模型,也会包含分配给该提供商的数据。 - - Args: - user: 提供商用户对象 - model_class: Django 模型类 - filter_field: 过滤字段名,默认为 'created_by' - site_group: 站点分组对象,如果提供则进一步过滤该站点分组下的数据 - - Returns: - QuerySet: 该提供商可见的数据查询集 - - Examples: - # 获取提供商创建的主机 - get_provider_queryset(user, Host, 'created_by') - - # 获取提供商创建的产品 - get_provider_queryset(user, Product, 'created_by') - - # 获取提供商创建的开户申请(通过产品关联) - get_provider_queryset(user, AccountOpeningRequest, - 'target_product__created_by') - """ - qs = model_class.objects.filter(**{filter_field: user}) - - if hasattr(model_class, "providers"): - qs = qs | model_class.objects.filter(providers=user) - qs = qs.distinct() - - if site_group is not None: - qs = qs.filter(site_group=site_group) - - return qs diff --git a/utils/rate_limit.py b/utils/rate_limit.py deleted file mode 100755 index 66d9d18..0000000 --- a/utils/rate_limit.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -限流装饰器模块 -提供 API 限流和登录保护 - -使用标准 Django cache API(get/set),兼容所有缓存后端: -- Redis(django-redis):高性能,支持分布式 -- LocMemCache:本地内存,无需额外依赖 -""" -from functools import wraps -from typing import Callable -from django.core.cache import cache -from django.conf import settings -from django.http import JsonResponse -from utils.helpers import get_client_ip -import logging - -logger = logging.getLogger('2c2a') - - -class RateLimitExceeded(Exception): - pass - - -def _cache_incr(key: str, period: int) -> int: - """ - 兼容所有缓存后端的原子计数器。 - - 使用 get/set 替代 incr/expire,确保 LocMemCache 等后端也能正常工作。 - Redis 后端下 django-redis 会自动处理并发安全。 - - Args: - key: 缓存键 - period: 过期时间(秒) - - Returns: - int: 递增后的计数值 - """ - current = cache.get(key, 0) - new_count = current + 1 - cache.set(key, new_count, timeout=period + 1) - return new_count - - -def _cache_ttl_fallback(key: str, default: int = 0) -> int: - """ - 获取缓存键的剩余 TTL,不兼容时返回默认值。 - - cache.ttl() 是 django-redis 专有方法, - LocMemCache 等后端不支持,此时返回默认值。 - """ - if hasattr(cache, 'ttl'): - try: - ttl = cache.ttl(key) - if ttl is not None and ttl > 0: - return int(ttl) - except Exception: - pass - return default - - -def rate_limit(key_prefix: str, limit: int, period: int = 60, per_user: bool = True): - """ - 限流装饰器(固定窗口计数器) - - Args: - key_prefix: 缓存键前缀 - limit: 限制次数 - period: 时间周期(秒) - per_user: 是否按用户限流 - """ - def decorator(func: Callable) -> Callable: - @wraps(func) - def wrapper(request, *args, **kwargs): - if per_user and hasattr(request, 'user') and request.user.is_authenticated: - key_parts = [key_prefix, request.user.username] - else: - key_parts = [key_prefix, get_client_ip(request)] - - rate_limit_key = f"rate_limit:{':'.join(key_parts)}" - - current_count = cache.get(rate_limit_key, 0) - - if current_count >= limit: - remaining_time = _cache_ttl_fallback(rate_limit_key, period) - logger.warning(f"Rate limit exceeded for {rate_limit_key} ({current_count}/{limit})") - - return JsonResponse({ - 'success': False, - 'error': { - 'type': 'RateLimitExceeded', - 'message': f'请求过于频繁,请在 {remaining_time} 秒后重试', - 'retry_after': remaining_time, - }, - }, status=429) - - _cache_incr(rate_limit_key, period) - - return func(request, *args, **kwargs) - return wrapper - return decorator - - -def login_rate_limit(): - """登录限流装饰器""" - return rate_limit(key_prefix='login', limit=settings.LOGIN_RATE_LIMIT, period=60) - - -def api_rate_limit(): - """API 通用限流装饰器""" - return rate_limit(key_prefix='api', limit=settings.API_RATE_LIMIT, period=60) - - -def register_rate_limit(view_func): - """注册限流装饰器""" - @wraps(view_func) - def wrapped_view(self, request, *args, **kwargs): - from django.contrib import messages - from django.shortcuts import redirect - - rate_limit_key = f"rate_limit:register:{get_client_ip(request)}" - limit = 5 - period = 3600 - - current_count = cache.get(rate_limit_key, 0) - - if current_count >= limit: - remaining = _cache_ttl_fallback(rate_limit_key, period) - minutes = max(1, remaining // 60) - logger.warning(f"Registration rate limit exceeded for IP {get_client_ip(request)}") - messages.error(request, f'注册过于频繁,请在 {minutes} 分钟后重试') - return redirect('accounts:register') - - _cache_incr(rate_limit_key, period) - - return view_func(self, request, *args, **kwargs) - return wrapped_view - - -def check_operation_rate_limit(operation_type: str, identifier: str, limit: int = 10, period: int = 60) -> bool: - """ - 检查操作是否达到限流 - - Args: - operation_type: 操作类型 - identifier: 操作标识符 - limit: 限制次数 - period: 时间周期(秒) - - Returns: - True 如果允许操作,False 如果达到限流 - """ - key = f"rate_limit:op:{operation_type}:{identifier}" - current_count = cache.get(key, 0) - - if current_count >= limit: - logger.warning(f"Operation rate limit exceeded: {key} ({current_count}/{limit})") - return False - - _cache_incr(key, period) - return True - - -class RateLimitMixin: - """在视图类中添加限流功能的 mixin""" - - rate_limit_key = None - rate_limit_count = None - rate_limit_period = 60 - - def dispatch(self, request, *args, **kwargs): - if self.rate_limit_key and self.rate_limit_count: - if hasattr(request, 'user') and request.user.is_authenticated: - identifier = request.user.username - else: - identifier = get_client_ip(request) - - key = f"rate_limit:view:{self.rate_limit_key}:{identifier}" - current_count = cache.get(key, 0) - - if current_count >= self.rate_limit_count: - return JsonResponse({ - 'success': False, - 'error': { - 'type': 'RateLimitExceeded', - 'message': '操作过于频繁,请稍后再试', - }, - }, status=429) - - _cache_incr(key, self.rate_limit_period) - - return super().dispatch(request, *args, **kwargs) - - -def rate_limit_ip(ip: str, key: str, limit: int, period: int = 60) -> bool: - """ - 基于 IP 的限流函数 - """ - cache_key = f"rate_limit:ip:{key}:{ip}" - current_count = cache.get(cache_key, 0) - - if current_count >= limit: - logger.warning(f"IP rate limit exceeded: {cache_key} ({current_count}/{limit})") - return False - - _cache_incr(cache_key, period) - return True diff --git a/utils/redis_helper.py b/utils/redis_helper.py deleted file mode 100644 index 15e053a..0000000 --- a/utils/redis_helper.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Redis 辅助工具模块 - -提供 Redis 可选支持:配置了 REDIS_URL 且 Redis 服务可达时自动启用, -否则静默降级到本地替代方案(LocMemCache / DB Session / SQLite Celery)。 - -延迟导入策略: -- REDIS_URL 未配置时,绝不 import redis 包 -- redis 包未安装时,静默降级,不报错 - -使用方式: - from utils.redis_helper import is_redis_available, get_redis_client - - if is_redis_available(): - client = get_redis_client() - client.set('key', 'value') -""" - -import logging -import os - -logger = logging.getLogger('2c2a') - -_redis_client = None -_redis_available = None - - -def _get_redis_url(): - return os.environ.get('REDIS_URL', '').strip() - - -def is_redis_available(): - """ - 检查 Redis 是否可用。 - - 首次调用时会尝试连接 Redis 并缓存结果, - 后续调用直接返回缓存值。 - - REDIS_URL 未配置时不 import redis,redis 包未安装也不报错。 - """ - global _redis_available - if _redis_available is not None: - return _redis_available - - url = _get_redis_url() - if not url: - logger.info('REDIS_URL not configured, using local alternatives') - _redis_available = False - return False - - try: - import redis - client = redis.Redis.from_url(url, socket_connect_timeout=3) - client.ping() - logger.info('Redis is available, will use Redis for cache/session/celery') - _redis_available = True - except Exception as e: - logger.warning( - 'REDIS_URL is configured but Redis is unreachable or redis package not installed, ' - 'falling back to local alternatives: %s', e, - ) - _redis_available = False - - return _redis_available - - -def get_redis_client(): - """ - 获取 Redis 客户端实例。 - - Returns: - redis.Redis | None: Redis 客户端,不可用时返回 None - """ - global _redis_client - if _redis_client is not None: - return _redis_client - - if not is_redis_available(): - return None - - try: - import redis - url = _get_redis_url() - _redis_client = redis.Redis.from_url(url, socket_connect_timeout=3) - return _redis_client - except Exception as e: - logger.warning('Failed to create Redis client: %s', e) - return None - - -def reset_redis_state(): - """ - 重置 Redis 状态缓存(主要用于测试) - """ - global _redis_client, _redis_available - _redis_client = None - _redis_available = None diff --git a/utils/sensitive_log_filters.py b/utils/sensitive_log_filters.py deleted file mode 100755 index bb39a19..0000000 --- a/utils/sensitive_log_filters.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -日志脱敏过滤器 -用于清理日志中的敏感信息 -""" -import re -import logging - - -class SensitiveDataFilter(logging.Filter): - """过滤日志中的敏感数据""" - - # 敏感字段正则表达式 - SENSITIVE_PATTERNS = [ - (r'(password)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(pwd)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(secret_key)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(token)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(api_key)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(access_token)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - (r'(session_key)\s*=\s*[\'"]?([^\s\'"]+)', r'\1=***'), - ] - - # IP 地址脱敏模式(保留前三段) - IP_PATTERN = r'(\b(?:[0-9]{1,3}\.){3})[0-9]{1,3}\b' - IP_REPLACEMENT = r'\1xxx' - - def filter(self, record): - """过滤日志记录中的敏感数据""" - if hasattr(record, 'msg'): - record.msg = self._sanitize_message(str(record.msg)) - - if hasattr(record, 'args') and record.args: - sanitized_args = [] - for arg in record.args: - if isinstance(arg, str): - sanitized_args.append(self._sanitize_message(arg)) - else: - sanitized_args.append(arg) - record.args = tuple(sanitized_args) - - return True - - def _sanitize_message(self, message: str) -> str: - """清理消息中的敏感信息""" - # 清理密码等敏感字段 - for pattern, replacement in self.SENSITIVE_PATTERNS: - message = re.sub(pattern, replacement, message, flags=re.IGNORECASE) - - # 清理 IP 地址 - message = re.sub(self.IP_PATTERN, self.IP_REPLACEMENT, message) - - return message - - -class AuditFilter(logging.Filter): - """审计日志过滤器,确保重要操作都被记录""" - - AUDIT_ACTIONS = [ - 'create_user', 'delete_user', 'reset_password', 'approve_request', - 'reject_request', 'modify_host', 'delete_host', 'login', 'logout', - 'view_password', 'process_opening_request' - ] - - def filter(self, record): - """确保审计相关的日志被记录""" - # 检查是否是审计操作 - if hasattr(record, 'action') and record.action in self.AUDIT_ACTIONS: - record.levelno = logging.INFO - record.levelname = logging.getLevelName(logging.INFO) - - return True \ No newline at end of file diff --git a/utils/site_group.py b/utils/site_group.py deleted file mode 100644 index c8bc261..0000000 --- a/utils/site_group.py +++ /dev/null @@ -1,154 +0,0 @@ -from django.core.cache import cache - - -# SiteGroupConfig 中可覆盖的字段名列表 -OVERRIDABLE_FIELDS = [ - "smtp_host", - "smtp_port", - "smtp_encryption", - "smtp_username", - "smtp_password", - "smtp_from_email", - "smtp_from_name", - "captcha_provider", - "captcha_type", - "login_captcha_type", - "register_captcha_type", - "email_captcha_type", - "enable_registration", - "email_suffix_whitelist", - "email_suffix_blacklist", - "site_name", - "site_icon", - "icp_number", - "police_number", -] - - -class EffectiveConfig: - """ - 合并配置:优先使用 SiteGroupConfig 的非空字段,回退到 SystemConfig。 - - 用法: - ec = get_effective_config(site_group) - ec.smtp_host # 优先站点组配置,回退全局配置 - ec.enable_registration - ec.get_captcha_config(scene='login') - ec.get_email_suffix_lists() - """ - - def __init__(self, system_config, site_group_config=None): - self._system = system_config - self._sg = site_group_config - - def __getattr__(self, name): - if name.startswith("_"): - raise AttributeError(name) - # 优先站点组配置的非空值 - if self._sg is not None: - sg_val = getattr(self._sg, name, None) - if sg_val is not None and name in OVERRIDABLE_FIELDS: - return sg_val - # 回退全局配置 - return getattr(self._system, name, None) - - def get_captcha_config(self, scene=None): - """根据场景获取验证码配置,与 SystemConfig.get_captcha_config 兼容""" - provider = self.captcha_provider - if scene == "login": - captcha_type = self.login_captcha_type or self.captcha_type - elif scene == "register": - captcha_type = self.register_captcha_type or self.captcha_type - elif scene == "email": - captcha_type = self.email_captcha_type or self.captcha_type - else: - captcha_type = self.captcha_type - return provider, captcha_type - - def get_email_suffix_lists(self): - """获取邮箱后缀白名单和黑名单列表(已解析为列表)""" - cache_key = f'effective_email_suffixes:{self._system.pk}:{getattr(self._sg, "pk", "none")}' - data = cache.get(cache_key) - if data is not None: - return data - - whitelist = [] - if self.email_suffix_whitelist: - whitelist = [ - s.strip() - for s in self.email_suffix_whitelist.strip().split("\n") - if s.strip() - ] - blacklist = [] - if self.email_suffix_blacklist: - blacklist = [ - s.strip() - for s in self.email_suffix_blacklist.strip().split("\n") - if s.strip() - ] - data = {"whitelist": whitelist, "blacklist": blacklist} - cache.set(cache_key, data, timeout=300) - return data - - def is_email_suffix_allowed(self, email): - """检查邮箱后缀是否符合当前配置""" - suffix = "@" + email.split("@")[1] if "@" in email else "" - data = self.get_email_suffix_lists() - if data["whitelist"]: - return suffix in data["whitelist"] - if data["blacklist"]: - return suffix not in data["blacklist"] - return True - - -def get_effective_config(site_group=None): - """ - 获取合并后的有效配置。 - - Args: - site_group: SiteGroup 实例或 None。None 时返回纯全局配置。 - - Returns: - EffectiveConfig 实例 - """ - from apps.dashboard.models import SystemConfig, SiteGroupConfig - - system_config = SystemConfig.get_config() - sg_config = SiteGroupConfig.get_config(site_group) if site_group else None - return EffectiveConfig(system_config, sg_config) - - -def get_site_group_queryset( - user, model_class, site_group=None, filter_field="created_by" -): - if user.is_superuser: - return model_class.objects.all() - - if _is_site_group_admin(user, site_group): - return _filter_by_site_group(model_class, site_group) - - provider_qs = _get_provider_queryset(user, model_class, filter_field) - site_group_qs = _filter_by_site_group(model_class, site_group) - return provider_qs & site_group_qs - - -def _is_site_group_admin(user, site_group): - if user.is_superuser: - return True - if site_group is None: - return False - return site_group.admins.filter(pk=user.pk).exists() - - -def _filter_by_site_group(model_class, site_group): - if site_group is not None: - return model_class.objects.filter(site_group=site_group) - return model_class.objects.filter(site_group__isnull=True) - - -def _get_provider_queryset(user, model_class, filter_field="created_by"): - qs = model_class.objects.filter(**{filter_field: user}) - if hasattr(model_class, "providers"): - qs = qs | model_class.objects.filter(providers=user) - qs = qs.distinct() - return qs diff --git a/utils/winrm_client.py b/utils/winrm_client.py deleted file mode 100755 index f5e2c20..0000000 --- a/utils/winrm_client.py +++ /dev/null @@ -1,712 +0,0 @@ -# Winrm客户端工具 -import logging -import re -import os -from dataclasses import dataclass -from typing import Optional, Dict, Any, List -from winrm import Session -from winrm.exceptions import InvalidCredentialsError -from django.conf import settings -import socket -import time -import secrets -import string -import functools - -logger = logging.getLogger("2c2a") - - -USERNAME_PATTERN = re.compile(r"^[a-zA-Z0-9_]{1,150}$") -GROUPNAME_PATTERN = re.compile(r"^[a-zA-Z0-9_\-\s]{1,256}$") -MAX_STRING_LENGTH = 4096 - - -class CommandInjectionError(Exception): - pass - - -def validate_username(username: str) -> str: - if not username: - raise CommandInjectionError("用户名不能为空") - if len(username) > 150: - raise CommandInjectionError("用户名长度不能超过150个字符") - if not USERNAME_PATTERN.match(username): - raise CommandInjectionError(f"用户名格式无效: 只允许字母、数字和下划线") - return username - - -def validate_groupname(group: str) -> str: - if not group: - raise CommandInjectionError("组名不能为空") - if len(group) > 256: - raise CommandInjectionError("组名长度不能超过256个字符") - if not GROUPNAME_PATTERN.match(group): - raise CommandInjectionError( - f"组名格式无效: 只允许字母、数字、下划线、连字符和空格" - ) - return group - - -def validate_string_length( - s: str, max_length: int = MAX_STRING_LENGTH, field_name: str = "输入" -) -> str: - if s and len(s) > max_length: - raise CommandInjectionError(f"{field_name}长度不能超过{max_length}个字符") - return s - - -def _escape_ps_string(s: str) -> str: - if not s: - return s - if len(s) > MAX_STRING_LENGTH: - raise CommandInjectionError(f"字符串长度超过最大限制 {MAX_STRING_LENGTH}") - return ( - s.replace("\x00", "") - .replace("`", "``") - .replace('"', '`"') - .replace("$", "`$") - .replace("\n", "`n") - .replace("\r", "`r") - ) - - -def _escape_for_here_string(s: str) -> str: - if not s: - return s - s = s.replace("\x00", "") - if '@"' in s or '"@' in s: - raise CommandInjectionError("内容包含非法的 here-string 分隔符") - return s - - -@dataclass -class WinrmResult: - status_code: int - std_out: str - std_err: str - - @property - def success(self) -> bool: - return self.status_code == 0 - - -class WinrmClient: - """WinRM客户端 - 远程管理Windows主机""" - - def __init__( - self, - hostname: str, - username: Optional[str] = None, - password: Optional[str] = None, - port: int = 5985, - use_ssl: bool = False, - auth_method: str = "ntlm", - cert_pem_path: Optional[str] = None, - cert_key_path: Optional[str] = None, - timeout: Optional[int] = None, - max_retries: Optional[int] = None, - server_cert_validation: str = "validate", - ca_trust_path: Optional[str] = None, - client_cert_pem: Optional[str] = None, - client_cert_key: Optional[str] = None, - ): - """ - 初始化WinRM客户端 - - 参数: - hostname: 主机名或IP地址 - username: 登录用户名(ntlm方式必填) - password: 登录密码(ntlm方式必填) - port: WinRM服务端口,默认为5985 - use_ssl: 是否使用SSL连接,默认为False - auth_method: 认证方式 ('ntlm', 'certificate') - cert_pem_path: 客户端证书PEM文件路径(certificate方式必填) - cert_key_path: 客户端私钥PEM文件路径(certificate方式必填) - timeout: 操作超时时间(秒),默认使用配置文件中的值 - max_retries: 最大重试次数,默认使用配置文件中的值 - server_cert_validation: 服务器证书验证模式 ('ignore', 'validate') - ca_trust_path: CA证书路径(用于验证服务器证书) - client_cert_pem: 客户端证书PEM文件路径(已弃用,使用cert_pem_path) - client_cert_key: 客户端证书私钥文件路径(已弃用,使用cert_key_path) - """ - if auth_method == "certificate": - if not cert_pem_path and client_cert_pem: - cert_pem_path = client_cert_pem - if not cert_key_path and client_cert_key: - cert_key_path = client_cert_key - if not cert_pem_path or not cert_key_path: - raise ValueError("证书认证方式必须提供证书和私钥路径") - if not os.path.exists(cert_pem_path): - raise ValueError(f"客户端证书文件不存在: {cert_pem_path}") - if not os.path.exists(cert_key_path): - raise ValueError(f"客户端私钥文件不存在: {cert_key_path}") - self.auth_method = "certificate" - self.cert_pem_path = cert_pem_path - self.cert_key_path = cert_key_path - self.username = username or "" - self.password = password or "" - elif auth_method == "ntlm": - if not username: - raise ValueError("NTLM认证方式必须提供用户名") - if not password: - raise ValueError("NTLM认证方式必须提供密码") - self.auth_method = "ntlm" - self.username = username - self.password = password - self.cert_pem_path = "" - self.cert_key_path = "" - else: - raise ValueError(f"不支持的认证方式: {auth_method}") - # 检查主机名是否包含端口(例如 "hostname:port" 或 "ip:port" 格式) - if ":" in hostname and not hostname.startswith("http"): - # 分离主机名和端口 - parts = hostname.split(":", 1) - if len(parts) == 2 and parts[1].isdigit(): - # 提取主机名和端口 - actual_hostname = parts[0] - actual_port = int(parts[1]) - # 更新实例变量 - self.hostname = actual_hostname - # 如果没有显式指定端口,则使用从主机名中提取的端口 - if port == 5985: # 5985是默认WinRM端口 - self.port = actual_port - else: - # 如果已显式指定端口,则使用指定的端口 - self.port = port - else: - self.hostname = hostname - self.port = port - else: - self.hostname = hostname - self.port = port - - self.use_ssl = use_ssl - self.timeout = timeout or settings.WINRM_TIMEOUT - self.max_retries = max_retries or settings.WINRM_MAX_RETRIES - - self.server_cert_validation = server_cert_validation - self.ca_trust_path = ca_trust_path - self.client_cert_pem = client_cert_pem - self.client_cert_key = client_cert_key - - if server_cert_validation == "ignore": - logger.warning( - f"WinRM连接到 {hostname} 未启用服务器证书验证," "存在中间人攻击风险" - ) - - if use_ssl and server_cert_validation == "validate": - if not ca_trust_path: - logger.warning("SSL验证启用但未提供CA证书路径,将使用系统默认证书") - elif not os.path.exists(ca_trust_path): - logger.error(f"CA证书文件不存在: {ca_trust_path}") - raise ValueError(f"CA证书文件不存在: {ca_trust_path}") - - if self.auth_method == "certificate": - transport = "certificate" - if not self.use_ssl: - self.use_ssl = True - if self.port == 5985: - self.port = 5986 - else: - transport = "ntlm" - - protocol = "https" if self.use_ssl else "http" - self.endpoint = f"{protocol}://{self.hostname}:{self.port}/wsman" - - if not self._validate_hostname(): - raise ValueError(f"主机名无法解析: {self.hostname}") - - session_kwargs = dict( - transport=transport, - server_cert_validation=self.server_cert_validation, - ca_trust_path=self.ca_trust_path or None, - operation_timeout_sec=self.timeout, - read_timeout_sec=self.timeout + 10, - ) - if self.auth_method == "certificate": - session_kwargs["cert_pem"] = self.cert_pem_path - session_kwargs["cert_key_pem"] = self.cert_key_path - self.session = Session( - self.endpoint, - auth=(self.username, self.password), - **session_kwargs, - ) - else: - self.session = Session( - self.endpoint, - auth=(self.username, self.password), - **session_kwargs, - ) - - logger.info( - f"初始化WinRM客户端: 主机={self.hostname}, 端口={self.port}, " - f"SSL={self.use_ssl}, 认证={self.auth_method}, " - f"超时={self.timeout}秒, 最大重试={self.max_retries}次" - ) - - def _validate_hostname(self) -> bool: - """ - 验证主机名是否可以解析 - - Returns: - bool: 如果主机名可以解析则返回True,否则返回False - """ - try: - # 尝试解析主机名 - socket.gethostbyname(self.hostname) - return True - except socket.gaierror: - logger.error(f"无法解析主机名: {self.hostname}:{self.port}") - return False - except Exception as e: - logger.error(f"验证主机名时发生未知错误: {str(e)}") - return False - - def execute_command( - self, command: str, arguments: Optional[list] = None - ) -> WinrmResult: - """ - 执行远程命令 - - 参数: - command: 要执行的命令 - arguments: 命令参数列表 - - 返回: - WinrmResult对象,包含执行结果 - - 异常: - Exception: 当所有重试尝试都失败时抛出 - """ - import os - - # 如果是DEMO模式,模拟执行命令而不实际执行 - if os.environ.get("2C2A_DEMO", "").lower() == "1": - logger.info(f"DEMO模式: 模拟执行远程命令: {command}, 参数: {arguments}") - # 模拟成功执行的结果 - return WinrmResult( - status_code=0, - std_out="Command executed successfully in demo mode", - std_err="", - ) - - logger.info(f"执行远程命令: {command}, 参数: {arguments}") - - for attempt in range(self.max_retries): - try: - result = self.session.run_cmd(command, arguments or []) - winrm_result = WinrmResult( - status_code=result.status_code, - std_out=result.std_out.decode("utf-8", errors="ignore"), - std_err=result.std_err.decode("utf-8", errors="ignore"), - ) - - if winrm_result.success: - logger.info(f"命令执行成功: {command}") - else: - logger.warning( - f"命令执行返回非零状态码: {command}, " - f"状态码={result.status_code}, 错误={winrm_result.std_err}" - ) - - return winrm_result - except Exception as e: - # 检查是否是网络连接错误 - error_str = str(e) - if ( - "NameResolutionError" in error_str - or "Failed to resolve" in error_str - ): - logger.error(f"主机名解析失败: {self.hostname}") - raise Exception( - f'主机名解析失败: 无法解析主机名 "{self.hostname}". 请检查主机名拼写或网络连接.' - ) - - logger.error( - f"命令执行失败 (尝试 {attempt + 1}/{self.max_retries}): " - f"{command}, 错误: {str(e)}" - ) - - if attempt == self.max_retries - 1: - logger.error(f"命令执行最终失败: {command}") - raise Exception(f"命令执行失败: {str(e)}") - - # 在重试之间等待一段时间 - time.sleep(1) - - def execute_powershell( - self, script: str, arguments: Optional[Dict[str, Any]] = None - ) -> WinrmResult: - """ - 执行PowerShell脚本 - - 参数: - script: 要执行的PowerShell脚本 - arguments: 脚本参数字典 - - 返回: - WinrmResult对象,包含执行结果 - - 异常: - Exception: 当所有重试尝试都失败时抛出 - """ - import os - - # 如果是DEMO模式,模拟执行PowerShell而不实际执行 - if os.environ.get("2C2A_DEMO", "").lower() == "1": - logger.info("DEMO模式: 模拟执行PowerShell脚本") - # 模拟成功执行的结果 - return WinrmResult( - status_code=0, - std_out="PowerShell script executed successfully in demo mode", - std_err="", - ) - - logger.info(f"执行远程命令: {script}, 参数: {arguments}") - - for attempt in range(self.max_retries): - try: - result = self.session.run_ps(script) - winrm_result = WinrmResult( - status_code=result.status_code, - std_out=result.std_out.decode("utf-8", errors="ignore"), - std_err=result.std_err.decode("utf-8", errors="ignore"), - ) - - if winrm_result.success: - logger.info(f"PowerShell脚本执行成功") - else: - logger.warning( - f"PowerShell脚本执行返回非零状态码: " - f"状态码={result.status_code}, 错误={winrm_result.std_err}" - ) - - return winrm_result - except Exception as e: - # 检查是否是网络连接错误 - error_str = str(e) - if ( - "NameResolutionError" in error_str - or "Failed to resolve" in error_str - ): - logger.error(f"主机名解析失败: {self.hostname}") - raise Exception( - f'主机名解析失败: 无法解析主机名 "{self.hostname}". 请检查主机名拼写或网络连接.' - ) - - logger.error( - f"PowerShell脚本执行失败 (尝试 {attempt + 1}/{self.max_retries}), " - f"错误: {str(e)}" - ) - - if attempt == self.max_retries - 1: - logger.error("PowerShell脚本执行最终失败") - raise Exception(f"PowerShell执行失败: {str(e)}") - - # 在重试之间等待一段时间 - time.sleep(1) - - def create_user( - self, - username: str, - password: str, - description: Optional[str] = None, - group: Optional[str] = None, - ) -> WinrmResult: - try: - validate_username(username) - validate_string_length(password, 256, "密码") - if description: - validate_string_length(description, 512, "描述") - if group: - validate_groupname(group) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - - safe_user = _escape_ps_string(username) - safe_pass = _escape_ps_string(password) - safe_desc = _escape_ps_string(description or "") - - script = f""" -$pw = ConvertTo-SecureString "{safe_pass}" -AsPlainText -Force -New-LocalUser -Name "{safe_user}" -Password $pw -Description "{safe_desc}" -ErrorAction Stop -net user "{safe_user}" /logonpasswordchg:NO -Add-LocalGroupMember -Group "Users" -Member "{safe_user}" -ErrorAction Stop -""" - if group: - safe_group = _escape_ps_string(group) - script += f'Add-LocalGroupMember -Group "{safe_group}" -Member "{safe_user}" -ErrorAction Stop\n' - - logger.info(f"创建用户: {username}") - result = self.execute_powershell(script) - self.add_to_remote_users(username) - return result - - def create_user_with_reset_password_on_next_login( - self, - username: str, - password: str, - description: Optional[str] = None, - group: Optional[str] = None, - ) -> WinrmResult: - try: - validate_username(username) - validate_string_length(password, 256, "密码") - if description: - validate_string_length(description, 512, "描述") - if group: - validate_groupname(group) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - - safe_user = _escape_ps_string(username) - safe_pass = _escape_ps_string(password) - safe_desc = _escape_ps_string(description or "") - - script = f""" -$pw = ConvertTo-SecureString "{safe_pass}" -AsPlainText -Force -New-LocalUser -Name "{safe_user}" -Password $pw -Description "{safe_desc}" -ErrorAction Stop -net user "{safe_user}" /logonpasswordchg:NO -Add-LocalGroupMember -Group "Users" -Member "{safe_user}" -ErrorAction Stop -""" - if group: - safe_group = _escape_ps_string(group) - script += f'Add-LocalGroupMember -Group "{safe_group}" -Member "{safe_user}" -ErrorAction Stop\n' - - logger.info(f"创建用户(首登改密): {username}") - result = self.execute_powershell(script) - self.add_to_remote_users(username) - return result - - def delete_user(self, username: str) -> WinrmResult: - try: - validate_username(username) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - script = f'Remove-LocalUser -Name "{safe_user}" -ErrorAction Stop' - logger.info(f"删除用户: {username}") - return self.execute_powershell(script) - - def enable_user(self, username: str) -> WinrmResult: - try: - validate_username(username) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - script = f'Enable-LocalUser -Name "{safe_user}" -ErrorAction Stop' - logger.info(f"启用用户: {username}") - return self.execute_powershell(script) - - def disabled_user(self, username: str) -> WinrmResult: - try: - validate_username(username) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - script = f'Disable-LocalUser -Name "{safe_user}" -ErrorAction Stop' - logger.info(f"禁用用户: {username}") - return self.execute_powershell(script) - - def get_user_info(self, username: str) -> WinrmResult: - try: - validate_username(username) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - script = f'Get-LocalUser -Name "{safe_user}" | ConvertTo-Json' - return self.execute_powershell(script) - - def list_users(self) -> WinrmResult: - return self.execute_powershell("Get-LocalUser | ConvertTo-Json") - - def check_user_exists(self, username: str) -> bool: - try: - validate_username(username) - except CommandInjectionError: - return False - safe_user = _escape_ps_string(username) - try: - script = f'$u = Get-LocalUser -Name "{safe_user}" -ErrorAction Stop; $true' - result = self.execute_powershell(script) - return result.success and "True" in result.std_out - except: - return False - - def get_password_policy(self) -> Dict[str, Any]: - """ - 动态获取密码策略要求 - - 返回: - Dict: 包含密码策略信息的字典 - """ - try: - script = f""" - secedit /export /cfg "$env:TEMP\\secpol.cfg" | Out-Null - Get-Content "$env:TEMP\\secpol.cfg" | Where-Object {{ $_ -match '^(MinimumPasswordLength|PasswordComplexity|PasswordHistorySize|MaximumPasswordAge|MinimumPasswordAge)\\s*=' }} - Remove-Item "$env:TEMP\\secpol.cfg" -ErrorAction SilentlyContinue - """ - result = self.execute_powershell(script) - - policy = {} - if result.success: - lines = result.std_out.strip().split("\n") - for line in lines: - line = line.strip() - if line.startswith("MinimumPasswordLength"): - try: - policy["minimum_length"] = int(line.split("=")[1].strip()) - except: - policy["minimum_length"] = 8 # 默认值 - elif line.startswith("PasswordComplexity"): - try: - policy["complexity_required"] = bool( - int(line.split("=")[1].strip()) - ) - except: - policy["complexity_required"] = True # 默认值 - elif line.startswith("PasswordHistorySize"): - try: - policy["history_size"] = int(line.split("=")[1].strip()) - except: - policy["history_size"] = 0 # 默认值 - elif line.startswith("MaximumPasswordAge"): - try: - policy["max_age_days"] = int(line.split("=")[1].strip()) - except: - policy["max_age_days"] = 0 # 默认值 - elif line.startswith("MinimumPasswordAge"): - try: - policy["min_age_days"] = int(line.split("=")[1].strip()) - except: - policy["min_age_days"] = 0 # 默认值 - - # 设置默认值 - if "minimum_length" not in policy: - policy["minimum_length"] = 8 - if "complexity_required" not in policy: - policy["complexity_required"] = True - - logger.info(f"获取密码策略成功: {policy}") - return policy - except Exception as e: - logger.error(f"获取密码策略失败: 错误: {str(e)}") - # 返回默认密码策略 - return { - "minimum_length": 8, - "complexity_required": True, - "history_size": 0, - "max_age_days": 42, - "min_age_days": 1, - } - - def generate_strong_password(self, length: Optional[int] = None) -> str: - """ - 根据密码策略生成强密码 - - 参数: - length: 密码长度,默认根据服务器策略确定 - - 返回: - str: 生成的强密码 - """ - import secrets - import string - - # 获取服务器密码策略 - policy = self.get_password_policy() - - # 确定密码长度 - actual_length = length or max(policy["minimum_length"], 12) # 默认至少12位 - - if policy["complexity_required"]: - # 密码复杂性要求:至少包含大写字母、小写字母、数字和特殊字符 - uppercase = secrets.choice(string.ascii_uppercase) - lowercase = secrets.choice(string.ascii_lowercase) - digit = secrets.choice(string.digits) - special_char = secrets.choice("!@#$%^&*()_+-=[]{}|;:,.<>?") - - # 剩余部分随机生成 - remaining_length = max(0, actual_length - 4) - alphabet = ( - string.ascii_letters + string.digits + "!@#$%^&*()_+-=[]{}|;:,.<>?" - ) - rest = "".join(secrets.choice(alphabet) for i in range(remaining_length)) - - # 打乱顺序以确保安全 - password_chars = list(uppercase + lowercase + digit + special_char + rest) - secrets.SystemRandom().shuffle(password_chars) - password = "".join(password_chars) - else: - # 不需要复杂性要求,简单生成随机密码 - alphabet = string.ascii_letters + string.digits - password = "".join(secrets.choice(alphabet) for i in range(actual_length)) - - logger.info(f"生成强密码完成,长度: {len(password)}") - return password - - def op_user(self, username: str) -> bool: - try: - validate_username(username) - except CommandInjectionError: - return False - safe_user = _escape_ps_string(username) - try: - result = self.execute_powershell( - f'net localgroup Administrators "{safe_user}" /add' - ) - return result.success - except: - return False - - def deop_user(self, username: str) -> bool: - try: - validate_username(username) - except CommandInjectionError: - return False - safe_user = _escape_ps_string(username) - try: - result = self.execute_powershell( - f'net localgroup Administrators "{safe_user}" /delete' - ) - return result.success - except: - return False - - def reset_password(self, username: str, password: str) -> WinrmResult: - try: - validate_username(username) - validate_string_length(password, 256, "密码") - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - safe_pass = _escape_ps_string(password) - script = f""" -$pw = ConvertTo-SecureString "{safe_pass}" -AsPlainText -Force -Set-LocalUser -Name "{safe_user}" -Password $pw -net user "{safe_user}" /logonpasswordchg:NO -""" - result = self.execute_powershell(script) - if result.success: - self.add_to_remote_users(username) - return result - - def add_to_remote_users(self, username: str) -> WinrmResult: - try: - validate_username(username) - except CommandInjectionError as e: - logger.warning(f"输入验证失败: {str(e)}") - return WinrmResult(1, "", str(e)) - safe_user = _escape_ps_string(username) - script = ( - f'Add-LocalGroupMember -Group "Remote Desktop Users" ' - f'-Member "{safe_user}" -ErrorAction SilentlyContinue' - ) - return self.execute_powershell(script) diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 0be394f..0000000 --- a/uv.lock +++ /dev/null @@ -1,1580 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" - -[[package]] -name = "2c2a" -version = "1.0.0" -source = { editable = "." } -dependencies = [ - { name = "celery" }, - { name = "cotton-icons" }, - { name = "cryptography" }, - { name = "django" }, - { name = "django-cors-headers" }, - { name = "django-cotton" }, - { name = "django-formtools" }, - { name = "django-tianai-captcha" }, - { name = "djangorestframework" }, - { name = "djlint" }, - { name = "heroicons" }, - { name = "idna" }, - { name = "kombu" }, - { name = "markdown" }, - { name = "pillow" }, - { name = "pyjwt" }, - { name = "pyotp" }, - { name = "python-dotenv" }, - { name = "pywinrm" }, - { name = "redis" }, - { name = "requests" }, - { name = "toml" }, - { name = "whitenoise" }, -] - -[package.optional-dependencies] -kerberos = [ - { name = "gssapi" }, - { name = "krb5" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black" }, - { name = "django-stubs" }, - { name = "flake8" }, - { name = "pyrefly" }, - { name = "pytest" }, - { name = "pytest-django" }, - { name = "redis" }, -] - -[package.metadata] -requires-dist = [ - { name = "celery", specifier = "==5.4.0" }, - { name = "cotton-icons", specifier = ">=0.2.0" }, - { name = "cryptography", specifier = "==46.0.3" }, - { name = "django", specifier = "==4.2.27" }, - { name = "django-cors-headers", specifier = "==4.3.1" }, - { name = "django-cotton", git = "https://github.com/2c2a/django-cotton.git?rev=feature%2Fx-prefix-tag-support" }, - { name = "django-formtools", specifier = ">=2.5.1" }, - { name = "django-tianai-captcha", git = "https://github.com/trustedinster/django-tianai-captcha.git" }, - { name = "djangorestframework", specifier = "==3.15.2" }, - { name = "djlint", specifier = ">=1.36.4" }, - { name = "gssapi", marker = "extra == 'kerberos'", specifier = ">=1.11.1" }, - { name = "heroicons", specifier = ">=2.14.0" }, - { name = "idna", specifier = "==3.15" }, - { name = "kombu", specifier = "==5.6.2" }, - { name = "krb5", marker = "extra == 'kerberos'", specifier = ">=0.9.0" }, - { name = "markdown", specifier = "==3.10.1" }, - { name = "pillow", specifier = "==12.1.0" }, - { name = "pyjwt", specifier = ">=2.8.0" }, - { name = "pyotp" }, - { name = "python-dotenv", specifier = "==1.2.1" }, - { name = "pywinrm", specifier = "==0.4.3" }, - { name = "redis", specifier = ">=5.0.0" }, - { name = "requests", specifier = "==2.32.3" }, - { name = "toml" }, - { name = "whitenoise", specifier = ">=6.12.0" }, -] -provides-extras = ["kerberos"] - -[package.metadata.requires-dev] -dev = [ - { name = "black" }, - { name = "django-stubs", specifier = ">=6.0.2" }, - { name = "flake8" }, - { name = "pyrefly", specifier = ">=0.60.0" }, - { name = "pytest" }, - { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-django" }, - { name = "redis", specifier = ">=7.4.0" }, -] - -[[package]] -name = "amqp" -version = "5.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "vine" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2" }, -] - -[[package]] -name = "asgiref" -version = "3.8.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47" }, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c" }, -] - -[[package]] -name = "billiard" -version = "4.2.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5" }, -] - -[[package]] -name = "black" -version = "26.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff" }, - { url = "https://mirrors.aliyun.com/pypi/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839" }, - { url = "https://mirrors.aliyun.com/pypi/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568" }, - { url = "https://mirrors.aliyun.com/pypi/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b" }, -] - -[[package]] -name = "celery" -version = "5.4.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "billiard" }, - { name = "click" }, - { name = "click-didyoumean" }, - { name = "click-plugins" }, - { name = "click-repl" }, - { name = "kombu" }, - { name = "python-dateutil" }, - { name = "tzdata" }, - { name = "vine" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453" }, - { url = "https://mirrors.aliyun.com/pypi/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495" }, - { url = "https://mirrors.aliyun.com/pypi/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739" }, - { url = "https://mirrors.aliyun.com/pypi/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26" }, - { url = "https://mirrors.aliyun.com/pypi/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba" }, - { url = "https://mirrors.aliyun.com/pypi/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27" }, - { url = "https://mirrors.aliyun.com/pypi/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91" }, - { url = "https://mirrors.aliyun.com/pypi/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef" }, - { url = "https://mirrors.aliyun.com/pypi/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592" }, - { url = "https://mirrors.aliyun.com/pypi/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313" }, - { url = "https://mirrors.aliyun.com/pypi/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93" }, - { url = "https://mirrors.aliyun.com/pypi/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381" }, - { url = "https://mirrors.aliyun.com/pypi/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815" }, - { url = "https://mirrors.aliyun.com/pypi/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86" }, - { url = "https://mirrors.aliyun.com/pypi/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed" }, - { url = "https://mirrors.aliyun.com/pypi/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72" }, - { url = "https://mirrors.aliyun.com/pypi/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894" }, - { url = "https://mirrors.aliyun.com/pypi/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14" }, - { url = "https://mirrors.aliyun.com/pypi/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14" }, - { url = "https://mirrors.aliyun.com/pypi/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191" }, - { url = "https://mirrors.aliyun.com/pypi/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838" }, - { url = "https://mirrors.aliyun.com/pypi/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090" }, - { url = "https://mirrors.aliyun.com/pypi/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828" }, - { url = "https://mirrors.aliyun.com/pypi/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" }, -] - -[[package]] -name = "click-didyoumean" -version = "0.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c" }, -] - -[[package]] -name = "click-plugins" -version = "1.1.1.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6" }, -] - -[[package]] -name = "click-repl" -version = "0.3.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "click" }, - { name = "prompt-toolkit" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, -] - -[[package]] -name = "cotton-icons" -version = "0.2.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "django-cotton" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/80/46/2b75ee203aac31785d9da42f89fdf4e06c7bd6fe5a6bfa76c66d0a6071eb/cotton_icons-0.2.0.tar.gz", hash = "sha256:59f5f9945a2e92ad2d9d7578357bba7dee07b1a8a8ed34a8a56e85bb10539f8b" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b9/26/52ef398c64754f2b930019b5e9d8f5d8170c2f21038d5b7507bcd5f1a669/cotton_icons-0.2.0-py3-none-any.whl", hash = "sha256:8a06a1b2acba534e08b018b02950c62221a7406cffcafc293ea4ccfa7efa45a0" }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849" }, - { url = "https://mirrors.aliyun.com/pypi/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91" }, - { url = "https://mirrors.aliyun.com/pypi/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926" }, - { url = "https://mirrors.aliyun.com/pypi/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715" }, - { url = "https://mirrors.aliyun.com/pypi/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54" }, - { url = "https://mirrors.aliyun.com/pypi/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459" }, - { url = "https://mirrors.aliyun.com/pypi/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044" }, - { url = "https://mirrors.aliyun.com/pypi/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665" }, - { url = "https://mirrors.aliyun.com/pypi/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936" }, - { url = "https://mirrors.aliyun.com/pypi/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683" }, - { url = "https://mirrors.aliyun.com/pypi/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971" }, - { url = "https://mirrors.aliyun.com/pypi/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df" }, - { url = "https://mirrors.aliyun.com/pypi/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c" }, -] - -[[package]] -name = "cssbeautifier" -version = "1.15.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "editorconfig" }, - { name = "jsbeautifier" }, - { name = "six" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" }, -] - -[[package]] -name = "django" -version = "4.2.27" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "sqlparse" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ce/ff/6aa5a94b85837af893ca82227301ac6ddf4798afda86151fb2066d26ca0a/django-4.2.27.tar.gz", hash = "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/dd/f5/1a2319cc090870bfe8c62ef5ad881a6b73b5f4ce7330c5cf2cb4f9536b12/django-4.2.27-py3-none-any.whl", hash = "sha256:f393a394053713e7d213984555c5b7d3caeee78b2ccb729888a0774dff6c11a8" }, -] - -[[package]] -name = "django-cors-headers" -version = "4.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "django" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8a/04/a280a98256602d3f4fffae37a9410711fb80f9d6cf199679f6e93bbdb8b3/django-cors-headers-4.3.1.tar.gz", hash = "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/fe/6a/3428ab5d1ec270e845f4ef064a7cefbf1339b4454788d77c00d36caa828c/django_cors_headers-4.3.1-py3-none-any.whl", hash = "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36" }, -] - -[[package]] -name = "django-cotton" -version = "2.6.2" -source = { git = "https://github.com/2c2a/django-cotton.git?rev=feature%2Fx-prefix-tag-support#46b6a93f9b7a7e77212ae43bd2957c35b10b04dc" } -dependencies = [ - { name = "django" }, -] - -[[package]] -name = "django-formtools" -version = "2.5.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "django" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/73/f8/bb9b228fc33230186f3612a6fc96274a81bab3509817498f2632d7aa6367/django-formtools-2.5.1.tar.gz", hash = "sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/12/63/91a107e3aaaf3987bad036494dfd8cc2675f4a66d22e65ffd6711f84ba70/django_formtools-2.5.1-py3-none-any.whl", hash = "sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a" }, -] - -[[package]] -name = "django-stubs" -version = "6.0.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "django" }, - { name = "django-stubs-ext" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "types-pyyaml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/03/b2/f0214d86180f937c8e3358ff831b20f0634d95bd77436b18861c647e15bc/django_stubs-6.0.2.tar.gz", hash = "sha256:56d43b5e3741563af0063e5b6283f908c625b0439aa06314268673699d1bdccd" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/49/e7/8f2aaa22eac7fa18db3aca0e7b651ccf5ac79a2021bf67e75a16934a7076/django_stubs-6.0.2-py3-none-any.whl", hash = "sha256:c3bc84d80421758f3b2ad9e1358e001d719388a8eb106e67c873e606216108d4" }, -] - -[[package]] -name = "django-stubs-ext" -version = "6.0.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "django" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/52/e0/f2e6caf627d176a51fba1ca9c34082c7ea10d3f521ff2c828532ca99fa91/django_stubs_ext-6.0.2.tar.gz", hash = "sha256:70b7b7ae837e7a6036e2facb64435550bf7cf8143c1a6e802864d4824ce6058c" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/2b/d2/9cb93cd1ef94ddc97c26c902ff75a859f5f154051fec98cf8242649b26ce/django_stubs_ext-6.0.2-py3-none-any.whl", hash = "sha256:b35bdec1995bf49765cc39fa89aa7c23f120a23d0cb0152ab7fb4e48ff7d667b" }, -] - -[[package]] -name = "django-tianai-captcha" -version = "1.0.0" -source = { git = "https://github.com/trustedinster/django-tianai-captcha.git#c6ccc879afa6f95a3e63458953a33e68a7c4ec03" } -dependencies = [ - { name = "django" }, - { name = "pillow" }, -] - -[[package]] -name = "djangorestframework" -version = "3.15.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "django" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20" }, -] - -[[package]] -name = "djlint" -version = "1.36.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama" }, - { name = "cssbeautifier" }, - { name = "jsbeautifier" }, - { name = "json5" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tqdm" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/53/71/6a3ce2b49a62e635b85dce30ccf3eb3a18fe79275d45535325a55a63d3a3/djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/72/47/308412dc579e277c910774f41b380308d582862b16763425583e69e0fc14/djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9b/6f/428dc044d1e34363265b1301dc9b53253007acd858879d54b369d233aa96/djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/13/0d488e551d73ddf369552fc6f4c7702ea683e4bc1305bcf5c1d198fbdace/djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/68/18ecd1e4d54a523e1d077f01419d669116e5dede97f97f1eb8ddb918a872/djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/03/005cf5c66e57ca2d26249f8385bc64420b2a95fea81c5eb619c925199029/djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9f/88/aea3c81343a273a87362f30442abc13351dc8ada0b10e51daa285b4dddac/djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483" }, - { url = "https://mirrors.aliyun.com/pypi/packages/60/77/0f767ac0b72e9a664bb8c92b8940f21bc1b1e806e5bd727584d40a4ca551/djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08" }, - { url = "https://mirrors.aliyun.com/pypi/packages/53/f5/9ae02b875604755d4d00cebf96b218b0faa3198edc630f56a139581aed87/djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/97/51/284443ff2f2a278f61d4ae6ae55eaf820ad9f0fd386d781cdfe91f4de495/djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6d/5e/791f4c5571f3f168ad26fa3757af8f7a05c623fde1134a9c4de814ee33b7/djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1f/11/894425add6f84deffcc6e373f2ce250f2f7b01aa58c7f230016ebe7a0085/djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd" }, -] - -[[package]] -name = "editorconfig" -version = "0.17.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, -] - -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e" }, -] - -[[package]] -name = "gssapi" -version = "1.11.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "decorator" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/23/52/c1e90623c259a42ab0587078bb04f959867b970add46ff66750ead8fc7c5/gssapi-1.11.1.tar.gz", hash = "sha256:2049ee4b1d0c363163a1344b7282a363f9f4094e51d2c36de0cf01d4735e0ae2" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d8/d8/dfd7632e42f3028b27fdae1dea0fd967bcee1d9164e0a9fa3946490e6c7a/gssapi-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:126352502e15dc42f786a4635e5fb4dc8ae4bbc89354e85ab094c478a9e49beb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0b/25/e668f8bfebdaf132b29a26bbc4cc50c5a624a83c5271e83e69c62c2ac5d3/gssapi-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25b3bcf75a0dd5638f02f939a9c40d1c907682ccca69ea1d05ea81ad58ea1022" }, - { url = "https://mirrors.aliyun.com/pypi/packages/48/57/391fb8511e0e9bd2f86ed342ba65d1d1dbc0bd77d54f1f75ff4e4616df49/gssapi-1.11.1-cp310-cp310-win32.whl", hash = "sha256:e5d01ac02df8fe67c32cd1684c0954e935d50158ebb956fdffbf9aad7695a3b3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d3/bf/37a3359ba24d9e422094b749e031116e6613fd038ec0d4c633d210ba314e/gssapi-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:0e8b4d76801f2a8f8e6d85746cb9048d47341c6706800b357c61ba09e4741c03" }, - { url = "https://mirrors.aliyun.com/pypi/packages/48/75/3cc18f2d084d19fbba38dc684588cf5f674c647e754f9cf1625bd78c39f8/gssapi-1.11.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2298e5909a8f2d27c29352885a24e4026cfd3fa24fc38d4a0a3743fa5a3e7667" }, - { url = "https://mirrors.aliyun.com/pypi/packages/81/f9/ac0f8c43c209d56c89655f80cd4ae43379f88370d01a7e11f264f081eef5/gssapi-1.11.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:5b60b1f8d8d3e36c025bd3494105de1dfccee578e8de001f423cc094468e3022" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f8/97/f4ea9248bfdf5fcde2c5bf0bc0e573d212748724a32a5aa1002e11edb760/gssapi-1.11.1-cp311-abi3-win32.whl", hash = "sha256:9738fe0ba163c28ccf521de7520640bde4b135c1b6e87a5ac5a90435369e89c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/54/ca/7f839880baf7c365768884161c246a3b6201738722fc7581a995190ec431/gssapi-1.11.1-cp311-abi3-win_amd64.whl", hash = "sha256:96a102ad1ec266e2d843468bf03149982969fc70344f303f81ea20197b80d7a1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/79/9148636b75ca5741ddc9c57c4b256adec422e3a89bca14306d53b48caac9/gssapi-1.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:82fba401e9514ad21749b8d8954e2de1c617b0a73204c8598ee84630e23c5810" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ca/77/f34fd81bbccf2e682073964a1b1b0a383e70d02946e472f78881d50cec6f/gssapi-1.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb0250f27d288d4217d7f606d3b68ecb9a10fce9391106129cada96434a685b0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b2/34/733a6f3372040992befd1fe62288cafea9fe25acdcf8b663ec8a7857cb69/gssapi-1.11.1-cp314-cp314t-win32.whl", hash = "sha256:b17875d236b8b030a777ee3f3ece55f3d316a91937c37abbc771afe1493703cf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/03/1b71feddb85f945101c3cdc07242805c5e9b48da546f8a922129ad8299e5/gssapi-1.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:da43c0e0ae84bb9f04c4e016eac6d3826c6357f827183042ba990ccedeeab052" }, -] - -[[package]] -name = "heroicons" -version = "2.14.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5f/fe/ed8a483bbd518f421b891a3db4eb61c6f2a5f84886a92a80b82d0d6637b5/heroicons-2.14.0.tar.gz", hash = "sha256:e55ecc0a839cf872f55977d2dd04a1855bfdf080bed1d13db5264b0060e65a96" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/08/89/5e1511183a70edb8b10fed0507b76726fe060056c952a53a2484be96db24/heroicons-2.14.0-py3-none-any.whl", hash = "sha256:cdf7fa6de02b7bc5b4513d7f592a9a1faea150b050a3b4270a0dc445ebe930c9" }, -] - -[[package]] -name = "idna" -version = "3.15" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" }, -] - -[[package]] -name = "jsbeautifier" -version = "1.15.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "editorconfig" }, - { name = "six" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528" }, -] - -[[package]] -name = "json5" -version = "0.14.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9c/4b/6f8906aaf67d501e259b0adab4d312945bb7211e8b8d4dcc77c92320edaa/json5-0.14.0.tar.gz", hash = "sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a" }, -] - -[[package]] -name = "kombu" -version = "5.6.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "amqp" }, - { name = "packaging" }, - { name = "tzdata" }, - { name = "vine" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93" }, -] - -[[package]] -name = "krb5" -version = "0.9.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/15/15/55a01be5f1816fe6d7d36fec4c6b2cb6f5264d289a015322562c582a81b7/krb5-0.9.0.tar.gz", hash = "sha256:4cdd2c85ff4770108edaf48fedf19888cf956ff374e2e97e40f8412b048caee6" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/7d/ed/473605378e398642d8f8262544774b6673178325e0166fe56ec7a7884d0a/krb5-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1bde451ddb3a1c064be1da967fa32b1976a06ca43969e9d80ed8b26506ac4e3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b4/96/00a11ef3118690cf3b3c6554127e26c16006525bf34b0e22d6b27dca3dd9/krb5-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6e612e763304bfe4f6845f581030502327da2d0a19efccc840d7c051448ee209" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1b/4e/0a35ae4821ca5f0491844d9343c816883b3218a265a4a95ecec66e76f239/krb5-0.9.0-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7a021e869833bfed44ed4c9dfefd25813fab40a381ad4482b79ea36744545f62" }, - { url = "https://mirrors.aliyun.com/pypi/packages/41/32/d2a9d977d23425777c0c5722947e6f0bc80882c7de15038bd02f9269c01a/krb5-0.9.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:2bca06e7ce1551f3eb7f674508bc34d9f44fa1c9056f24120878ec9ab8e430cb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7d/b9/fd0079f9208738bc09cf99208fc92cdf7d8b6a2a363af9a73256efa76399/krb5-0.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ea2ee73bb5aa7818ce50895fb29c276c7fe478c6eda121a5b66b0cc45fe2d42e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ca/9f/df4e24995e0fea7792e8ab152124c65ac845e00ddfb4dd8f7d22907d122b/krb5-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9da4558a47ec105a1a2c185bb4b2d1dd90b7c021e3116895171f913ef048d03" }, -] - -[[package]] -name = "markdown" -version = "3.10.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3" }, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" }, -] - -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" }, -] - -[[package]] -name = "pillow" -version = "12.1.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75" }, - { url = "https://mirrors.aliyun.com/pypi/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304" }, - { url = "https://mirrors.aliyun.com/pypi/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208" }, - { url = "https://mirrors.aliyun.com/pypi/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661" }, - { url = "https://mirrors.aliyun.com/pypi/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670" }, - { url = "https://mirrors.aliyun.com/pypi/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82" }, - { url = "https://mirrors.aliyun.com/pypi/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796" }, - { url = "https://mirrors.aliyun.com/pypi/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13" }, - { url = "https://mirrors.aliyun.com/pypi/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef" }, - { url = "https://mirrors.aliyun.com/pypi/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955" }, -] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" }, -] - -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" }, -] - -[[package]] -name = "pyjwt" -version = "2.12.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c" }, -] - -[[package]] -name = "pyotp" -version = "2.9.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f3/b2/1d5994ba2acde054a443bd5e2d384175449c7d2b6d1a0614dbca3a63abfc/pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c3/c0/c33c8792c3e50193ef55adb95c1c3c2786fe281123291c2dbf0eaab95a6f/pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612" }, -] - -[[package]] -name = "pyrefly" -version = "0.60.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c6/c7/28d14b64888e2d03815627ebff8d57a9f08389c4bbebfe70ae1ed98a1267/pyrefly-0.60.0.tar.gz", hash = "sha256:2499f5b6ff5342e86dfe1cd94bcce133519bbbc93b7ad5636195fea4f0fa3b81" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/31/99/6c9984a09220e5eb7dd5c869b7a32d25c3d06b5e8854c6eb679db1145c3e/pyrefly-0.60.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bf1691af0fee69d0c99c3c6e9d26ab6acd3c8afef96416f9ba2e74934833b7b5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/b3/6216aa3c00c88e59a27eb4149851b5affe86eeea6129f4224034a32dddb0/pyrefly-0.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e71b70c9b95545cf3b479bc55d1381b531de7b2380eb64411088a1e56b634cb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9b/87/eb8dd73abd92a93952ac27a605e463c432fb250fb23186574038c7035594/pyrefly-0.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:680ee5f8f98230ea145652d7344708f5375786209c5bf03d8b911fdb0d0d4195" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0d/34/dc6aeb67b840c745fcee6db358295d554abe6ab555a7eaaf44624bd80bf1/pyrefly-0.60.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d0b20dbbe4aff15b959e8d825b7521a144c4122c11e57022e83b36568c54470" }, - { url = "https://mirrors.aliyun.com/pypi/packages/66/6b/c863fcf7ef592b7d1db91502acf0d1113be8bed7a2a7143fc6f0dd90616f/pyrefly-0.60.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2911563c8e6b2eaefff68885c94727965469a35375a409235a7a4d2b7157dc15" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8e/a2/25ea095ab2ecca8e62884669b11a79f14299db93071685b73a97efbaf4f3/pyrefly-0.60.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a631d9d04705e303fe156f2e62551611bc7ef8066c34708ceebcfb3088bd55" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8e/2c/097bdc6e8d40676b28eb03710a4577bc3c7b803cd24693ac02bf15de3d67/pyrefly-0.60.0-py3-none-win32.whl", hash = "sha256:a08d69298da5626cf502d3debbb6944fd13d2f405ea6625363751f1ff570d366" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/d4/8d27fe310e830c8d11ab73db38b93f9fd2e218744b6efb1204401c9a74d5/pyrefly-0.60.0-py3-none-win_amd64.whl", hash = "sha256:56cf30654e708ae1dd635ffefcba4fa4b349dd7004a6ccc5c41e3a9bb944320c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1f/ad/8eea1f8fb8209f91f6dbfe48000c9d05fd0cdb1b5b3157283c9b1dada55d/pyrefly-0.60.0-py3-none-win_arm64.whl", hash = "sha256:b6d27fba970f4777063c0227c54167d83bece1804ea34f69e7118e409ba038d2" }, -] - -[[package]] -name = "pyspnego" -version = "0.12.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "sspilib", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f9/e4/8b32a91aeba6fbc6943a630c44b2fe038615e5c7dec8814316fafdcf4bf4/pyspnego-0.12.0.tar.gz", hash = "sha256:e1d9cd3520a87a1d6db8d68783b17edc4e1464eae3d51ead411a51c82dbaae67" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/01/e9/95430b8f3b747ebd3b86a66484a79ef387167655bcb15ab416f563045565/pyspnego-0.12.0-py3-none-any.whl", hash = "sha256:84cc8dae6ad21e04b37c50c1d3c743f05f193e39498f6010cc68ec1146afd007" }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9" }, -] - -[[package]] -name = "pytest-django" -version = "4.12.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" }, -] - -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe" }, - { url = "https://mirrors.aliyun.com/pypi/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440" }, - { url = "https://mirrors.aliyun.com/pypi/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321" }, - { url = "https://mirrors.aliyun.com/pypi/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324" }, - { url = "https://mirrors.aliyun.com/pypi/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de" }, -] - -[[package]] -name = "pywinrm" -version = "0.4.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "requests" }, - { name = "requests-ntlm" }, - { name = "six" }, - { name = "xmltodict" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7c/ba/78329e124138f8edf40a41b4252baf20cafdbea92ea45d50ec712124e99b/pywinrm-0.4.3.tar.gz", hash = "sha256:995674bf5ac64b2562c9c56540473109e530d36bde10c262d5a5296121ad5565" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/5c/1a/74bdbb7a3f8a6c1d2254c39c53c2d388529a314366130147d180522c59a3/pywinrm-0.4.3-py2.py3-none-any.whl", hash = "sha256:c476c1e20dd0875da6fe4684c4d8e242dd537025907c2f11e1990c5aee5c9526" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00" }, - { url = "https://mirrors.aliyun.com/pypi/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" }, - { url = "https://mirrors.aliyun.com/pypi/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" }, - { url = "https://mirrors.aliyun.com/pypi/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" }, - { url = "https://mirrors.aliyun.com/pypi/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" }, - { url = "https://mirrors.aliyun.com/pypi/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" }, - { url = "https://mirrors.aliyun.com/pypi/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" }, - { url = "https://mirrors.aliyun.com/pypi/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788" }, - { url = "https://mirrors.aliyun.com/pypi/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" }, - { url = "https://mirrors.aliyun.com/pypi/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" }, -] - -[[package]] -name = "redis" -version = "7.4.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec" }, -] - -[[package]] -name = "regex" -version = "2026.5.9" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44" }, - { url = "https://mirrors.aliyun.com/pypi/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538" }, - { url = "https://mirrors.aliyun.com/pypi/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00" }, - { url = "https://mirrors.aliyun.com/pypi/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03" }, - { url = "https://mirrors.aliyun.com/pypi/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48" }, - { url = "https://mirrors.aliyun.com/pypi/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555" }, - { url = "https://mirrors.aliyun.com/pypi/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919" }, - { url = "https://mirrors.aliyun.com/pypi/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66" }, - { url = "https://mirrors.aliyun.com/pypi/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962" }, - { url = "https://mirrors.aliyun.com/pypi/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070" }, - { url = "https://mirrors.aliyun.com/pypi/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04" }, - { url = "https://mirrors.aliyun.com/pypi/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88" }, - { url = "https://mirrors.aliyun.com/pypi/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178" }, - { url = "https://mirrors.aliyun.com/pypi/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de" }, - { url = "https://mirrors.aliyun.com/pypi/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14" }, - { url = "https://mirrors.aliyun.com/pypi/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763" }, - { url = "https://mirrors.aliyun.com/pypi/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372" }, - { url = "https://mirrors.aliyun.com/pypi/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20" }, - { url = "https://mirrors.aliyun.com/pypi/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58" }, - { url = "https://mirrors.aliyun.com/pypi/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77" }, - { url = "https://mirrors.aliyun.com/pypi/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa" }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" }, -] - -[[package]] -name = "requests-ntlm" -version = "1.3.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyspnego" }, - { name = "requests" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/15/74/5d4e1815107e9d78c44c3ad04740b00efd1189e5a9ec11e5275b60864e54/requests_ntlm-1.3.0.tar.gz", hash = "sha256:b29cc2462623dffdf9b88c43e180ccb735b4007228a542220e882c58ae56c668" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/9e/5d/836b97537a390cf811b0488490c389c5a614f0a93acb23f347bd37a2d914/requests_ntlm-1.3.0-py3-none-any.whl", hash = "sha256:4c7534a7d0e482bb0928531d621be4b2c74ace437e88c5a357ceb7452d25a510" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" }, -] - -[[package]] -name = "sqlparse" -version = "0.5.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb" }, -] - -[[package]] -name = "sspilib" -version = "0.5.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a7/e6/d0d74b18bed8c16949fddc0401005c072947ae7bf1bab982ed28f9ebc2d8/sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/6d/cb/7cc967b48d182cb012229ccc9f9e3fd5e245b7f0c80667297ddded580877/sspilib-0.5.0-cp310-cp310-win32.whl", hash = "sha256:8dab68e994d24a08f854d36ac96409b3b8cc03fdebc590925f76f9d733c3a902" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/0d/7746ade4e3c4dba6c6d9b2afe3c3903f4bcab2da6b300b5d81afee089196/sspilib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e1947df07110ee1861009fc117bd089a7710403f3f5c488fb52a6e00b7c5b84" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8d/fb/6821418037e9d78179153e770e2b0280956f28f4bf51069dcbcc0348505d/sspilib-0.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:28d0eb944f7ff70bc99fe729d06fa230aec1649c5bc216809e359cd0c77d4840" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ce/1b/dd9066491168933b0f7ab6e396ac58cc024c8954e95264c38e3dc9363d7c/sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/6a/a11abf90172ff580ac2f9ade3496d868e05e851c4ecf487dd5baeb966b1d/sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45" }, - { url = "https://mirrors.aliyun.com/pypi/packages/45/05/983876d281b9e7926f1c9126e72de8bd5928b1de45433163f54d4e217502/sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/43/f8/34e8e86883054b961c2eb88a5b42b89b2bf975723b1acca090966c2d03ff/sspilib-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:9dad272abf3f4cf0bf95d495075d2987f6ba1fb300f8d603661ccac07d11272f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/d8/8c4ba75f925fd9651cb855c47e0e67931a175d6fd41e569193a8d58133ac/sspilib-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7d7724d5dbb31f68e62465863dfb862fe2793281ce40d0c8f2dc60c8f07998f2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/74/c3/07af17b6fcc2b02af294a8817e30441a502880a04c8d60be2d71e0a1eacc/sspilib-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8ce23ec740dee025136370ed4ae64b7d1535368321049ef960012a57c93ebe15" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076" }, - { url = "https://mirrors.aliyun.com/pypi/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049" }, - { url = "https://mirrors.aliyun.com/pypi/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585" }, - { url = "https://mirrors.aliyun.com/pypi/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917" }, - { url = "https://mirrors.aliyun.com/pypi/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257" }, - { url = "https://mirrors.aliyun.com/pypi/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54" }, - { url = "https://mirrors.aliyun.com/pypi/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897" }, - { url = "https://mirrors.aliyun.com/pypi/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36" }, - { url = "https://mirrors.aliyun.com/pypi/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662" }, - { url = "https://mirrors.aliyun.com/pypi/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853" }, - { url = "https://mirrors.aliyun.com/pypi/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15" }, - { url = "https://mirrors.aliyun.com/pypi/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232" }, - { url = "https://mirrors.aliyun.com/pypi/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41" }, - { url = "https://mirrors.aliyun.com/pypi/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf" }, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250915" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, -] - -[[package]] -name = "vine" -version = "5.1.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc" }, -] - -[[package]] -name = "wcwidth" -version = "0.3.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/07/0b5bcc9812b1b2fd331cc88289ef4d47d428afdbbf0216bb7d53942d93d6/wcwidth-0.3.2.tar.gz", hash = "sha256:d469b3059dab6b1077def5923ed0a8bf5738bd4a1a87f686d5e2de455354c4ad" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/72/c6/1452e716c5af065c018f75d42ca97517a04ac6aae4133722e0424649a07c/wcwidth-0.3.2-py3-none-any.whl", hash = "sha256:817abc6a89e47242a349b5d100cbd244301690d6d8d2ec6335f26fe6640a6315" }, -] - -[[package]] -name = "whitenoise" -version = "6.12.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2" }, -] - -[[package]] -name = "xmltodict" -version = "1.0.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d" }, -] diff --git a/uv.toml b/uv.toml deleted file mode 100644 index c50b6b0..0000000 --- a/uv.toml +++ /dev/null @@ -1,25 +0,0 @@ -[[index]] -name = "tsinghua" -url = "https://pypi.tuna.tsinghua.edu.cn/simple" -default = true - -[[index]] -name = "aliyun" -url = "https://mirrors.aliyun.com/pypi/simple" - -[[index]] -name = "ustc" -url = "https://pypi.mirrors.ustc.edu.cn/simple" - -# Python 解释器下载镜像配置 -# uv 默认从 GitHub (astral-sh/python-build-standalone) 下载 Python -# 国内可通过以下方式加速: -# -# 方式1:环境变量(推荐,灵活配置) -# export UV_PYTHON_INSTALL_MIRROR=https://your-mirror.com/github/astral-sh/python-build-standalone/releases/download -# -# 方式2:使用 GitHub 代理 -# export UV_PYTHON_INSTALL_MIRROR=https://ghproxy.com/https://github.com/astral-sh/python-build-standalone/releases/download -# -# 方式3:本地镜像 -# export UV_PYTHON_INSTALL_MIRROR=file:///path/to/local/python-build-standalone From 0683ad62a1536d3aca5376ac24650504528aa41f Mon Sep 17 00:00:00 2001 From: trustedinster Date: Thu, 18 Jun 2026 13:27:07 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E9=AB=98=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=A8=A1=E5=9D=97=E5=8C=96=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- app/cli/__init__.py | 6 + app/cli/__main__.py | 5 + app/cli/account.py | 381 ++++++++++++++++++++++++++++++++++++++++++++ app/cli/db.py | 143 +++++++++++++++++ app/cli/main.py | 116 ++++++++++++++ app/cli/plugins.py | 244 ++++++++++++++++++++++++++++ app/cli/server.py | 189 ++++++++++++++++++++++ app/cli/static.py | 199 +++++++++++++++++++++++ app/cli/tenant.py | 310 +++++++++++++++++++++++++++++++++++ app/cli/utils.py | 82 ++++++++++ pyproject.toml | 6 + 11 files changed, 1681 insertions(+) create mode 100644 app/cli/__init__.py create mode 100644 app/cli/__main__.py create mode 100644 app/cli/account.py create mode 100644 app/cli/db.py create mode 100644 app/cli/main.py create mode 100644 app/cli/plugins.py create mode 100644 app/cli/server.py create mode 100644 app/cli/static.py create mode 100644 app/cli/tenant.py create mode 100644 app/cli/utils.py diff --git a/app/cli/__init__.py b/app/cli/__init__.py new file mode 100644 index 0000000..e94fe8f --- /dev/null +++ b/app/cli/__init__.py @@ -0,0 +1,6 @@ +"""2c2a CLI 管理工具。 + +提供数据库迁移、账户管理、服务器启动、插件管理、collectstatic 等实用功能。 + +入口:``2c2a``(pyproject.toml 注册)或 ``python -m app.cli``。 +""" diff --git a/app/cli/__main__.py b/app/cli/__main__.py new file mode 100644 index 0000000..ef2f688 --- /dev/null +++ b/app/cli/__main__.py @@ -0,0 +1,5 @@ +"""允许通过 ``python -m app.cli`` 调用 CLI。""" +from app.cli.main import app + +if __name__ == "__main__": + app() diff --git a/app/cli/account.py b/app/cli/account.py new file mode 100644 index 0000000..a3f4df6 --- /dev/null +++ b/app/cli/account.py @@ -0,0 +1,381 @@ +"""账户管理命令。 + +用法: + 2c2a account createsuperuser # 创建超级管理员 + 2c2a account create # 创建普通用户 + 2c2a account list # 列出用户 + 2c2a account changepassword # 修改密码 + 2c2a account activate # 启用账号 + 2c2a account deactivate # 禁用账号 + 2c2a account ban # 封禁账号(递增 ban_version) + 2c2a account unban # 解封 + 2c2a account promote # 提升为 staff + 2c2a account demote # 取消 staff + 2c2a account delete # 删除账号 +""" +from __future__ import annotations + +from datetime import datetime, timezone + +import typer +from sqlalchemy import select + +from app.cli.utils import ( + blake2b_prehash_interactive, + confirm, + db_session, + error, + print_table, + run_async, + success, + warn, +) +from app.models.user import User, UserBan, UserProfile +from app.security.password import hash_password + +account_app = typer.Typer(help="账户管理", no_args_is_help=True) + + +async def _get_user_by_username(session, username: str) -> User | None: + result = await session.execute(select(User).where(User.username == username)) + return result.scalar_one_or_none() + + +@account_app.command("createsuperuser") +def create_superuser( + username: str = typer.Option(..., "--username", "-u", help="用户名"), + email: str = typer.Option(None, "--email", "-e", help="邮箱"), + password: str = typer.Option(None, "--password", "-p", help="密码(不传则交互输入)"), +): + """创建超级管理员。""" + _create_user( + username=username, + email=email, + password=password, + is_superuser=True, + is_staff=True, + is_verified=True, + label="超级管理员", + ) + + +@account_app.command("create") +def create_user( + username: str = typer.Option(..., "--username", "-u", help="用户名"), + email: str = typer.Option(None, "--email", "-e", help="邮箱"), + password: str = typer.Option(None, "--password", "-p", help="密码(不传则交互输入)"), + staff: bool = typer.Option(False, "--staff", help="赋予 staff 权限"), +): + """创建普通用户。""" + _create_user( + username=username, + email=email, + password=password, + is_superuser=False, + is_staff=staff, + is_verified=False, + label="用户", + ) + + +def _create_user( + username: str, + email: str | None, + password: str | None, + is_superuser: bool, + is_staff: bool, + is_verified: bool, + label: str, +) -> None: + # 密码处理:交互输入或命令行传入 + if password: + if len(password) < 8: + error("密码至少 8 位") + raise typer.Exit(1) + import hashlib + + prehash = hashlib.blake2b(password.encode(), digest_size=64).hexdigest() + else: + prehash = blake2b_prehash_interactive("密码") + + async def _do(): + async with db_session() as session: + existing = await _get_user_by_username(session, username) + if existing is not None: + error(f"用户名 {username} 已存在") + raise typer.Exit(1) + user = User( + username=username, + email=email, + password_hash=hash_password(prehash), + is_active=True, + is_staff=is_staff, + is_superuser=is_superuser, + is_verified=is_verified, + ) + session.add(user) + await session.flush() + session.add(UserProfile(user_id=user.id)) + return user.id + + user_id = run_async(_do()) + success(f"{label}已创建: {username} (id={user_id})") + + +@account_app.command("list") +def list_users( + staff: bool = typer.Option(False, "--staff", help="仅显示 staff"), + superuser: bool = typer.Option(False, "--superuser", help="仅显示超管"), + active: bool = typer.Option(False, "--active", help="仅显示启用"), + limit: int = typer.Option(100, "--limit", "-n", help="最多显示数量"), +): + """列出用户。""" + from sqlalchemy import or_ + + async def _do(): + async with db_session() as session: + stmt = select(User) + if staff: + stmt = stmt.where(User.is_staff == True) # noqa: E712 + if superuser: + stmt = stmt.where(User.is_superuser == True) # noqa: E712 + if active: + stmt = stmt.where(User.is_active == True) # noqa: E712 + stmt = stmt.order_by(User.id).limit(limit) + result = await session.execute(stmt) + return result.scalars().all() + + users = run_async(_do()) + rows = [ + [ + u.id, + u.username, + u.email or "-", + "✓" if u.is_active else "✗", + "✓" if u.is_staff else "-", + "✓" if u.is_superuser else "-", + u.ban_version, + u.date_joined.strftime("%Y-%m-%d %H:%M") if u.date_joined else "-", + ] + for u in users + ] + print_table( + f"用户列表(共 {len(users)} 个)", + ["ID", "用户名", "邮箱", "启用", "Staff", "超管", "BanVer", "注册时间"], + rows, + ) + + +@account_app.command("changepassword") +def change_password( + username: str = typer.Argument(..., help="用户名"), + password: str = typer.Option(None, "--password", "-p", help="新密码(不传则交互输入)"), +): + """修改用户密码。""" + if password: + if len(password) < 8: + error("密码至少 8 位") + raise typer.Exit(1) + import hashlib + + prehash = hashlib.blake2b(password.encode(), digest_size=64).hexdigest() + else: + prehash = blake2b_prehash_interactive("新密码") + + async def _do(): + async with db_session() as session: + user = await _get_user_by_username(session, username) + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + user.password_hash = hash_password(prehash) + # 修改密码后递增 ban_version,使旧令牌失效 + user.ban_version += 1 + + run_async(_do()) + success(f"已修改 {username} 的密码(旧令牌已失效)") + + +@account_app.command("activate") +def activate(username: str = typer.Argument(..., help="用户名")): + """启用账号。""" + _set_flag(username, is_active=True, msg="已启用") + + +@account_app.command("deactivate") +def deactivate(username: str = typer.Argument(..., help="用户名")): + """禁用账号。""" + _set_flag(username, is_active=False, msg="已禁用", bump_ban=True) + + +@account_app.command("promote") +def promote(username: str = typer.Argument(..., help="用户名")): + """提升为 staff(管理员)。""" + _set_flag(username, is_staff=True, msg="已提升为 staff") + + +@account_app.command("demote") +def demote(username: str = typer.Argument(..., help="用户名")): + """取消 staff 权限。""" + _set_flag(username, is_staff=False, msg="已取消 staff") + + +@account_app.command("superuser") +def set_superuser( + username: str = typer.Argument(..., help="用户名"), + revoke: bool = typer.Option(False, "--revoke", help="取消超管权限"), +): + """授予/取消超级管理员权限。""" + _set_flag( + username, + is_superuser=not revoke, + msg="已取消超管" if revoke else "已授予超管", + ) + + +def _set_flag( + username: str, + *, + is_active: bool | None = None, + is_staff: bool | None = None, + is_superuser: bool | None = None, + msg: str, + bump_ban: bool = False, +) -> None: + async def _do(): + async with db_session() as session: + user = await _get_user_by_username(session, username) + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + if is_active is not None: + user.is_active = is_active + if is_staff is not None: + user.is_staff = is_staff + if is_superuser is not None: + user.is_superuser = is_superuser + if bump_ban: + user.ban_version += 1 + + run_async(_do()) + success(f"{username} {msg}") + + +@account_app.command("ban") +def ban_user( + username: str = typer.Argument(..., help="用户名"), + reason: str = typer.Option("管理员封禁", "--reason", "-r", help="封禁原因"), +): + """封禁账号(递增 ban_version,所有令牌立即失效)。""" + from sqlalchemy.orm import selectinload + + async def _do(): + async with db_session() as session: + result = await session.execute( + select(User).options(selectinload(User.active_ban)).where(User.username == username) + ) + user = result.scalar_one_or_none() + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + # 创建/更新封禁记录 + if user.active_ban is None: + session.add(UserBan(user_id=user.id, reason=reason)) + else: + user.active_ban.reason = reason + user.is_active = False + user.ban_version += 1 # 无状态秒级令牌撤销 + + run_async(_do()) + warn(f"已封禁 {username}(原因:{reason}),所有令牌已失效") + + +@account_app.command("unban") +def unban_user(username: str = typer.Argument(..., help="用户名")): + """解封账号。""" + from sqlalchemy import delete + + async def _do(): + async with db_session() as session: + user = await _get_user_by_username(session, username) + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + # 删除封禁记录 + await session.execute(delete(UserBan).where(UserBan.user_id == user.id)) + user.is_active = True + + run_async(_do()) + success(f"已解封 {username}") + + +@account_app.command("delete") +def delete_user( + username: str = typer.Argument(..., help="用户名"), + yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"), +): + """删除账号(级联删除关联数据)。""" + if not yes and not confirm(f"⚠️ 确认删除用户 {username}?此操作不可恢复"): + raise typer.Abort() + + async def _do(): + async with db_session() as session: + user = await _get_user_by_username(session, username) + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + await session.delete(user) + + run_async(_do()) + warn(f"已删除用户 {username}") + + +@account_app.command("info") +def user_info(username: str = typer.Argument(..., help="用户名")): + """查看用户详情。""" + async def _do(): + async with db_session() as session: + from sqlalchemy.orm import selectinload + + result = await session.execute( + select(User).options(selectinload(User.active_ban)).where(User.username == username) + ) + user = result.scalar_one_or_none() + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + # 在会话内预加载所有需要的属性,避免 DetachedInstanceError + return { + "id": user.id, + "username": user.username, + "email": user.email, + "phone": user.phone, + "is_active": user.is_active, + "is_staff": user.is_staff, + "is_superuser": user.is_superuser, + "is_verified": user.is_verified, + "ban_version": user.ban_version, + "date_joined": user.date_joined, + "last_login": user.last_login, + "last_login_ip": user.last_login_ip, + "ban_reason": user.active_ban.reason if user.active_ban else None, + } + + data = run_async(_do()) + from app.cli.utils import console + + console.print(f"\n[bold]用户详情[/bold]") + console.print(f" ID: {data['id']}") + console.print(f" 用户名: {data['username']}") + console.print(f" 邮箱: {data['email'] or '-'}") + console.print(f" 电话: {data['phone'] or '-'}") + console.print(f" 启用: {'✓' if data['is_active'] else '✗'}") + console.print(f" Staff: {'✓' if data['is_staff'] else '-'}") + console.print(f" 超管: {'✓' if data['is_superuser'] else '-'}") + console.print(f" 已验证: {'✓' if data['is_verified'] else '✗'}") + console.print(f" Ban版本: {data['ban_version']}") + console.print(f" 注册时间: {data['date_joined']}") + console.print(f" 最后登录: {data['last_login'] or '-'}") + console.print(f" 最后登录IP: {data['last_login_ip'] or '-'}") + if data["ban_reason"]: + console.print(f" [red]封禁中[/red]: {data['ban_reason']}") diff --git a/app/cli/db.py b/app/cli/db.py new file mode 100644 index 0000000..1e37abd --- /dev/null +++ b/app/cli/db.py @@ -0,0 +1,143 @@ +"""数据库迁移命令(基于 Alembic)。 + +用法: + 2c2a db init # 初始化数据库(create_all,开发用) + 2c2a db migrate # 生成迁移脚本(autogenerate) + 2c2a db upgrade [rev] # 升级到指定版本(默认 head) + 2c2a db downgrade # 回滚到指定版本 + 2c2a db history # 查看迁移历史 + 2c2a db current # 查看当前版本 + 2c2a db heads # 查看最新版本 + 2c2a db reset # 危险:重置数据库(drop_all + create_all) +""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import typer + +from app.cli.utils import confirm, error, info, run_async, success, warn + +db_app = typer.Typer(help="数据库迁移与管理", no_args_is_help=True) + +# Alembic 配置文件路径 +_ALEMBIC_INI = str(Path(__file__).resolve().parent.parent.parent / "alembic.ini") + + +def _run_alembic(*args: str) -> int: + """执行 alembic 子命令,透传输出。""" + cmd = [sys.executable, "-m", "alembic", "-c", _ALEMBIC_INI, *args] + info(f"执行: {' '.join(cmd)}") + result = subprocess.run(cmd, check=False) + return result.returncode + + +@db_app.command("init") +def init_db(): + """初始化数据库(直接 create_all,跳过迁移,开发用)。 + + 生产环境请使用 ``2c2a db upgrade`` 走 Alembic 迁移。 + """ + from app.core.db import engine + from app.models import Base + + async def _create(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await engine.dispose() + + run_async(_create()) + success("数据库表已创建(create_all)") + + +@db_app.command("migrate") +def make_migrations( + message: str = typer.Option(..., "-m", "--message", help="迁移说明"), + empty: bool = typer.Option(False, "--empty", help="生成空迁移脚本(手动编辑)"), +): + """生成迁移脚本(autogenerate)。""" + args = ["revision", "--autogenerate", "-m", message] + if empty: + args = ["revision", "-m", message] + rc = _run_alembic(*args) + if rc == 0: + success("迁移脚本已生成") + else: + error("迁移脚本生成失败") + raise typer.Exit(rc) + + +@db_app.command("upgrade") +def upgrade( + revision: str = typer.Argument("head", help="目标版本(默认 head)"), +): + """升级数据库到指定版本。""" + rc = _run_alembic("upgrade", revision) + if rc == 0: + success(f"已升级到 {revision}") + else: + raise typer.Exit(rc) + + +@db_app.command("downgrade") +def downgrade( + revision: str = typer.Argument(..., help="目标版本(如 -1 回滚一步)"), +): + """回滚数据库到指定版本。""" + if not confirm(f"确认回滚到 {revision}?此操作可能丢失数据"): + raise typer.Abort() + rc = _run_alembic("downgrade", revision) + if rc == 0: + success(f"已回滚到 {revision}") + else: + raise typer.Exit(rc) + + +@db_app.command("history") +def history( + verbose: bool = typer.Option(False, "-v", "--verbose", help="显示详细信息"), +): + """查看迁移历史。""" + args = ["history"] + if verbose: + args.append("--verbose") + _run_alembic(*args) + + +@db_app.command("current") +def current(): + """查看当前数据库版本。""" + _run_alembic("current") + + +@db_app.command("heads") +def heads(): + """查看最新迁移版本。""" + _run_alembic("heads") + + +@db_app.command("reset") +def reset_db( + yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"), +): + """危险:重置数据库(drop 所有表 + create_all)。 + + 仅用于开发环境,生产环境请用迁移。 + """ + if not yes and not confirm("⚠️ 这将删除所有数据!确认重置数据库?", default=False): + raise typer.Abort() + warn("正在删除所有表...") + + from app.core.db import engine + from app.models import Base + + async def _reset(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + await engine.dispose() + + run_async(_reset()) + success("数据库已重置") diff --git a/app/cli/main.py b/app/cli/main.py new file mode 100644 index 0000000..4de45ae --- /dev/null +++ b/app/cli/main.py @@ -0,0 +1,116 @@ +"""2c2a CLI 主入口。 + +用法: + 2c2a --help # 查看所有命令 + 2c2a db upgrade # 数据库迁移 + 2c2a account createsuperuser # 创建超管 + 2c2a serve # 启动服务器 + 2c2a plugin list # 插件管理 + 2c2a collectstatic # 收集静态文件 + 2c2a keys generate # 生成密钥 + +也可通过 ``python -m app.cli`` 调用。 +""" +from __future__ import annotations + +import typer +from rich.console import Console + +from app.cli.account import account_app +from app.cli.db import db_app +from app.cli.plugins import plugin_app +from app.cli.server import server_app +from app.cli.static import collectstatic, keys_app +from app.cli.tenant import tenant_app + +console = Console() + +app = typer.Typer( + name="2c2a", + help="2c2a 异步架构管理工具(数据库迁移、账户、服务器、插件、静态资源、密钥、租户)", + no_args_is_help=True, + rich_markup_mode="rich", + add_completion=False, +) + + +@app.callback() +def main( + version: bool = typer.Option( + False, "--version", "-V", help="显示版本号", is_eager=True + ), +): + """2c2a 管理工具。""" + if version: + from app import __version__ + + console.print(f"2c2a v{__version__}") + raise typer.Exit() + + +# 注册子命令组 +app.add_typer(db_app, name="db", help="数据库迁移与管理") +app.add_typer(account_app, name="account", help="账户管理") +app.add_typer(server_app, name="serve", help="服务器与运行时") +app.add_typer(plugin_app, name="plugin", help="插件管理") +app.add_typer(keys_app, name="keys", help="密钥生成") +app.add_typer(tenant_app, name="tenant", help="租户(站点组)管理") + +# 顶层直接命令 +app.command(name="collectstatic", help="收集静态文件到指定目录")(collectstatic) + + +# ── 顶层快捷命令 ── + +@app.command("migrate") +def migrate_fast( + message: str = typer.Option(None, "-m", "--message", help="迁移说明"), + target: str = typer.Option(None, "--target", help="目标版本(默认 head)"), +): + """快捷:生成迁移并升级(等同 db migrate + db upgrade)。""" + from app.cli.db import _run_alembic + + if message: + rc = _run_alembic("revision", "--autogenerate", "-m", message) + if rc != 0: + raise typer.Exit(rc) + rc = _run_alembic("upgrade", target or "head") + if rc != 0: + raise typer.Exit(rc) + console.print("[green]✓[/green] 迁移完成") + + +@app.command("createsuperuser") +def createsuperuser_fast( + username: str = typer.Option(..., "--username", "-u"), + email: str = typer.Option(None, "--email", "-e"), + password: str = typer.Option(None, "--password", "-p"), +): + """快捷:创建超级管理员(等同 account createsuperuser)。""" + from app.cli.account import _create_user + + _create_user( + username=username, + email=email, + password=password, + is_superuser=True, + is_staff=True, + is_verified=True, + label="超级管理员", + ) + + +@app.command("runserver") +def runserver_fast( + host: str = typer.Option(None, "--host", "-h"), + port: int = typer.Option(None, "--port", "-p"), + reload: bool = typer.Option(False, "--reload"), +): + """快捷:启动开发服务器(等同 serve serve --reload)。""" + from app.cli.server import serve + + serve(host=host, port=port, workers=1, reload=reload, interface="asgi") + + +if __name__ == "__main__": + app() diff --git a/app/cli/plugins.py b/app/cli/plugins.py new file mode 100644 index 0000000..af28537 --- /dev/null +++ b/app/cli/plugins.py @@ -0,0 +1,244 @@ +"""插件管理命令。 + +用法: + 2c2a plugin list # 列出所有插件 + 2c2a plugin info # 查看插件详情 + 2c2a plugin enable # 启用插件 + 2c2a plugin disable # 禁用插件 + 2c2a plugin reload # 重新发现并加载所有插件 + 2c2a plugin routes # 查看插件挂载的路由 + 2c2a plugin services # 查看插件注册的服务 +""" +from __future__ import annotations + +import asyncio + +import typer + +from app.cli.utils import console, error, info, print_table, success, warn +from app.plugins import get_plugin_manager +from app.plugins.loader import PluginLoader + +plugin_app = typer.Typer(help="插件管理", no_args_is_help=True) + + +async def _load_plugins(): + """发现并加载所有插件(CLI 上下文)。""" + manager = get_plugin_manager() + loader = PluginLoader(manager) + await loader.load_discovered() + return manager + + +@plugin_app.command("list") +def list_plugins(): + """列出所有已发现的插件。""" + manager = asyncio.run(_load_plugins()) + plugins = manager.list_plugins() + if not plugins: + info("未发现任何插件") + return + + rows = [ + [ + p.get("plugin_id", p.get("id", "-")), + p.get("name", "-"), + p.get("version", "-"), + "✓" if p.get("enabled", False) else "✗", + p.get("description", "")[:40], + ] + for p in plugins + ] + print_table( + f"插件列表(共 {len(plugins)} 个)", + ["ID", "名称", "版本", "启用", "描述"], + rows, + ) + + +@plugin_app.command("info") +def plugin_info(plugin_id: str = typer.Argument(..., help="插件 ID")): + """查看插件详情。""" + manager = asyncio.run(_load_plugins()) + plugin = manager.get_plugin(plugin_id) + if plugin is None: + error(f"插件 {plugin_id} 不存在") + raise typer.Exit(1) + + console.print(f"\n[bold]插件详情[/bold]") + console.print(f" ID: {plugin.plugin_id}") + console.print(f" 名称: {plugin.name}") + console.print(f" 版本: {plugin.version}") + console.print(f" 描述: {plugin.description or '-'}") + console.print(f" 启用: {'✓' if plugin.enabled else '✗'}") + console.print(f" 类: {type(plugin).__module__}.{type(plugin).__name__}") + + # 能力检测 + from app.plugins.base import RouteProvider, ServiceProvider, UIExtensionProvider + + capabilities = [] + if isinstance(plugin, RouteProvider): + capabilities.append("路由提供者") + if isinstance(plugin, ServiceProvider): + capabilities.append(f"服务提供者({plugin.get_service_name()})") + if isinstance(plugin, UIExtensionProvider): + capabilities.append("UI 扩展提供者") + console.print(f" 能力: {', '.join(capabilities) or '-'}") + + # 路由 + if isinstance(plugin, RouteProvider): + console.print("\n [bold]路由:[/bold]") + for prefix, router in manager.get_routers(): + if prefix.strip("/") == plugin_id: + for route in router.routes: + path = getattr(route, "path", "?") + methods = getattr(route, "methods", set()) or set() + console.print(f" {','.join(sorted(methods)):20s} {prefix}{path}") + + +@plugin_app.command("enable") +def enable_plugin(plugin_id: str = typer.Argument(..., help="插件 ID")): + """启用插件。""" + manager = asyncio.run(_load_plugins()) + + async def _do(): + ok = await manager.enable_plugin(plugin_id) + return ok + + ok = asyncio.run(_do()) + if ok: + success(f"插件 {plugin_id} 已启用") + else: + error(f"启用失败:插件 {plugin_id} 不存在") + raise typer.Exit(1) + + +@plugin_app.command("disable") +def disable_plugin(plugin_id: str = typer.Argument(..., help="插件 ID")): + """禁用插件。""" + manager = asyncio.run(_load_plugins()) + + async def _do(): + ok = await manager.disable_plugin(plugin_id) + return ok + + ok = asyncio.run(_do()) + if ok: + warn(f"插件 {plugin_id} 已禁用") + else: + error(f"禁用失败:插件 {plugin_id} 不存在") + raise typer.Exit(1) + + +@plugin_app.command("reload") +def reload_plugins(): + """重新发现并加载所有插件。""" + manager = get_plugin_manager() + + async def _do(): + # 先卸载所有 + await manager.shutdown_all() + manager.plugins.clear() + # 重新发现加载 + loader = PluginLoader(manager) + loaded = await loader.load_discovered() + return loaded + + loaded = asyncio.run(_do()) + success(f"已重新加载 {len(loaded)} 个插件: {', '.join(loaded) or '无'}") + + +@plugin_app.command("routes") +def list_routes(): + """查看所有插件挂载的路由。""" + manager = asyncio.run(_load_plugins()) + routers = manager.get_routers() + if not routers: + info("无插件路由") + return + + rows = [] + for prefix, router in routers: + for route in router.routes: + path = getattr(route, "path", "?") + methods = getattr(route, "methods", set()) or set() + rows.append([prefix, ",".join(sorted(methods)), f"{prefix}{path}"]) + print_table("插件路由", ["挂载前缀", "方法", "完整路径"], rows) + + +@plugin_app.command("services") +def list_services(): + """查看插件注册的服务。""" + manager = asyncio.run(_load_plugins()) + services = manager.service_registry.list_services() + if not services: + info("无已注册服务") + return + + rows = [[name] for name in services] + print_table("插件服务", ["服务名"], rows) + + +@plugin_app.command("scaffold") +def scaffold_plugin( + plugin_id: str = typer.Argument(..., help="插件 ID(目录名)"), + name: str = typer.Option(None, "--name", "-n", help="插件显示名"), + description: str = typer.Option("", "--desc", "-d", help="插件描述"), +): + """生成插件骨架。""" + from pathlib import Path + + plugin_dir = Path(__file__).resolve().parent.parent / "plugins" / plugin_id + if plugin_dir.exists(): + error(f"插件目录已存在: {plugin_dir}") + raise typer.Exit(1) + + plugin_dir.mkdir(parents=True) + (plugin_dir / "__init__.py").write_text(f'"""插件 {plugin_id} 包。"""\n') + + display_name = name or plugin_id + plugin_code = f'''"""插件 {display_name}。""" +from __future__ import annotations + +from fastapi import APIRouter + +from app.plugins import PluginInterface, RouteProvider + +__plugin_meta__ = {{ + "id": "{plugin_id}", + "name": "{display_name}", + "version": "0.1.0", + "description": "{description}", + "enabled": True, +}} + + +class Plugin(PluginInterface, RouteProvider): + """{display_name} 插件实现。""" + + def __init__(self) -> None: + super().__init__( + plugin_id="{plugin_id}", + name="{display_name}", + version="0.1.0", + description="{description}", + ) + + async def initialize(self) -> bool: + return True + + async def shutdown(self) -> bool: + return True + + def get_routers(self) -> list[APIRouter]: + router = APIRouter() + + @router.get("/hello") + async def hello(): + return {{"message": "Hello from {display_name}!"}} + + return [router] +''' + (plugin_dir / "plugin.py").write_text(plugin_code) + success(f"插件骨架已生成: {plugin_dir}") + info(f"编辑 {plugin_dir}/plugin.py 实现功能,重启服务后自动加载") diff --git a/app/cli/server.py b/app/cli/server.py new file mode 100644 index 0000000..ca6a666 --- /dev/null +++ b/app/cli/server.py @@ -0,0 +1,189 @@ +"""服务器启动命令。 + +用法: + 2c2a serve # 启动 Granian ASGI 服务器 + 2c2a serve --reload # 开发模式(热重载) + 2c2a serve --workers 4 # 多 worker + 2c2a worker # 启动 RedisHuey 任务消费者 + 2c2a shell # 启动交互式 Python shell(预加载 app 上下文) +""" +from __future__ import annotations + +import code +import subprocess +import sys + +import typer + +from app.cli.utils import info, success, warn +from app.core.config import settings + +server_app = typer.Typer(help="服务器与运行时", no_args_is_help=True) + + +@server_app.command("serve") +def serve( + host: str = typer.Option(None, "--host", "-h", help="监听地址(默认 0.0.0.0)"), + port: int = typer.Option(None, "--port", "-p", help="监听端口(默认 8000)"), + workers: int = typer.Option(None, "--workers", "-w", help="worker 数量"), + reload: bool = typer.Option(False, "--reload", help="开发模式热重载"), + interface: str = typer.Option("asgi", "--interface", help="ASGI/RSGI/WSGI"), +): + """启动 Granian ASGI 服务器。""" + host = host or settings.host + port = port or settings.port + workers = workers or settings.workers + + cmd = [ + sys.executable, + "-m", + "granian", + "--interface", + interface, + f"app.main:app", + "--host", + host, + "--port", + str(port), + ] + if reload: + cmd.append("--reload") + warn("开发模式:热重载已启用") + else: + cmd.extend(["--workers", str(workers)]) + + info(f"启动 Granian: {host}:{port} ({workers} workers, {interface})") + info(f"命令: {' '.join(cmd)}") + # 直接 exec,让信号正确传递 + try: + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + warn("服务器已停止") + except FileNotFoundError: + typer.secho( + "未找到 granian,请安装: pip install granian", + fg=typer.colors.RED, + ) + raise typer.Exit(1) + + +@server_app.command("worker") +def worker( + workers: int = typer.Option(1, "--workers", "-w", help="消费者进程数"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="详细日志"), +): + """启动 RedisHuey 任务消费者(处理后台 WinRM 操作等异步任务)。""" + cmd = [ + sys.executable, + "-m", + "huey_consumer", + "app.tasks.huey_app.huey", + "--workers", + str(workers), + ] + if verbose: + cmd.append("-v") + + info(f"启动 Huey 消费者 ({workers} workers)") + info(f"命令: {' '.join(cmd)}") + try: + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + warn("消费者已停止") + except FileNotFoundError: + typer.secho( + "未找到 huey_consumer,请安装: pip install huey[redis]", + fg=typer.colors.RED, + ) + raise typer.Exit(1) + + +@server_app.command("shell") +def shell(): + """启动交互式 Python shell(预加载应用上下文)。""" + import app # noqa: F401 + from app.core.config import settings as _settings + from app.core.db import AsyncSessionLocal as _Session + from app.models import Base as _Base + from app.models import User as _User + + banner = ( + "2c2a 交互式 Shell\n" + "已预加载:app, settings, AsyncSessionLocal, Base, User\n" + "异步操作请用 asyncio.run() 包装。" + ) + namespace = { + "app": app, + "settings": _settings, + "AsyncSessionLocal": _Session, + "Base": _Base, + "User": _User, + "asyncio": __import__("asyncio"), + } + code.interact(banner=banner, local=namespace) + + +@server_app.command("check") +def check(): + """检查配置与依赖是否就绪。""" + from app.cli.utils import console + + console.print("\n[bold]配置检查[/bold]") + + # 环境检查 + checks = [] + + # 1. 密钥检查 + if settings.is_prod: + checks.append(("SECRET_KEY", bool(settings.secret_key))) + checks.append(("ED25519 私钥", bool(settings.ed25519_private_key_pem))) + checks.append(("ED25519 公钥", bool(settings.ed25519_public_key_pem))) + checks.append(("AES-GCM 主密钥", bool(settings.crypto_master_key_b64))) + checks.append(("缓存签名密钥", bool(settings.cache_signing_key))) + else: + checks.append(("开发模式密钥", True)) + + checks.append(("数据库引擎", settings.db_engine != "")) + checks.append(("Redis 启用", settings.redis_enabled)) + + for name, ok in checks: + status = "[green]✓[/green]" if ok else "[red]✗[/red]" + console.print(f" {status} {name}") + + # 2. 数据库连接检查 + console.print("\n[bold]数据库连接[/bold]") + import asyncio + + async def _check_db(): + from app.core.db import engine + + try: + async with engine.connect() as conn: + await conn.execute(__import__("sqlalchemy").text("SELECT 1")) + await engine.dispose() + return True + except Exception as e: # noqa: BLE001 + console.print(f" [red]✗[/red] 数据库连接失败: {e}") + return False + + if asyncio.run(_check_db()): + console.print(" [green]✓[/green] 数据库连接正常") + + # 3. Redis 连接检查 + if settings.redis_enabled: + console.print("\n[bold]Redis 连接[/bold]") + try: + from app.core.redis import get_redis + + async def _check_redis(): + r = await get_redis() + await r.ping() + await r.aclose() + + asyncio.run(_check_redis()) + console.print(" [green]✓[/green] Redis 连接正常") + except Exception as e: # noqa: BLE001 + console.print(f" [red]✗[/red] Redis 连接失败: {e}") + + console.print() + success("检查完成") diff --git a/app/cli/static.py b/app/cli/static.py new file mode 100644 index 0000000..69430fe --- /dev/null +++ b/app/cli/static.py @@ -0,0 +1,199 @@ +"""静态资源收集与密钥生成命令。 + +用法: + 2c2a collectstatic [dest] # 收集静态文件到指定目录 + 2c2a keys generate # 生成所有密钥(Ed25519/AES/BLAKE2b/SECRET_KEY) + 2c2a keys ed25519 # 仅生成 Ed25519 密钥对 + 2c2a keys aes # 仅生成 AES-GCM 主密钥 + 2c2a keys blake2b # 仅生成 keyed-BLAKE2b 签名密钥 + 2c2a keys secret # 仅生成 SECRET_KEY + 2c2a keys show # 显示当前已加载的密钥配置状态 +""" +from __future__ import annotations + +import base64 +import os +import secrets +import shutil +from pathlib import Path + +import typer + +from app.cli.utils import console, error, info, success, warn +from app.core.config import settings + +# keys 作为顶层命令组导出 +keys_app = typer.Typer(help="密钥生成", no_args_is_help=True) + + +def collectstatic( + destination: str = typer.Argument( + None, help="目标目录(默认 staticfiles/)" + ), + clear: bool = typer.Option(False, "--clear", "-c", help="先清空目标目录"), + dry_run: bool = typer.Option(False, "--dry-run", help="仅显示将复制哪些文件"), +): + """收集所有静态文件到指定目录(供 Nginx/CDN 直接服务)。 + + 收集来源: + - app/static/(应用自带静态资源) + - 各插件目录下的 static/(插件静态资源,如有) + """ + dest = Path(destination) if destination else Path("staticfiles") + src = Path(__file__).resolve().parent.parent / "static" + + if not src.is_dir(): + error(f"静态资源源目录不存在: {src}") + raise typer.Exit(1) + + if dry_run: + info(f"[dry-run] 将复制 {src} → {dest}") + for f in src.rglob("*"): + if f.is_file(): + console.print(f" {f.relative_to(src)}") + return + + if dest.exists(): + if clear: + warn(f"清空目标目录: {dest}") + shutil.rmtree(dest) + else: + error(f"目标目录已存在: {dest}(使用 --clear 清空后重试)") + raise typer.Exit(1) + + dest.mkdir(parents=True) + + # 复制应用静态资源 + copied = 0 + for f in src.rglob("*"): + if f.is_file(): + rel = f.relative_to(src) + target = dest / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(f, target) + copied += 1 + + # 复制插件静态资源(app/plugins/*/static/) + plugins_dir = Path(__file__).resolve().parent.parent / "plugins" + plugin_copied = 0 + for plugin_static in plugins_dir.glob("*/static"): + if plugin_static.is_dir(): + plugin_id = plugin_static.parent.name + target_dir = dest / "plugins" / plugin_id + target_dir.mkdir(parents=True, exist_ok=True) + for f in plugin_static.rglob("*"): + if f.is_file(): + rel = f.relative_to(plugin_static) + target = target_dir / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(f, target) + plugin_copied += 1 + + success(f"已收集 {copied} 个应用静态文件 → {dest}") + if plugin_copied: + success(f"已收集 {plugin_copied} 个插件静态文件 → {dest}/plugins/") + + +# ───────────────────────── 密钥生成 ───────────────────────── + + +@keys_app.command("generate") +def generate_all(): + """生成所有密钥(输出 .env 格式,可追加到 .env 文件)。""" + console.print("\n[bold]生成所有密钥(.env 格式)[/bold]\n") + _print_secret_key() + _print_aes_key() + _print_blake2b_key() + _print_ed25519_keys() + console.print("\n[dim]将以上内容追加到 .env 文件即可使用[/dim]") + + +@keys_app.command("secret") +def gen_secret(): + """生成 SECRET_KEY。""" + _print_secret_key() + + +@keys_app.command("aes") +def gen_aes(): + """生成 AES-GCM 主密钥(32 字节 base64)。""" + _print_aes_key() + + +@keys_app.command("blake2b") +def gen_blake2b(): + """生成 keyed-BLAKE2b 缓存签名密钥。""" + _print_blake2b_key() + + +@keys_app.command("ed25519") +def gen_ed25519(): + """生成 Ed25519 密钥对(用于 JWT 签名)。""" + _print_ed25519_keys() + + +@keys_app.command("show") +def show_keys(): + """显示当前已加载的密钥配置状态(不显示密钥本身)。""" + console.print("\n[bold]密钥配置状态[/bold]\n") + items = [ + ("SECRET_KEY", bool(settings.secret_key), "通用密钥"), + ("ED25519_PRIVATE_KEY_PEM", bool(settings.ed25519_private_key_pem), "JWT 签名私钥"), + ("ED25519_PUBLIC_KEY_PEM", bool(settings.ed25519_public_key_pem), "JWT 验签公钥"), + ("CRYPTO_MASTER_KEY_B64", bool(settings.crypto_master_key_b64), "AES-GCM 主密钥"), + ("CACHE_SIGNING_KEY", bool(settings.cache_signing_key), "BLAKE2b 缓存签名密钥"), + ] + for name, configured, desc in items: + status = "[green]✓ 已配置[/green]" if configured else "[red]✗ 未配置[/red]" + console.print(f" {status:30s} {name:30s} [dim]{desc}[/dim]") + + if settings.is_prod: + missing = [n for n, c, _ in items if not c] + if missing: + console.print(f"\n[red]⚠ 生产环境缺少必需密钥: {', '.join(missing)}[/red]") + else: + console.print("\n[green]✓ 所有密钥已就绪[/green]") + else: + console.print("\n[dim]开发模式:未配置的密钥将从 SECRET_KEY 自动派生[/dim]") + + +def _print_secret_key(): + key = secrets.token_urlsafe(48) + console.print(f"[cyan]# SECRET_KEY(通用密钥)[/cyan]") + console.print(f"SECRET_KEY={key}\n") + + +def _print_aes_key(): + key = base64.b64encode(os.urandom(32)).decode() + console.print(f"[cyan]# CRYPTO_MASTER_KEY_B64(AES-256-GCM 主密钥,32 字节 base64)[/cyan]") + console.print(f"CRYPTO_MASTER_KEY_B64={key}\n") + + +def _print_blake2b_key(): + key = secrets.token_urlsafe(32) + console.print(f"[cyan]# CACHE_SIGNING_KEY(keyed-BLAKE2b 缓存签名密钥)[/cyan]") + console.print(f"CACHE_SIGNING_KEY={key}\n") + + +def _print_ed25519_keys(): + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + k = Ed25519PrivateKey.generate() + priv = k.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ).decode() + pub = k.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() + + console.print(f"[cyan]# ED25519 密钥对(JWT 签名/验签)[/cyan]") + console.print("ED25519_PRIVATE_KEY_PEM=\"\"\"") + console.print(priv, end="") + console.print('"""') + console.print("ED25519_PUBLIC_KEY_PEM=\"\"\"") + console.print(pub, end="") + console.print('"""') diff --git a/app/cli/tenant.py b/app/cli/tenant.py new file mode 100644 index 0000000..a8d5a2a --- /dev/null +++ b/app/cli/tenant.py @@ -0,0 +1,310 @@ +"""租户(站点组)管理命令。 + +用法: + 2c2a tenant list # 列出所有站点组 + 2c2a tenant create # 创建站点组 + 2c2a tenant info # 查看站点组详情 + 2c2a tenant add-hostname # 绑定域名 + 2c2a tenant remove-hostname # 解绑域名 + 2c2a tenant add-admin # 添加站点组管理员 + 2c2a tenant remove-admin # 移除站点组管理员 + 2c2a tenant activate # 激活站点组 + 2c2a tenant deactivate # 停用站点组 + 2c2a tenant config # 查看/修改站点组配置 + 2c2a tenant invalidate-cache # 清除租户解析缓存 +""" +from __future__ import annotations + +import typer +from sqlalchemy import delete, select + +from app.cli.utils import ( + console, + db_session, + error, + info, + print_table, + run_async, + success, + warn, +) +from app.models.tenant import SiteGroup, SiteGroupConfig, SiteGroupHostname +from app.models.user import User + +tenant_app = typer.Typer(help="租户(站点组)管理", no_args_is_help=True) + + +async def _get_by_slug(session, slug: str) -> SiteGroup | None: + result = await session.execute(select(SiteGroup).where(SiteGroup.slug == slug)) + return result.scalar_one_or_none() + + +async def _get_by_name(session, name: str) -> SiteGroup | None: + result = await session.execute(select(SiteGroup).where(SiteGroup.name == name)) + return result.scalar_one_or_none() + + +async def _get_user(session, username: str) -> User | None: + result = await session.execute(select(User).where(User.username == username)) + return result.scalar_one_or_none() + + +@tenant_app.command("list") +def list_tenants(): + """列出所有站点组。""" + async def _do(): + async with db_session() as session: + result = await session.execute(select(SiteGroup).order_by(SiteGroup.id)) + groups = result.scalars().all() + rows = [] + for g in groups: + # 统计域名数 + host_result = await session.execute( + select(SiteGroupHostname).where(SiteGroupHostname.site_group_id == g.id) + ) + host_count = len(host_result.scalars().all()) + rows.append([ + g.id, + g.name, + g.slug, + "✓" if g.is_active else "✗", + g.site_name or "-", + host_count, + len(g.admins), + ]) + return rows + + rows = run_async(_do()) + print_table( + f"站点组列表(共 {len(rows)} 个)", + ["ID", "名称", "Slug", "启用", "站点名", "域名数", "管理员数"], + rows, + ) + + +@tenant_app.command("create") +def create_tenant( + name: str = typer.Argument(..., help="站点组名称"), + slug: str = typer.Option(None, "--slug", "-s", help="Slug(默认自动生成)"), + site_name: str = typer.Option(None, "--site-name", help="站点显示名"), + description: str = typer.Option("", "--desc", "-d", help="描述"), +): + """创建站点组。""" + import re + + if not slug: + # 尝试从名称生成 slug(仅保留 a-z0-9-),无法生成则用随机串 + slug = re.sub(r"[^a-zA-Z0-9-]", "-", name.lower()).strip("-") + if not slug: + import secrets + + slug = f"site-{secrets.token_hex(4)}" + if not re.match(r"^[a-z0-9-]+$", slug): + error("slug 只能包含小写字母、数字、连字符") + raise typer.Exit(1) + + async def _do(): + async with db_session() as session: + existing = await _get_by_slug(session, slug) + if existing: + error(f"slug {slug} 已存在") + raise typer.Exit(1) + sg = SiteGroup( + name=name, + slug=slug, + site_name=site_name or name, + description=description, + is_active=True, + ) + session.add(sg) + await session.flush() + # 创建空配置覆盖 + session.add(SiteGroupConfig(site_group_id=sg.id)) + return sg.id + + sg_id = run_async(_do()) + success(f"站点组已创建: {name} (slug={slug}, id={sg_id})") + + +@tenant_app.command("info") +def tenant_info(slug: str = typer.Argument(..., help="站点组 slug")): + """查看站点组详情。""" + async def _do(): + async with db_session() as session: + sg = await _get_by_slug(session, slug) + if sg is None: + error(f"站点组 {slug} 不存在") + raise typer.Exit(1) + # 域名列表 + host_result = await session.execute( + select(SiteGroupHostname).where(SiteGroupHostname.site_group_id == sg.id) + ) + hostnames = host_result.scalars().all() + return sg, hostnames + + sg, hostnames = run_async(_do()) + console.print(f"\n[bold]站点组详情[/bold]") + console.print(f" ID: {sg.id}") + console.print(f" 名称: {sg.name}") + console.print(f" Slug: {sg.slug}") + console.print(f" 启用: {'✓' if sg.is_active else '✗'}") + console.print(f" 站点名: {sg.site_name or '-'}") + console.print(f" 描述: {sg.description or '-'}") + console.print(f" 管理员: {', '.join(u.username for u in sg.admins) or '-'}") + console.print(f" 成员数: {len(sg.members)}") + console.print(f"\n [bold]绑定域名({len(hostnames)}):[/bold]") + for h in hostnames: + console.print(f" • {h.hostname}") + + +@tenant_app.command("add-hostname") +def add_hostname( + slug: str = typer.Argument(..., help="站点组 slug"), + hostname: str = typer.Argument(..., help="要绑定的域名(如 example.com)"), +): + """绑定域名到站点组。""" + hostname = hostname.lower().strip() + + async def _do(): + async with db_session() as session: + sg = await _get_by_slug(session, slug) + if sg is None: + error(f"站点组 {slug} 不存在") + raise typer.Exit(1) + # 检查域名是否已绑定 + existing = await session.execute( + select(SiteGroupHostname).where(SiteGroupHostname.hostname == hostname) + ) + if existing.scalar_one_or_none(): + error(f"域名 {hostname} 已绑定到其他站点组") + raise typer.Exit(1) + session.add(SiteGroupHostname(hostname=hostname, site_group_id=sg.id)) + return sg.name + + name = run_async(_do()) + # 清除缓存 + run_async(_invalidate(hostname)) + success(f"域名 {hostname} 已绑定到站点组 {name}") + + +@tenant_app.command("remove-hostname") +def remove_hostname(hostname: str = typer.Argument(..., help="要解绑的域名")): + """解绑域名。""" + hostname = hostname.lower().strip() + + async def _do(): + async with db_session() as session: + result = await session.execute( + select(SiteGroupHostname).where(SiteGroupHostname.hostname == hostname) + ) + h = result.scalar_one_or_none() + if h is None: + error(f"域名 {hostname} 未绑定") + raise typer.Exit(1) + await session.delete(h) + + run_async(_do()) + run_async(_invalidate(hostname)) + success(f"域名 {hostname} 已解绑") + + +@tenant_app.command("add-admin") +def add_admin( + slug: str = typer.Argument(..., help="站点组 slug"), + username: str = typer.Argument(..., help="用户名"), +): + """添加站点组管理员。""" + async def _do(): + async with db_session() as session: + sg = await _get_by_slug(session, slug) + if sg is None: + error(f"站点组 {slug} 不存在") + raise typer.Exit(1) + user = await _get_user(session, username) + if user is None: + error(f"用户 {username} 不存在") + raise typer.Exit(1) + if user not in sg.admins: + sg.admins.append(user) + + run_async(_do()) + success(f"{username} 已成为站点组 {slug} 的管理员") + + +@tenant_app.command("remove-admin") +def remove_admin( + slug: str = typer.Argument(..., help="站点组 slug"), + username: str = typer.Argument(..., help="用户名"), +): + """移除站点组管理员。""" + async def _do(): + async with db_session() as session: + sg = await _get_by_slug(session, slug) + if sg is None: + error(f"站点组 {slug} 不存在") + raise typer.Exit(1) + user = await _get_user(session, username) + if user and user in sg.admins: + sg.admins.remove(user) + + run_async(_do()) + success(f"{username} 已移出站点组 {slug} 的管理员") + + +@tenant_app.command("activate") +def activate_tenant(slug: str = typer.Argument(..., help="站点组 slug")): + """激活站点组。""" + _set_active(slug, True) + + +@tenant_app.command("deactivate") +def deactivate_tenant(slug: str = typer.Argument(..., help="站点组 slug")): + """停用站点组。""" + _set_active(slug, False) + + +def _set_active(slug: str, active: bool): + async def _do(): + async with db_session() as session: + sg = await _get_by_slug(session, slug) + if sg is None: + error(f"站点组 {slug} 不存在") + raise typer.Exit(1) + sg.is_active = active + + run_async(_do()) + success(f"站点组 {slug} 已{'激活' if active else '停用'}") + + +@tenant_app.command("invalidate-cache") +def invalidate_cache( + hostname: str = typer.Option(None, "--hostname", "-h", help="仅清除指定域名的缓存"), +): + """清除租户解析缓存(域名 → 站点组)。""" + if hostname: + run_async(_invalidate(hostname.lower().strip())) + success(f"已清除 {hostname} 的租户缓存") + else: + from app.core.config import settings + from app.core.redis import get_redis + + async def _clear_all(): + if not settings.redis_enabled: + warn("Redis 未启用,无缓存可清除") + return 0 + r = await get_redis() + count = 0 + async for key in r.scan_iter(match="tenant:host:*", count=100): + await r.delete(key) + count += 1 + await r.aclose() + return count + + count = run_async(_clear_all()) + success(f"已清除 {count} 个租户缓存键") + + +async def _invalidate(hostname: str): + from app.tenant.resolver import invalidate_tenant_cache + + await invalidate_tenant_cache(hostname) diff --git a/app/cli/utils.py b/app/cli/utils.py new file mode 100644 index 0000000..1930d49 --- /dev/null +++ b/app/cli/utils.py @@ -0,0 +1,82 @@ +"""CLI 共享工具:异步运行、表格输出、密码交互、数据库会话。""" +from __future__ import annotations + +import asyncio +import hashlib +import getpass +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import asynccontextmanager +from typing import Any + +import typer +from rich.console import Console +from rich.table import Table +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.db import AsyncSessionLocal + +console = Console() + + +def run_async(coro: Awaitable[Any]) -> Any: + """在同步 CLI 中运行异步协程。""" + return asyncio.run(coro) + + +@asynccontextmanager +async def db_session() -> AsyncIterator[AsyncSession]: + """提供异步数据库会话上下文(CLI 专用,自动提交)。""" + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +def blake2b_prehash_interactive(prompt: str = "密码") -> str: + """交互式输入密码并做 BLAKE2b 预哈希(与前端流程一致)。 + + 用于 CLI 创建/修改密码场景,保证与 Web 端密码哈希链路一致: + 原始密码 → BLAKE2b 预哈希 → Argon2id 慢哈希。 + """ + pw = getpass.getpass(f"{prompt}: ") + pw2 = getpass.getpass(f"确认{prompt}: ") + if pw != pw2: + raise typer.BadParameter("两次输入的密码不一致") + if len(pw) < 8: + raise typer.BadParameter("密码至少 8 位") + # BLAKE2b 预哈希(与前端一致,防 DoS 截断) + return hashlib.blake2b(pw.encode(), digest_size=64).hexdigest() + + +def print_table(title: str, columns: list[str], rows: list[list[Any]]) -> None: + """用 rich 输出对齐表格。""" + table = Table(title=title, show_lines=False) + for col in columns: + table.add_column(col, overflow="fold") + for row in rows: + table.add_row(*[str(c) for c in row]) + console.print(table) + + +def confirm(message: str, default: bool = False) -> bool: + """危险操作确认。""" + return typer.confirm(message, default=default) + + +def success(message: str) -> None: + console.print(f"[green]✓[/green] {message}") + + +def error(message: str) -> None: + console.print(f"[red]✗[/red] {message}") + + +def info(message: str) -> None: + console.print(f"[cyan]ℹ[/cyan] {message}") + + +def warn(message: str) -> None: + console.print(f"[yellow]![/yellow] {message}") diff --git a/pyproject.toml b/pyproject.toml index 2c7bfb7..bec85ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,8 +41,14 @@ dependencies = [ "httpx>=0.27.0", "structlog>=24.4.0", "python-dotenv>=1.0.1", + # CLI 管理工具 + "typer>=0.12.0", + "rich>=13.7.0", ] +[project.scripts] +2c2a = "app.cli.main:app" + [project.optional-dependencies] dev = [ "pytest>=8.3.0", From 1f316cf63f718b440b2c94159498e43d1d545c3a Mon Sep 17 00:00:00 2001 From: trustedinster Date: Fri, 19 Jun 2026 09:51:41 +0000 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E9=AB=98=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=A8=A1=E5=9D=97=E5=8C=96=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- AGENTS.md | 57 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4784030..0327de5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,38 @@ -<2c2a_iron_laws> -🚨 VIOLATION = SEVERE ERROR: -1. All Python cmds MUST use `uv run`. NO `pip`, NO bare `python`. -2. NO `django-admin` for provider/user dashboard. Build custom views. -3. NO raw `